From ce2da8b7aed5de201b45bf4fe569e849ba33cac9 Mon Sep 17 00:00:00 2001 From: Kijun Kwon <39583312+kkjsw17@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:51:12 +0900 Subject: [PATCH] =?UTF-8?q?[#32]=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20API=20=EC=B6=94=EA=B0=80=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- .../pregen/common/domain/Constant.kt | 13 +++++++ .../pregen/practice/domain/Constant.kt | 9 ----- .../practice/presentation/HandshakeFilter.kt | 2 +- .../PresentationApiApplication.kt | 2 + .../pregen/presentation/file/domain/File.kt | 21 +++++++++- .../file/infrastructure/FileJpaRepository.kt | 7 ++++ .../file/infrastructure/FileLocalWriter.kt | 25 ++++++++++++ .../file/infrastructure/FileProperties.kt | 14 +++++++ .../file/infrastructure/FileRepositoryImpl.kt | 15 ++++++++ .../file/presentation/FileController.kt | 25 ++++++++++++ .../file/presentation/FileResponse.kt | 15 ++++++++ .../file/service/FileRepository.kt | 8 ++++ .../file/service/FileUploadService.kt | 38 +++++++++++++++++++ .../file/service/FileValidator.kt | 27 +++++++++++++ .../presentation/file/service/FileWriter.kt | 10 +++++ .../pregen/presentation/slide/domain/Slide.kt | 2 +- .../src/main/resources/application.yml | 3 ++ 18 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 common/src/main/kotlin/org/kkeunkkeun/pregen/common/domain/Constant.kt delete mode 100644 practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/domain/Constant.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileJpaRepository.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileLocalWriter.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileProperties.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileRepositoryImpl.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileController.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileResponse.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileRepository.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileUploadService.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileValidator.kt create mode 100644 presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileWriter.kt diff --git a/.gitignore b/.gitignore index 6af995c..96ec051 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ out/ .DS_Store ### OTHERS ### -application-secret.yml \ No newline at end of file +application-secret.yml +data diff --git a/common/src/main/kotlin/org/kkeunkkeun/pregen/common/domain/Constant.kt b/common/src/main/kotlin/org/kkeunkkeun/pregen/common/domain/Constant.kt new file mode 100644 index 0000000..ebfdcd0 --- /dev/null +++ b/common/src/main/kotlin/org/kkeunkkeun/pregen/common/domain/Constant.kt @@ -0,0 +1,13 @@ +package org.kkeunkkeun.pregen.common.domain + +class Constant { + + companion object { + + const val SESSION_ID_HEADER_NAME = "X-SESSION-ID" + + const val MAX_IMAGE_FILE_SIZE = 10 * 1024 * 1024 + + val ALLOWED_IMAGE_FILE_EXTENSIONS = setOf("image/jpeg", "image/png") + } +} \ No newline at end of file diff --git a/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/domain/Constant.kt b/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/domain/Constant.kt deleted file mode 100644 index 297a313..0000000 --- a/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/domain/Constant.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.kkeunkkeun.pregen.practice.domain - -class Constant { - - companion object { - - const val SESSION_ID_HEADER_NAME = "X-SESSION-ID" - } -} \ No newline at end of file diff --git a/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/presentation/HandshakeFilter.kt b/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/presentation/HandshakeFilter.kt index 098a994..6246ae0 100644 --- a/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/presentation/HandshakeFilter.kt +++ b/practice-websocket/src/main/kotlin/org/kkeunkkeun/pregen/practice/presentation/HandshakeFilter.kt @@ -12,7 +12,7 @@ import org.kkeunkkeun.pregen.account.service.AccountRepository import org.kkeunkkeun.pregen.common.presentation.ErrorResponse import org.kkeunkkeun.pregen.common.presentation.ErrorStatus import org.kkeunkkeun.pregen.common.presentation.PregenException -import org.kkeunkkeun.pregen.practice.domain.Constant.Companion.SESSION_ID_HEADER_NAME +import org.kkeunkkeun.pregen.common.domain.Constant.Companion.SESSION_ID_HEADER_NAME import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/PresentationApiApplication.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/PresentationApiApplication.kt index 34bbe37..d50da74 100644 --- a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/PresentationApiApplication.kt +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/PresentationApiApplication.kt @@ -1,10 +1,12 @@ package org.kkeunkkeun.pregen.presentation import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication import org.springframework.context.annotation.ComponentScan @SpringBootApplication +@ConfigurationPropertiesScan @ComponentScan(basePackages = [ "org.kkeunkkeun.pregen.account", "org.kkeunkkeun.pregen.common", diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/domain/File.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/domain/File.kt index 5c96ce1..d43a428 100644 --- a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/domain/File.kt +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/domain/File.kt @@ -2,6 +2,8 @@ package org.kkeunkkeun.pregen.presentation.file.domain import jakarta.persistence.* import jakarta.persistence.EnumType.STRING +import org.kkeunkkeun.pregen.presentation.file.domain.FileType.IMAGE +import org.springframework.web.multipart.MultipartFile @Entity class File( @@ -22,7 +24,22 @@ class File( val generatedName: String ) { - fun absolutePath(): String { - return String.format("%s/%s", path, generatedName) + + val absolutePath: String + get() = "$path/$generatedName" + + companion object { + + fun from(file: MultipartFile, path: String, generatedName: String): File { + val originalName = file.originalFilename ?: throw IllegalStateException("original name not provided.") + + return File( + null, + fileType = IMAGE, + path = path, + originalName = originalName, + generatedName = generatedName, + ) + } } } \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileJpaRepository.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileJpaRepository.kt new file mode 100644 index 0000000..731a027 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileJpaRepository.kt @@ -0,0 +1,7 @@ +package org.kkeunkkeun.pregen.presentation.file.infrastructure + +import org.kkeunkkeun.pregen.presentation.file.domain.File +import org.springframework.data.jpa.repository.JpaRepository + +interface FileJpaRepository: JpaRepository { +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileLocalWriter.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileLocalWriter.kt new file mode 100644 index 0000000..8230549 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileLocalWriter.kt @@ -0,0 +1,25 @@ +package org.kkeunkkeun.pregen.presentation.file.infrastructure + +import org.kkeunkkeun.pregen.presentation.file.domain.File +import org.kkeunkkeun.pregen.presentation.file.service.FileWriter +import org.springframework.stereotype.Repository +import org.springframework.web.multipart.MultipartFile +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption + +@Repository +class FileLocalWriter: FileWriter { + + override fun writeMultipartFile(file: MultipartFile, path: String, physicalName: String): File { + val targetLocation = Paths.get(path) + + // Resolve the file path to prevent overwriting issues and keep original file name + val targetPath = targetLocation.resolve(physicalName) + + // Copy the file to the target location (replacing existing file with the same name) + Files.copy(file.inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING) + + return File.from(file, path, physicalName) + } +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileProperties.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileProperties.kt new file mode 100644 index 0000000..a90bc1a --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileProperties.kt @@ -0,0 +1,14 @@ +package org.kkeunkkeun.pregen.presentation.file.infrastructure + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "file") +data class FileProperties( + val basePath: String = "", + val thumbnailPath: String = "" +) { + val fullThumbnailPath: String + get() = "$basePath/$thumbnailPath" + .replace("//", "/") + .replace(".", "") +} diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileRepositoryImpl.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileRepositoryImpl.kt new file mode 100644 index 0000000..07242c3 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/infrastructure/FileRepositoryImpl.kt @@ -0,0 +1,15 @@ +package org.kkeunkkeun.pregen.presentation.file.infrastructure + +import org.kkeunkkeun.pregen.presentation.file.domain.File +import org.kkeunkkeun.pregen.presentation.file.service.FileRepository +import org.springframework.stereotype.Repository + +@Repository +class FileRepositoryImpl( + private val fileJpaRepository: FileJpaRepository +): FileRepository { + + override fun save(file: File) { + fileJpaRepository.save(file) + } +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileController.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileController.kt new file mode 100644 index 0000000..92063d3 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileController.kt @@ -0,0 +1,25 @@ +package org.kkeunkkeun.pregen.presentation.file.presentation + +import org.kkeunkkeun.pregen.presentation.file.service.FileUploadService +import org.springframework.http.HttpStatus.CREATED +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/api/files") +class FileController( + private val fileUploadService: FileUploadService, +) { + + @PostMapping("/upload") + fun uploadFile(@RequestPart(name = "file") multipartFile: MultipartFile): ResponseEntity { + val fileEntity = fileUploadService.upload(multipartFile) + + return ResponseEntity.status(CREATED) + .body(FileResponse.from(fileEntity)) + } +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileResponse.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileResponse.kt new file mode 100644 index 0000000..90a8f8d --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/presentation/FileResponse.kt @@ -0,0 +1,15 @@ +package org.kkeunkkeun.pregen.presentation.file.presentation + +import org.kkeunkkeun.pregen.presentation.file.domain.File + +data class FileResponse( + val id: Long, + val path: String, +) { + + companion object { + fun from(file: File): FileResponse { + return FileResponse(file.id!!, file.absolutePath) + } + } +} diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileRepository.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileRepository.kt new file mode 100644 index 0000000..ec25c24 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileRepository.kt @@ -0,0 +1,8 @@ +package org.kkeunkkeun.pregen.presentation.file.service + +import org.kkeunkkeun.pregen.presentation.file.domain.File + +interface FileRepository { + + fun save(file: File) +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileUploadService.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileUploadService.kt new file mode 100644 index 0000000..67cff96 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileUploadService.kt @@ -0,0 +1,38 @@ +package org.kkeunkkeun.pregen.presentation.file.service + +import org.kkeunkkeun.pregen.presentation.file.domain.File +import org.kkeunkkeun.pregen.presentation.file.infrastructure.FileProperties +import org.springframework.stereotype.Service +import org.springframework.util.StringUtils +import org.springframework.web.multipart.MultipartFile +import java.util.UUID + +@Service +class FileUploadService( + private val validator: FileValidator, + private val fileRepository: FileRepository, + private val fileWriter: FileWriter, + private val fileProperties: FileProperties, +) { + + fun upload(file: MultipartFile): File { + validator.validate(file) + + val fileEntity = fileWriter.writeMultipartFile( + file = file, + path = fileProperties.fullThumbnailPath, + physicalName = generateFilePhysicalName(file) + ) + + fileRepository.save(fileEntity) + + return fileEntity + } + + private fun generateFilePhysicalName(file: MultipartFile): String { + val extension = StringUtils.getFilenameExtension(file.originalFilename) + val generatedFileName = "${UUID.randomUUID()}.$extension" + + return generatedFileName + } +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileValidator.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileValidator.kt new file mode 100644 index 0000000..e5debb7 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileValidator.kt @@ -0,0 +1,27 @@ +package org.kkeunkkeun.pregen.presentation.file.service + +import org.kkeunkkeun.pregen.common.domain.Constant.Companion.ALLOWED_IMAGE_FILE_EXTENSIONS +import org.kkeunkkeun.pregen.common.domain.Constant.Companion.MAX_IMAGE_FILE_SIZE +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +@Service +class FileValidator { + + fun validate(file: MultipartFile) { + if (file.isEmpty) { + throw IllegalStateException("The file is empty. Please select a non-empty file.") + } + + // Check the file size (10MB = 10 * 1024 * 1024 bytes) + val maxFileSize = MAX_IMAGE_FILE_SIZE + if (file.size > maxFileSize) { + throw IllegalStateException("The file exceeds the maximum allowed size of 10MB.") + } + + // Check the file type + if (!ALLOWED_IMAGE_FILE_EXTENSIONS.contains(file.contentType)) { + throw IllegalStateException("Invalid file type. Only JPG and PNG files are allowed.") + } + } +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileWriter.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileWriter.kt new file mode 100644 index 0000000..4bc40b5 --- /dev/null +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/file/service/FileWriter.kt @@ -0,0 +1,10 @@ +package org.kkeunkkeun.pregen.presentation.file.service + +import org.kkeunkkeun.pregen.presentation.file.domain.File +import org.springframework.web.multipart.MultipartFile + +interface FileWriter { + + fun writeMultipartFile(file: MultipartFile, path: String, physicalName: String): File + +} \ No newline at end of file diff --git a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/slide/domain/Slide.kt b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/slide/domain/Slide.kt index 7f9e8d1..488e7f5 100644 --- a/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/slide/domain/Slide.kt +++ b/presentation-api/src/main/kotlin/org/kkeunkkeun/pregen/presentation/slide/domain/Slide.kt @@ -36,7 +36,7 @@ class Slide( } fun imageFilePath(): String? { - return imageFile?.absolutePath() + return imageFile?.absolutePath } fun unmap() { diff --git a/presentation-api/src/main/resources/application.yml b/presentation-api/src/main/resources/application.yml index 52bac11..93c6cb1 100644 --- a/presentation-api/src/main/resources/application.yml +++ b/presentation-api/src/main/resources/application.yml @@ -35,3 +35,6 @@ spring: hibernate: format_sql: true use_sql_comments: true +file: + base-path: ./data + thumbnail-path: /thumbnails