From 3e3fa5c7903977d06a77b67023b0737b8737d6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=ED=98=81?= Date: Thu, 16 Jan 2025 13:24:17 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20#211?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S3 버전 관리 기능을 활용한 드라이브 휴지통 API 구현 --- build.gradle | 3 + .../classfit/common/config/S3Config.java | 23 ++- .../classfit/common/util/DriveUtil.java | 34 +--- .../controller/DriveFolderController.java | 14 -- .../controller/DriveTrashController.java | 21 +- .../drive/service/DriveDeleteService.java | 63 ++++-- .../drive/service/DriveFolderService.java | 13 -- .../drive/service/DriveRestoreService.java | 63 +++--- .../drive/service/DriveTrashService.java | 180 ++++++++++++------ .../drive/service/DriveUploadService.java | 1 - 10 files changed, 245 insertions(+), 170 deletions(-) diff --git a/build.gradle b/build.gradle index e9af6590..24f772fc 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' // swagger implementation 'net.nurigo:sdk:4.3.0' + //jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.3' implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' @@ -48,6 +49,8 @@ dependencies { //s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'software.amazon.awssdk:s3:2.20.56' + } tasks.named('test') { diff --git a/src/main/java/classfit/example/classfit/common/config/S3Config.java b/src/main/java/classfit/example/classfit/common/config/S3Config.java index 45012e4d..ed140918 100644 --- a/src/main/java/classfit/example/classfit/common/config/S3Config.java +++ b/src/main/java/classfit/example/classfit/common/config/S3Config.java @@ -7,23 +7,42 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; @Configuration public class S3Config { + @Value("${cloud.aws.credentials.access-key}") private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") private String secretKey; + @Value("${cloud.aws.region.static}") private String region; @Bean - public AmazonS3Client amazonS3Client(){ + public AmazonS3Client amazonS3Client() { return (AmazonS3Client) AmazonS3ClientBuilder.standard() .withCredentials( - new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey,secretKey)) + new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)) ) .withRegion(region) .build(); } + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey) + ) + ) + .region(Region.of(region)) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/classfit/example/classfit/common/util/DriveUtil.java b/src/main/java/classfit/example/classfit/common/util/DriveUtil.java index f3b95cdd..ec14c2ea 100644 --- a/src/main/java/classfit/example/classfit/common/util/DriveUtil.java +++ b/src/main/java/classfit/example/classfit/common/util/DriveUtil.java @@ -6,13 +6,10 @@ import classfit.example.classfit.drive.dto.response.FileResponse; import classfit.example.classfit.member.domain.Member; import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.amazonaws.services.s3.model.Tag; -import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpStatus; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.List; import java.util.Map; import static classfit.example.classfit.drive.domain.DriveType.PERSONAL; @@ -32,22 +29,6 @@ public static String generatedOriginPath(Member member, DriveType driveType, Str throw new ClassfitException("지원하지 않는 드라이브 타입입니다.", HttpStatus.NO_CONTENT); } - public static String generateTrashPath(Member member, DriveType driveType, String fileName) { - - String basePath; - - if (driveType == DriveType.PERSONAL) { - basePath = String.format("trash/personal/%d/", member.getId()); - } else if (driveType == DriveType.SHARED) { - Long academyId = member.getAcademy().getId(); - basePath = String.format("trash/shared/%d/", academyId); - } else { - throw new ClassfitException("지원하지 않는 드라이브 타입입니다.", HttpStatus.NO_CONTENT); - } - - return (fileName != null && !fileName.trim().isEmpty()) ? basePath + fileName : basePath; - } - public static String buildPrefix(DriveType driveType, Member member, String folderPath) { String basePrefix; @@ -65,15 +46,6 @@ public static String buildPrefix(DriveType driveType, Member member, String fold return basePrefix + folderPath + "/"; } - public static String extractTags(List tags, String tagKey) { - return tags.stream() - .filter(tag -> tag.getKey().equals(tagKey)) - .map(Tag::getValue) - .findFirst() - .orElse(""); - } - - @NotNull public static FileResponse getFileResponse(S3ObjectSummary summary, String fileName, String fileUrl, Map tagMap) { @@ -120,12 +92,10 @@ private static LocalDateTime parseUploadedAt(String uploadedAtStr) { private static String formatFileSize(long sizeInBytes) { double sizeInKB = sizeInBytes / 1024.0; if (sizeInKB >= 1024) { - // 1MB 이상이면 MB 단위로 표시 double sizeInMB = sizeInKB / 1024.0; - return String.format("%.1f MB", sizeInMB); // 소수점 한 자리까지 MB로 표시 + return String.format("%.1f MB", sizeInMB); } - // 1MB 미만이면 KB 단위로 표시 - return String.format("%.1f KB", sizeInKB); // 소수점 한 자리까지 KB로 표시 + return String.format("%.1f KB", sizeInKB); } private static String getFileNameWithoutPrefix(String objectKey) { diff --git a/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java b/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java index aa5ff523..c6f6e578 100644 --- a/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java +++ b/src/main/java/classfit/example/classfit/drive/controller/DriveFolderController.java @@ -8,7 +8,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Nullable; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -49,17 +48,4 @@ public ApiResponse> getFolders( List folders = driveFolderService.getFolders(member, driveType, folderPath); return ApiResponse.success(folders, 200, "폴더 목록 조회 성공"); } - - @DeleteMapping("/folder") - @Operation(summary = "폴더 삭제", description = "폴더를 삭제하는 API입니다.") - public ApiResponse deleteFolder( - @AuthMember Member member, - @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") - @RequestParam DriveType driveType, - @Parameter(description = "삭제할 폴더 이름입니다.") - @RequestParam String folderName - ) { - driveFolderService.deleteFolder(member, driveType, folderName); - return ApiResponse.success(null, 200, "SUCCESS"); - } } diff --git a/src/main/java/classfit/example/classfit/drive/controller/DriveTrashController.java b/src/main/java/classfit/example/classfit/drive/controller/DriveTrashController.java index a07af492..870b378f 100644 --- a/src/main/java/classfit/example/classfit/drive/controller/DriveTrashController.java +++ b/src/main/java/classfit/example/classfit/drive/controller/DriveTrashController.java @@ -32,15 +32,17 @@ public class DriveTrashController { public ApiResponse> trashList( @AuthMember Member member, @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") - @RequestParam DriveType driveType + @RequestParam DriveType driveType, + @Parameter(description = "폴더 경로입니다. 비어 있으면 루트 폴더로 지정됩니다.") + @RequestParam(required = false, defaultValue = "") String folderPath ) { - List filesFromTrash = driveTrashService.getFilesFromTrash(member, driveType); + List filesFromTrash = driveTrashService.getFilesFromTrash(member, driveType, folderPath); return ApiResponse.success(filesFromTrash, 200, "조회 성공"); } @PostMapping("/trash") @Operation(summary = "휴지통 이동", description = "휴지통 이동 API 입니다.") - public ApiResponse> moveToTrash( + public ApiResponse> storeTrash( @AuthMember Member member, @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") @RequestParam DriveType driveType, @@ -49,20 +51,20 @@ public ApiResponse> moveToTrash( @Parameter(description = "파일 이름") @RequestParam List fileNames ) { - List trashPathList = driveTrashService.moveToTrash(member, driveType, folderPath, fileNames); + List trashPathList = driveTrashService.storeTrash(member, driveType, folderPath, fileNames); return ApiResponse.success(trashPathList, 200, "휴지통 이동 완료"); } @PostMapping("/trash/restore") @Operation(summary = "휴지통 복원", description = "휴지통 복원 API 입니다.") - public ApiResponse> restoreFromTrash( + public ApiResponse> restoreTrash( @AuthMember Member member, @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") @RequestParam DriveType driveType, @Parameter(description = "파일 이름") @RequestParam List fileNames ) { - List restorePathList = driveRestoreService.restoreFromTrash(member, driveType, fileNames); + List restorePathList = driveRestoreService.restoreTrash(member, driveType, fileNames); return ApiResponse.success(restorePathList, 200, "복원 성공"); } @@ -72,10 +74,13 @@ public ApiResponse deleteFromTrash( @AuthMember Member member, @Parameter(description = "내 드라이브는 PERSONAL, 공용 드라이브는 SHARED 입니다.") @RequestParam DriveType driveType, + @Parameter(description = "폴더 경로입니다. 비어 있으면 루트 폴더로 지정됩니다.") + @RequestParam(required = false, defaultValue = "") String folderPath, @Parameter(description = "파일 이름") - @RequestParam List fileName + @RequestParam List fileNames + ) { - driveDeleteService.deleteFromTrash(member, driveType, fileName); + driveDeleteService.deleteFromTrash(member, driveType, folderPath, fileNames); return ApiResponse.success(null, 200, "삭제 성공"); } } diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveDeleteService.java b/src/main/java/classfit/example/classfit/drive/service/DriveDeleteService.java index 496e0178..90be1105 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveDeleteService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveDeleteService.java @@ -1,43 +1,66 @@ package classfit.example.classfit.drive.service; -import classfit.example.classfit.common.exception.ClassfitException; +import classfit.example.classfit.common.util.DriveUtil; import classfit.example.classfit.drive.domain.DriveType; import classfit.example.classfit.member.domain.Member; -import com.amazonaws.services.s3.AmazonS3; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest; +import software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; import java.util.List; @Service @RequiredArgsConstructor public class DriveDeleteService { - private final AmazonS3 amazonS3; + private final S3Client s3Client; @Value("${cloud.aws.s3.bucket}") private String bucketName; - public void deleteFromTrash(Member member, DriveType driveType, List fileNames) { - for (String fileName : fileNames) { + public void deleteFromTrash(Member member, DriveType driveType, String folderPath, List fileNames) { + + fileNames.forEach(fileName -> { + String key = DriveUtil.generatedOriginPath(member, driveType, folderPath, fileName); + try { - String trashPath = generateTrashPath(member, driveType, fileName); - amazonS3.deleteObject(bucketName, trashPath); - } catch (Exception e) { - System.err.println("파일 삭제 중 오류 발생: " + fileName); + ListObjectVersionsRequest listVersionsRequest = ListObjectVersionsRequest.builder() + .bucket(bucketName) + .prefix(key) + .build(); + + ListObjectVersionsResponse response = s3Client.listObjectVersions(listVersionsRequest); + + response.versions().forEach(version -> { + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(version.key()) + .versionId(version.versionId()) + .build(); + + s3Client.deleteObject(deleteRequest); + }); + + response.deleteMarkers().forEach(deleteMarker -> { + DeleteObjectRequest deleteMarkerRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(deleteMarker.key()) + .versionId(deleteMarker.versionId()) + .build(); + + s3Client.deleteObject(deleteMarkerRequest); + }); + + } catch (S3Exception e) { + System.err.println("Error deleting file: " + key + ", Error: " + e.awsErrorDetails().errorMessage()); } - } + }); } - private String generateTrashPath(Member member, DriveType driveType, String fileName) { - if (driveType == DriveType.PERSONAL) { - return String.format("trash/personal/%d/%s", member.getId(), fileName); - } else if (driveType == DriveType.SHARED) { - Long academyId = member.getAcademy().getId(); - return String.format("trash/shared/%d/%s", academyId, fileName); - } - throw new ClassfitException("지원하지 않는 드라이브 타입입니다.", HttpStatus.NO_CONTENT); - } + } diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java b/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java index 05514121..d88f3ac1 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveFolderService.java @@ -2,7 +2,6 @@ import classfit.example.classfit.common.util.DriveUtil; import classfit.example.classfit.drive.domain.DriveType; -import classfit.example.classfit.drive.dto.response.FileResponse; import classfit.example.classfit.member.domain.Member; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.*; @@ -101,16 +100,4 @@ public List getFolders(Member member, DriveType driveType, String folder .map(folder -> folder.substring(prefix.length())) .collect(Collectors.toList()); } - - public void deleteFolder(Member member, DriveType driveType, String folderName) { - List filesFromS3 = driveGetService.getFilesFromS3(member, driveType, folderName); - List strings = filesFromS3.stream() - .map(FileResponse::fileName) - .collect(Collectors.toList()); - - driveTrashService.moveToTrash(member, driveType, folderName, strings); - - String prefix = DriveUtil.buildPrefix(driveType, member, folderName); - amazonS3.deleteObject(new DeleteObjectRequest(bucketName, prefix)); - } } diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveRestoreService.java b/src/main/java/classfit/example/classfit/drive/service/DriveRestoreService.java index 67044178..ba9f5640 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveRestoreService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveRestoreService.java @@ -3,53 +3,72 @@ import classfit.example.classfit.common.util.DriveUtil; import classfit.example.classfit.drive.domain.DriveType; import classfit.example.classfit.member.domain.Member; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.*; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class DriveRestoreService { - private final AmazonS3 amazonS3; + private final S3Client s3Client; @Value("${cloud.aws.s3.bucket}") private String bucketName; - public List restoreFromTrash(Member member, DriveType driveType, List fileNames) { + public List restoreTrash(Member member, DriveType driveType, List fileNames) { List restoredPaths = new ArrayList<>(); for (String fileName : fileNames) { - String trashPath = DriveUtil.generateTrashPath(member, driveType, fileName); - List filteredTags = getFilteredTags(trashPath); - String folderPath = DriveUtil.extractTags(filteredTags, "folderPath"); + String prefix = DriveUtil.generatedOriginPath(member, driveType, "", fileName); + List deleteMarkers = listDeleteMarkers(prefix); - String restoredPath = DriveUtil.generatedOriginPath(member, driveType, folderPath, fileName); - CopyObjectRequest copyRequest = new CopyObjectRequest(bucketName, trashPath, bucketName, restoredPath); - amazonS3.copyObject(copyRequest); + for (ObjectVersion deleteMarker : deleteMarkers) { + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(deleteMarker.key()) + .versionId(deleteMarker.versionId()) + .build(); - amazonS3.deleteObject(bucketName, trashPath); - restoredPaths.add(restoredPath); + s3Client.deleteObject(deleteRequest); + restoredPaths.add(deleteMarker.key()); + } } - return restoredPaths; } - private List getFilteredTags(String trashPath) { - GetObjectTaggingRequest taggingRequest = new GetObjectTaggingRequest(bucketName, trashPath); - GetObjectTaggingResult taggingResult = amazonS3.getObjectTagging(taggingRequest); - List filteredTags = taggingResult.getTagSet().stream() - .filter(tag -> !tag.getKey().equals("deletedBy") && !tag.getKey().equals("deleteAt")) - .collect(Collectors.toList()); + private List listDeleteMarkers(String prefix) { + List deleteMarkers = new ArrayList<>(); + + ListObjectVersionsRequest request = ListObjectVersionsRequest.builder() + .bucket(bucketName) + .prefix(prefix) + .build(); + + ListObjectVersionsResponse response; + do { + response = s3Client.listObjectVersions(request); + + for (DeleteMarkerEntry deleteMarkerEntry : response.deleteMarkers()) { + deleteMarkers.add( + ObjectVersion.builder() + .key(deleteMarkerEntry.key()) + .versionId(deleteMarkerEntry.versionId()) + .build() + ); + } - amazonS3.setObjectTagging(new SetObjectTaggingRequest(bucketName, trashPath, new ObjectTagging(filteredTags))); - return filteredTags; + request = request.toBuilder() + .keyMarker(response.nextKeyMarker()) + .versionIdMarker(response.nextVersionIdMarker()) + .build(); + } while (response.isTruncated()); + return deleteMarkers; } } diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java b/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java index de794957..dffbb4b1 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveTrashService.java @@ -5,99 +5,163 @@ import classfit.example.classfit.drive.dto.response.FileResponse; import classfit.example.classfit.member.domain.Member; import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.*; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.ListObjectsV2Request; +import com.amazonaws.services.s3.model.ListObjectsV2Result; +import com.amazonaws.services.s3.model.S3ObjectSummary; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; -import java.util.stream.Stream; @Service @RequiredArgsConstructor public class DriveTrashService { private final AmazonS3 amazonS3; + private final S3Client s3Client; @Value("${cloud.aws.s3.bucket}") private String bucketName; - public List getFilesFromTrash(Member member, DriveType driveType) { - String prefix = DriveUtil.generateTrashPath(member, driveType, null); + public List getFilesFromTrash(Member member, DriveType driveType, String folderPath) { + List deletedFiles = new ArrayList<>(); + String prefix = DriveUtil.buildPrefix(driveType, member, folderPath); - ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request() - .withBucketName(bucketName) - .withPrefix(prefix); - List objectSummaries = amazonS3.listObjectsV2(listObjectsV2Request).getObjectSummaries(); + ListObjectVersionsRequest request = ListObjectVersionsRequest.builder() + .bucket(bucketName) + .prefix(prefix) + .build(); - return objectSummaries.stream() - .map(this::buildFileTrashInfo) - .toList(); - } + ListObjectVersionsResponse response; + do { + response = s3Client.listObjectVersions(request); - private FileResponse buildFileTrashInfo(S3ObjectSummary summary) { - String fileName = summary.getKey(); - String fileUrl = amazonS3.getUrl(bucketName, fileName).toString(); + ListObjectVersionsResponse finalResponse = response; + response.deleteMarkers().forEach(deleteMarker -> { + String key = deleteMarker.key(); + String deleteMarkerVersionId = deleteMarker.versionId(); - Map tagMap = amazonS3.getObjectTagging(new GetObjectTaggingRequest(bucketName, fileName)) - .getTagSet().stream() - .collect(Collectors.toMap(Tag::getKey, Tag::getValue)); + String relativePath = key.substring(prefix.length()); - return DriveUtil.getFileResponse(summary, fileName, fileUrl, tagMap); - } + if (relativePath.contains("/") && relativePath.indexOf("/") != relativePath.length() - 1) { + return; + } - public List moveToTrash(Member member, DriveType driveType, String folderPath, List fileNames) { - List trashPaths = new ArrayList<>(); + Optional previousVersionOpt = finalResponse.versions().stream() + .filter(version -> version.key().equals(key)) + .filter(version -> !version.versionId().equals(deleteMarkerVersionId)) + .findFirst(); - for (String fileName : fileNames) { - String originPath = DriveUtil.generatedOriginPath(member, driveType, folderPath, fileName); - String trashPath = DriveUtil.generateTrashPath(member, driveType, fileName); + Map tagMap = new HashMap<>(); + long fileSize = 0; + if (previousVersionOpt.isPresent()) { + ObjectVersion previousVersion = previousVersionOpt.get(); + fileSize = previousVersion.size(); + + GetObjectTaggingRequest taggingRequest = GetObjectTaggingRequest.builder() + .bucket(bucketName) + .key(key) + .versionId(previousVersion.versionId()) + .build(); + + GetObjectTaggingResponse taggingResponse = s3Client.getObjectTagging(taggingRequest); + tagMap = taggingResponse.tagSet().stream() + .collect(Collectors.toMap(Tag::key, Tag::value)); + } + + S3ObjectSummary summary = new S3ObjectSummary(); + summary.setKey(key); + summary.setSize(fileSize); + + String fileUrl = s3Client.utilities().getUrl(builder -> + builder.bucket(bucketName).key(key)).toExternalForm(); + + FileResponse fileResponse = DriveUtil.getFileResponse(summary, key, fileUrl, tagMap); + deletedFiles.add(fileResponse); + }); + + request = request.toBuilder() + .keyMarker(response.nextKeyMarker()) + .versionIdMarker(response.nextVersionIdMarker()) + .build(); + + } while (response.isTruncated()); + + if (!folderPath.isEmpty()) { + deletedFiles.removeFirst(); + } + + return deletedFiles; + } - CopyObjectRequest copyRequest = new CopyObjectRequest(bucketName, originPath, bucketName, trashPath); - amazonS3.copyObject(copyRequest); - addDeleteTagsToS3Object(trashPath, member); - amazonS3.deleteObject(bucketName, originPath); + public List storeTrash(Member member, DriveType driveType, String folderPath, List fileNames) { + List trashPaths = new ArrayList<>(); - trashPaths.add(trashPath); + for (String fileName : fileNames) { + if (fileName.endsWith("/")) { + List objectsToDelete = fileInFolder(member, driveType, fileName); + deleteObjects(objectsToDelete); + trashPaths.addAll(objectsToDelete); + } else { + String originPath = cleanPath(DriveUtil.generatedOriginPath(member, driveType, folderPath, fileName)); + amazonS3.deleteObject(bucketName, originPath); + trashPaths.add(originPath); + } } - createPlaceHolder(member, driveType, folderPath); return trashPaths; } - private void createPlaceHolder(Member member, DriveType driveType, String folderPath) { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(0); + private List fileInFolder(Member member, DriveType driveType, String folderPath) { + List files = new ArrayList<>(); + + String prefix = cleanPath(DriveUtil.generatedOriginPath(member, driveType, folderPath, "")); + if (!prefix.endsWith("/")) { + prefix += "/"; + } + + ListObjectsV2Request request = new ListObjectsV2Request() + .withBucketName(bucketName) + .withPrefix(prefix); - String fullFolderPath = DriveUtil.generatedOriginPath(member, driveType, folderPath, ""); - InputStream emptyContent = new ByteArrayInputStream(new byte[0]); - amazonS3.putObject(new PutObjectRequest(bucketName, fullFolderPath, emptyContent, metadata)); + ListObjectsV2Result result; + do { + result = amazonS3.listObjectsV2(request); + for (S3ObjectSummary objectSummary : result.getObjectSummaries()) { + String key = objectSummary.getKey(); + files.add(key); + } + request.setContinuationToken(result.getNextContinuationToken()); + } while (result.isTruncated()); + + return files; } - private void addDeleteTagsToS3Object(String objectKey, Member member) { - GetObjectTaggingRequest getTaggingRequest = new GetObjectTaggingRequest(bucketName, objectKey); - GetObjectTaggingResult taggingResult = amazonS3.getObjectTagging(getTaggingRequest); + private void deleteObjects(List objectKeys) { + if (objectKeys.isEmpty()) { + return; + } - List existingTags = taggingResult.getTagSet(); - LocalDateTime now = LocalDateTime.now(); - String formattedDate = now.format(DateTimeFormatter.ISO_DATE_TIME); + List keysToDelete = objectKeys.stream() + .map(DeleteObjectsRequest.KeyVersion::new) + .collect(Collectors.toList()); - List updatedTags = Stream.concat( - existingTags.stream(), - Stream.of( - new Tag("deletedBy", member.getName()), - new Tag("deleteAt", formattedDate) - ) - ).collect(Collectors.toList()); + DeleteObjectsRequest deleteRequest = new DeleteObjectsRequest(bucketName) + .withKeys(keysToDelete); - amazonS3.setObjectTagging(new SetObjectTaggingRequest(bucketName, objectKey, new ObjectTagging(updatedTags))); + amazonS3.deleteObjects(deleteRequest); + } + + private String cleanPath(String path) { + if (path == null) { + return ""; + } + return path.replaceAll("/+", "/"); } } diff --git a/src/main/java/classfit/example/classfit/drive/service/DriveUploadService.java b/src/main/java/classfit/example/classfit/drive/service/DriveUploadService.java index ffdb9755..537fba1b 100644 --- a/src/main/java/classfit/example/classfit/drive/service/DriveUploadService.java +++ b/src/main/java/classfit/example/classfit/drive/service/DriveUploadService.java @@ -67,7 +67,6 @@ private void addTagsToS3Object(String objectKey, Member member, String folderPat LocalDateTime now = LocalDateTime.now(); String formattedDate = now.format(DateTimeFormatter.ISO_DATE_TIME); String finalFolderPath = folderPath != null && !folderPath.trim().isEmpty() ? folderPath : ""; - finalFolderPath = finalFolderPath + "/"; List tags = List.of( new Tag("folderPath", finalFolderPath),