Skip to content

Commit

Permalink
[#32] 파일 업로드 API 추가 (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
kkjsw17 authored Feb 24, 2024
1 parent 2922d74 commit ce2da8b
Show file tree
Hide file tree
Showing 18 changed files with 225 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ out/
.DS_Store

### OTHERS ###
application-secret.yml
application-secret.yml
data
Original file line number Diff line number Diff line change
@@ -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")
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<File, Long> {
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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(".", "")
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<FileResponse> {
val fileEntity = fileUploadService.upload(multipartFile)

return ResponseEntity.status(CREATED)
.body(FileResponse.from(fileEntity))
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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.")
}
}
}
Original file line number Diff line number Diff line change
@@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Slide(
}

fun imageFilePath(): String? {
return imageFile?.absolutePath()
return imageFile?.absolutePath
}

fun unmap() {
Expand Down
3 changes: 3 additions & 0 deletions presentation-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ spring:
hibernate:
format_sql: true
use_sql_comments: true
file:
base-path: ./data
thumbnail-path: /thumbnails

0 comments on commit ce2da8b

Please sign in to comment.