diff --git a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/entity/FileMetaData.java b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/entity/FileMetaData.java index 7b19293b..301c9871 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/entity/FileMetaData.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/entity/FileMetaData.java @@ -25,13 +25,9 @@ public class FileMetaData extends BaseEntity { @Column(nullable = false) private FileCategory fileCategory; - @Column(nullable = false) - private String fileName; - @Builder - public FileMetaData(FileCategory fileCategory, UUID fileId, String fileName) { + private FileMetaData(FileCategory fileCategory, UUID fileId) { this.fileId = fileId; this.fileCategory = fileCategory; - this.fileName = fileName; } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataService.java b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataService.java index 8045842a..d0396906 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataService.java @@ -1,9 +1,9 @@ package ddingdong.ddingdongBE.domain.filemetadata.service; -import ddingdong.ddingdongBE.domain.filemetadata.entity.FileMetaData; -import java.util.UUID; +import ddingdong.ddingdongBE.domain.filemetadata.service.dto.CreateFileMetaDataCommand; public interface FacadeFileMetaDataService { - FileMetaData getFileUrlWithMetaData(UUID fileId); + void create(CreateFileMetaDataCommand command); + } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataServiceImpl.java b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataServiceImpl.java index c0bc4f13..30a3e80a 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataServiceImpl.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/FacadeFileMetaDataServiceImpl.java @@ -1,6 +1,6 @@ package ddingdong.ddingdongBE.domain.filemetadata.service; -import ddingdong.ddingdongBE.domain.filemetadata.entity.FileMetaData; +import ddingdong.ddingdongBE.domain.filemetadata.service.dto.CreateFileMetaDataCommand; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -14,8 +14,14 @@ public class FacadeFileMetaDataServiceImpl implements FacadeFileMetaDataService private final FileMetaDataService fileMetaDataService; @Override - public FileMetaData getFileUrlWithMetaData(UUID fileId) { - return fileMetaDataService.getByFileId(fileId); + public void create(CreateFileMetaDataCommand command) { + String fileId = extractFilename(command.key()); + fileMetaDataService.create(command.toEntity(UUID.fromString(fileId))); + } + + private String extractFilename(String key) { + String[] splitKey = key.split("/"); + return splitKey[splitKey.length - 1]; } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/GeneralFileMetaDataService.java b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/GeneralFileMetaDataService.java index 6dc09e62..af81136a 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/GeneralFileMetaDataService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/GeneralFileMetaDataService.java @@ -15,8 +15,8 @@ public class GeneralFileMetaDataService implements FileMetaDataService { private final FileMetaDataRepository fileMetaDataRepository; - @Transactional @Override + @Transactional public void create(FileMetaData fileMetaData) { fileMetaDataRepository.save(fileMetaData); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/dto/FileMetaDataCommand.java b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/dto/CreateFileMetaDataCommand.java similarity index 65% rename from src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/dto/FileMetaDataCommand.java rename to src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/dto/CreateFileMetaDataCommand.java index 85dc31bb..bf0c66db 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/dto/FileMetaDataCommand.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/filemetadata/service/dto/CreateFileMetaDataCommand.java @@ -3,19 +3,16 @@ import ddingdong.ddingdongBE.domain.filemetadata.entity.FileCategory; import ddingdong.ddingdongBE.domain.filemetadata.entity.FileMetaData; import java.util.UUID; -import lombok.Builder; -@Builder -public record FileMetaDataCommand( - UUID fileId, - String fileName +public record CreateFileMetaDataCommand( + String key, + FileCategory fileCategory ) { - public FileMetaData toEntity(FileCategory fileCategory) { + public FileMetaData toEntity(UUID fileId) { return FileMetaData.builder() .fileCategory(fileCategory) .fileId(fileId) - .fileName(fileName) .build(); } } diff --git a/src/main/java/ddingdong/ddingdongBE/file/AwsS3FileStore.java b/src/main/java/ddingdong/ddingdongBE/file/AwsS3FileStore.java index aef152b0..7f86b231 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/AwsS3FileStore.java +++ b/src/main/java/ddingdong/ddingdongBE/file/AwsS3FileStore.java @@ -29,7 +29,7 @@ public class AwsS3FileStore implements FileStore { @Value("${cloud.aws.region.static}") private String region; - @Value("${spring.s3.bucket}") + @Value("${spring.s3.input-bucket}") private String bucketName; @Override diff --git a/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java b/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java index 48624f27..7ef70c50 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java +++ b/src/main/java/ddingdong/ddingdongBE/file/api/S3FileAPi.java @@ -1,5 +1,6 @@ package ddingdong.ddingdongBE.file.api; +import ddingdong.ddingdongBE.auth.PrincipalDetails; import ddingdong.ddingdongBE.common.exception.ErrorResponse; import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; import io.swagger.v3.oas.annotations.Operation; @@ -12,6 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -53,6 +55,7 @@ public interface S3FileAPi { @ResponseStatus(HttpStatus.OK) @SecurityRequirement(name = "AccessToken") @GetMapping("/upload-url") - UploadUrlResponse getUploadUrl(@RequestParam("fileName") String fileName); + UploadUrlResponse getPreSignedUrl(@AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestParam("fileName") String fileName); } diff --git a/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java b/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java index f46c2386..28a590b5 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java +++ b/src/main/java/ddingdong/ddingdongBE/file/controller/S3FileController.java @@ -1,8 +1,14 @@ package ddingdong.ddingdongBE.file.controller; +import ddingdong.ddingdongBE.auth.PrincipalDetails; +import ddingdong.ddingdongBE.domain.user.entity.User; import ddingdong.ddingdongBE.file.api.S3FileAPi; import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; import ddingdong.ddingdongBE.file.service.S3FileService; +import ddingdong.ddingdongBE.file.service.dto.command.GeneratePreSignedUrlRequestCommand; +import ddingdong.ddingdongBE.file.service.dto.query.GeneratePreSignedUrlRequestQuery; +import java.net.URL; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; @@ -13,7 +19,13 @@ public class S3FileController implements S3FileAPi { private final S3FileService s3FileService; @Override - public UploadUrlResponse getUploadUrl(String fileName) { - return s3FileService.generatePreSignedUrl(fileName); + public UploadUrlResponse getPreSignedUrl(PrincipalDetails principalDetails, String fileName) { + User user = principalDetails.getUser(); + LocalDateTime now = LocalDateTime.now(); + GeneratePreSignedUrlRequestQuery query = + s3FileService.generatePresignedUrlRequest( + new GeneratePreSignedUrlRequestCommand(now, user.getAuthId(), fileName)); + URL presingedUrl = s3FileService.getPresignedUrl(query.generatePresignedUrlRequest()); + return UploadUrlResponse.of(query, presingedUrl); } } diff --git a/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java b/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java index 26769782..a1895ed0 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/file/controller/dto/response/UploadUrlResponse.java @@ -1,6 +1,8 @@ package ddingdong.ddingdongBE.file.controller.dto.response; +import ddingdong.ddingdongBE.file.service.dto.query.GeneratePreSignedUrlRequestQuery; import io.swagger.v3.oas.annotations.media.Schema; +import java.net.URL; import lombok.Builder; @Schema( @@ -12,14 +14,18 @@ public record UploadUrlResponse( @Schema(description = "presignedUrl", example = "https://test-bucket.s3.amazonaws.com/test/jpg/image.jpg") String uploadUrl, - @Schema(description = "업로드 파일 식별자(UUID)", example = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d") - String fileId + @Schema(description = "업로드 key(경로)", example = "local/file/2024-01-01/cow/UUID") + String key, + @Schema(description = "contentType(presignedUrl 업로드 요청 시 사용)", example = "image/png") + String contentType + ) { - public static UploadUrlResponse of(String uploadUrl, String fileId) { + public static UploadUrlResponse of(GeneratePreSignedUrlRequestQuery query, URL uploadUrl) { return UploadUrlResponse.builder() - .uploadUrl(uploadUrl) - .fileId(fileId) + .uploadUrl(uploadUrl.toString()) + .key(query.key()) + .contentType(query.contentType()) .build(); } diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/ContentType.java b/src/main/java/ddingdong/ddingdongBE/file/service/ContentType.java new file mode 100644 index 00000000..c3cd117a --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/ContentType.java @@ -0,0 +1,56 @@ +package ddingdong.ddingdongBE.file.service; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; + +@Getter +public enum ContentType { + JPEG("image/jpeg", false, "jpg", "jpeg", "jpe", "jif", "jfif", "jfi"), + PNG("image/png", false, "png"), + GIF("image/gif", false, "gif"), + WEBP("image/webp", false, "webp"), + TIFF("image/tiff", false, "tiff", "tif"), + BMP("image/bmp", false, "bmp"), + SVG("image/svg+xml", false, "svg", "svgz"), + ICO("image/x-icon", false, "ico"), + HEIC("image/heic", false, "heic"), + HEIF("image/heif", false, "heif"), + RAW("image/x-raw", false, "raw", "arw", "cr2", "nrw", "k25"), + PSD("image/vnd.adobe.photoshop", false, "psd"), + PDF("application/pdf", false, "pdf"), + MSWORD("application/msword", false, "doc"), + DOCX("application/vnd.openxmlformats-officedocument.wordprocessingml.document", false, "docx"), + EXCEL("application/vnd.ms-excel", false, "xls"), + XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", false, "xlsx"), + TEXT("text/plain", false, "txt"), + HTML("text/html", false, "html"), + MP4("video/mp4", true, "mp4"), + WEBM("video/webm", true, "webm"), + MOV("video/quicktime", true, "mov"), + OCTET_STREAM("application/octet-stream", false); + + private final String mimeType; + private final boolean isVideo; + private final List extensions; + + ContentType(String mimeType, boolean isVideo, String... extensions) { + this.mimeType = mimeType; + this.isVideo = isVideo; + this.extensions = Arrays.asList(extensions); + } + + public static ContentType fromExtension(String extension) { + String lowerExtension = extension.toLowerCase(); + for (ContentType contentType : values()) { + if (contentType.extensions.contains(lowerExtension)) { + return contentType; + } + } + return OCTET_STREAM; + } + + public boolean isVideo() { + return this.isVideo; + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/FileService.java b/src/main/java/ddingdong/ddingdongBE/file/service/FileService.java index 96e2ea34..a8a5b3b1 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/service/FileService.java +++ b/src/main/java/ddingdong/ddingdongBE/file/service/FileService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +//TODO: 리팩토링 후 제거 @Service @RequiredArgsConstructor public class FileService { diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java b/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java index f54554ea..4781ea67 100644 --- a/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java +++ b/src/main/java/ddingdong/ddingdongBE/file/service/S3FileService.java @@ -8,41 +8,52 @@ import com.github.f4b6a3.uuid.UuidCreator; import ddingdong.ddingdongBE.common.exception.AwsException.AwsClient; import ddingdong.ddingdongBE.common.exception.AwsException.AwsService; -import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; -import java.net.URL; -import java.util.Date; -import java.util.UUID; +import ddingdong.ddingdongBE.file.service.dto.command.GeneratePreSignedUrlRequestCommand; +import ddingdong.ddingdongBE.file.service.dto.query.GeneratePreSignedUrlRequestQuery; +import ddingdong.ddingdongBE.file.service.dto.query.UploadedFileUrlQuery; +import ddingdong.ddingdongBE.file.service.dto.query.UploadedVideoUrlQuery; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.UUID; + @Service @Slf4j @RequiredArgsConstructor public class S3FileService { - @Value("${spring.s3.bucket}") - private String bucketName; + private static final String S3_URL_FORMAT = "https://%s.s3.%s.amazonaws.com/"; + private static final String FILE_CDN_URL = "https://ddn4vjj3ws13w.cloudfront.net"; + private static final String VIDEO_CDN_URL = "https://d2syrtcctrfiup.cloudfront.net"; + private static final long PRE_SIGNED_URL_EXPIRATION_TIME = 1000 * 60 * 5; // 5 minutes + @Value("${spring.s3.input-bucket}") + private String inputBucket; + @Value("${spring.s3.output-bucket}") + private String outputBucket; @Value("${spring.config.activate.on-profile}") private String serverProfile; private final AmazonS3Client amazonS3Client; - public UploadUrlResponse generatePreSignedUrl(String fileName) { - UUID uploadFileName = UuidCreator.getTimeOrderedEpoch(); - String s3FilePath = createFilePath(fileName, uploadFileName); + public GeneratePreSignedUrlRequestQuery generatePresignedUrlRequest(GeneratePreSignedUrlRequestCommand command) { + UUID fileId = UuidCreator.getTimeOrderedEpoch(); + ContentType contentType = ContentType.fromExtension(extractFileExtension(command.fileName())); + String key = generateKey(contentType, command, fileId); + Date expiration = getExpirationTime(); - Date expiration = setExpirationTime(); - try { - GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, - s3FilePath) - .withMethod(HttpMethod.PUT) - .withExpiration(expiration); + GeneratePresignedUrlRequest request = createPresignedUrlRequest(key, contentType, expiration); + return new GeneratePreSignedUrlRequestQuery(request, key, contentType.getMimeType()); + } - URL uploadUrl = amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest); - return UploadUrlResponse.of(uploadUrl.toString(), uploadFileName.toString()); + public URL getPresignedUrl(GeneratePresignedUrlRequest generatePresignedUrlRequest) { + try { + return amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest); } catch (AmazonServiceException e) { log.warn("AWS Service Error : {}", e.getMessage()); throw new AwsService(); @@ -50,36 +61,69 @@ public UploadUrlResponse generatePreSignedUrl(String fileName) { log.warn("AWS Client Error : {}", e.getMessage()); throw new AwsClient(); } + } + public UploadedFileUrlQuery getUploadedFileUrl(String key) { + String region = amazonS3Client.getRegionName(); + String originUrl = String.format(S3_URL_FORMAT, inputBucket, region) + key; + String cdnUrl = FILE_CDN_URL + key; + return new UploadedFileUrlQuery(originUrl, cdnUrl); } - public String getUploadedFileUrl(String fileName, String uploadFileName) { + public UploadedVideoUrlQuery getUploadedVideoUrl(String key) { + String fileId = extractFileId(key); String region = amazonS3Client.getRegionName(); - String fileExtension = extractFileExtension(fileName); - return String.format("https://%s.s3.%s.amazonaws.com/%s/%s/%s", - bucketName, - region, - serverProfile, - fileExtension, - uploadFileName); + String thumbnailOriginUrl = generateS3Url(outputBucket, region, "thumbnail", fileId, ".jpg"); + String thumbnailCdnUrl = generateCdnUrl("thumbnail", fileId, ".jpg"); + String videoOriginUrl = generateS3Url(outputBucket, region, "hls", fileId, "_720.m3u8"); + String videoCdnUrl = generateCdnUrl("hls", fileId, "_720.m3u8"); + + return new UploadedVideoUrlQuery(thumbnailOriginUrl, thumbnailCdnUrl, videoOriginUrl, videoCdnUrl); + } + + private GeneratePresignedUrlRequest createPresignedUrlRequest(String key, ContentType contentType, + Date expiration) { + return new GeneratePresignedUrlRequest(inputBucket, key) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration) + .withContentType(contentType.getMimeType()); } - private Date setExpirationTime() { + private Date getExpirationTime() { Date expiration = new Date(); - long expTimeMillis = expiration.getTime(); - expTimeMillis += 1000 * 60 * 5; - expiration.setTime(expTimeMillis); + expiration.setTime(expiration.getTime() + PRE_SIGNED_URL_EXPIRATION_TIME); return expiration; } - private String createFilePath(String fileName, UUID uploadFileName) { - String fileExtension = extractFileExtension(fileName); - return String.format("%s/%s/%s", serverProfile, fileExtension, uploadFileName.toString()); + private String generateKey(ContentType contentType, GeneratePreSignedUrlRequestCommand command, + UUID uploadFileName) { + return String.format("%s/%s/%s/%s/%s", + serverProfile, + contentType.isVideo() ? "video" : "file", + formatDate(command.generatedAt()), + command.authId(), + uploadFileName.toString()); + } + + private String formatDate(LocalDateTime dateTime) { + return String.format("%d-%d-%d", dateTime.getYear(), dateTime.getMonthValue(), dateTime.getDayOfMonth()); } private String extractFileExtension(String fileName) { return fileName.substring(fileName.lastIndexOf('.') + 1); } + private String extractFileId(String key) { + String[] splitKey = key.split("/"); + return splitKey[splitKey.length - 1]; + } + + private String generateS3Url(String bucket, String region, String prefix, String filename, String suffix) { + return String.format(S3_URL_FORMAT + "%s", bucket, region, prefix) + filename + suffix; + } + + private String generateCdnUrl(String prefix, String filename, String suffix) { + return S3FileService.VIDEO_CDN_URL + "/" + prefix + filename + suffix; + } } diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/dto/command/GeneratePreSignedUrlRequestCommand.java b/src/main/java/ddingdong/ddingdongBE/file/service/dto/command/GeneratePreSignedUrlRequestCommand.java new file mode 100644 index 00000000..9b69fd80 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/dto/command/GeneratePreSignedUrlRequestCommand.java @@ -0,0 +1,11 @@ +package ddingdong.ddingdongBE.file.service.dto.command; + +import java.time.LocalDateTime; + +public record GeneratePreSignedUrlRequestCommand( + LocalDateTime generatedAt, + String authId, + String fileName +) { + +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/GeneratePreSignedUrlRequestQuery.java b/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/GeneratePreSignedUrlRequestQuery.java new file mode 100644 index 00000000..149d9172 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/GeneratePreSignedUrlRequestQuery.java @@ -0,0 +1,11 @@ +package ddingdong.ddingdongBE.file.service.dto.query; + +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; + +public record GeneratePreSignedUrlRequestQuery( + GeneratePresignedUrlRequest generatePresignedUrlRequest, + String key, + String contentType +) { + +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/UploadedFileUrlQuery.java b/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/UploadedFileUrlQuery.java new file mode 100644 index 00000000..64b417f7 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/UploadedFileUrlQuery.java @@ -0,0 +1,8 @@ +package ddingdong.ddingdongBE.file.service.dto.query; + +public record UploadedFileUrlQuery( + String originUrl, + String cdnUrl +) { + +} diff --git a/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/UploadedVideoUrlQuery.java b/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/UploadedVideoUrlQuery.java new file mode 100644 index 00000000..9c8b84f4 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/file/service/dto/query/UploadedVideoUrlQuery.java @@ -0,0 +1,10 @@ +package ddingdong.ddingdongBE.file.service.dto.query; + +public record UploadedVideoUrlQuery( + String thumbnailOriginUrl, + String thumbnailCdnUrl, + String videoOriginUrl, + String videoCdnUrl +) { + +} diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 630f3095..c2d30ba1 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -20,7 +20,8 @@ spring: mode: never s3: - bucket: "test" + input-bucket: "test" + output-bucket: "test" access-key: "test" secret-key: "test" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e05dc71d..25dccaab 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,7 +13,8 @@ spring: ddl-auto: none s3: - bucket: ${BUCKET_NAME} + input-bucket: ${INPUT_BUCKET_NAME} + output-bucket: ${OUTPUT_BUCKET_NAME} access-key: ${AWS_ACCESS_KEY_ID} secret-key: ${AWS_SECRET_ACCESS_KEY} diff --git a/src/main/resources/db/migration/V13__alter_file_meta_data_table_column.sql b/src/main/resources/db/migration/V13__alter_file_meta_data_table_column.sql new file mode 100644 index 00000000..8ddd2c4a --- /dev/null +++ b/src/main/resources/db/migration/V13__alter_file_meta_data_table_column.sql @@ -0,0 +1,5 @@ +ALTER TABLE file_meta_data + DROP COLUMN file_name; + +ALTER TABLE file_meta_data + MODIFY file_category VARCHAR (255) NOT NULL; diff --git a/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java b/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java index b4e3bb47..a0ae4ae4 100644 --- a/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java +++ b/src/test/java/ddingdong/ddingdongBE/file/service/S3FileServiceTest.java @@ -1,73 +1,104 @@ package ddingdong.ddingdongBE.file.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.when; - -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; -import ddingdong.ddingdongBE.file.controller.dto.response.UploadUrlResponse; -import java.net.MalformedURLException; -import java.net.URL; + +import ddingdong.ddingdongBE.common.support.TestContainerSupport; +import ddingdong.ddingdongBE.file.service.dto.command.GeneratePreSignedUrlRequestCommand; +import ddingdong.ddingdongBE.file.service.dto.query.GeneratePreSignedUrlRequestQuery; +import java.time.LocalDateTime; import java.util.regex.Pattern; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.util.ReflectionTestUtils; - -@ExtendWith(MockitoExtension.class) -@ActiveProfiles("test") -class S3FileServiceTest { +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; - @Mock - private AmazonS3Client amazonS3Client; +@SpringBootTest +class S3FileServiceTest extends TestContainerSupport { - @InjectMocks + @Autowired private S3FileService s3FileService; - @DisplayName("presignedUrl을 생성한다.") + private static final Pattern UUID7_PATTERN = Pattern.compile( + "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-7[0-9A-Fa-f]{3}-[89ab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$" + ); + + @DisplayName("GeneratePreSignedUrlRequest(FILE)를 생성한다.") @Test - void generatePreSignedUrl() throws MalformedURLException { + void generateFILEPreSignedUrlRequest() { //given + LocalDateTime now = LocalDateTime.now(); + String authId = "test"; String fileName = "image.jpg"; - - URL expectedUrl = new URL("https://test-bucket.s3.amazonaws.com/test/jpg/image.jpg"); - given(amazonS3Client.generatePresignedUrl(any(GeneratePresignedUrlRequest.class))).willReturn(expectedUrl); + GeneratePreSignedUrlRequestCommand command = + new GeneratePreSignedUrlRequestCommand(now, authId, fileName); //when - UploadUrlResponse uploadUrlResponse = s3FileService.generatePreSignedUrl(fileName); + GeneratePreSignedUrlRequestQuery query = s3FileService.generatePresignedUrlRequest(command); //then - Pattern UUID7_PATTERN = Pattern.compile( - "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-7[0-9A-Fa-f]{3}-[89ab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$" - ); - assertThat(uploadUrlResponse.uploadUrl()).isEqualTo(expectedUrl.toString()); - assertThat(Pattern.matches(UUID7_PATTERN.pattern(), uploadUrlResponse.fileId())).isTrue(); - } - @DisplayName("s3 uploadedFileUrl을 조회한다.") - @Test - void getUploadedFileUrl() { - //given - String fileName = "image.jpg"; - String uploadFileName = "test"; + String[] split = query.key().split("/"); + String uploadName = split[split.length - 1]; + assertThat(query.generatePresignedUrlRequest()) + .satisfies(request -> { + assertThat(request.getContentType()) + .as("Content type should be image/jpeg") + .isEqualTo("image/jpeg"); - when(amazonS3Client.getRegionName()).thenReturn("ap-northeast-2"); + assertThat(request.getKey()) + .as("Key should contain correct date, authId, and fileId") + .contains(String.format("%s/%d-%d-%d/%s/", + "file", now.getYear(), now.getMonthValue(), now.getDayOfMonth(), authId)) + .contains(uploadName); + }); + assertThat(Pattern.matches(UUID7_PATTERN.pattern(), uploadName)).isTrue(); + assertThat(query.contentType()).isEqualTo("image/jpeg"); + } - ReflectionTestUtils.setField(s3FileService, "bucketName", "test"); - ReflectionTestUtils.setField(s3FileService, "serverProfile", "test"); + @DisplayName("GeneratePreSignedUrlRequest(VIDEO)를 생성한다.") + @ParameterizedTest + @ValueSource(strings = {"video.mp4", "video.webm", "video.mov"}) + void generateVIDEOPreSignedUrlRequest(String fileName) { + //given + LocalDateTime now = LocalDateTime.now(); + String authId = "test"; + GeneratePreSignedUrlRequestCommand command = + new GeneratePreSignedUrlRequestCommand(now, authId, fileName); //when - String uploadedFileUrl = s3FileService.getUploadedFileUrl(fileName, uploadFileName); + GeneratePreSignedUrlRequestQuery query = s3FileService.generatePresignedUrlRequest(command); //then - Assertions.assertThat(uploadedFileUrl).isEqualTo("https://test.s3.ap-northeast-2.amazonaws.com/test/jpg/test"); + String[] split = query.key().split("/"); + String uploadName = split[split.length - 1]; + + assertThat(query.generatePresignedUrlRequest()) + .satisfies(request -> { + assertThat(request.getContentType()) + .as("Content type should match the video's MIME type") + .isEqualTo(expectedContentType(fileName)); + assertThat(request.getKey()) + .as("Key should contain correct date, authId, and fileId") + .contains(String.format("%s/%d-%d-%d/%s/", + "video", now.getYear(), now.getMonthValue(), now.getDayOfMonth(), authId)) + .contains(uploadName); + + assertThat(Pattern.matches(UUID7_PATTERN.pattern(), uploadName)).isTrue(); + }); + } + private String expectedContentType(String fileName) { + if (fileName.endsWith(".mp4")) { + return "video/mp4"; + } else if (fileName.endsWith(".webm")) { + return "video/webm"; + } else if (fileName.endsWith(".mov")) { + return "video/quicktime"; + } else { + throw new IllegalArgumentException("Unsupported video format"); + } + } }