diff --git a/.gitignore b/.gitignore index 6b82cb2e1e32070e9a6fec867854c8810ed301a6..54d579f0222f518760af9ef7b031e5c58ecb753b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +/application-aws.properites **/*.properties diff --git a/build.gradle b/build.gradle index df58bc560c9437f75bef5437315933d3395914ea..bac4b34c3222d2e9e57c5108b56ecb52f9fa2736 100644 --- a/build.gradle +++ b/build.gradle @@ -31,17 +31,16 @@ dependencies { //H2 DB 추가 runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' -// mySQL + // mySQL runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' - - + // AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { diff --git a/src/main/java/umc/spring/file/config/AwsConfig.java b/src/main/java/umc/spring/file/config/AwsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..3a9329850a229048c8bc17054bfaedf0e132550c --- /dev/null +++ b/src/main/java/umc/spring/file/config/AwsConfig.java @@ -0,0 +1,32 @@ +package umc.spring.file.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsConfig { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/file/controller/FileController.java b/src/main/java/umc/spring/file/controller/FileController.java new file mode 100644 index 0000000000000000000000000000000000000000..677c28481dfd829084b2ea51b2d4bd68ea116edc --- /dev/null +++ b/src/main/java/umc/spring/file/controller/FileController.java @@ -0,0 +1,66 @@ +package umc.spring.file.controller; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import umc.spring.file.service.AmazonS3Service; +import umc.spring.file.service.S3Service; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/file") +public class FileController { + + private final AmazonS3Service amazon3SService; + + @PostMapping("/uploads") + public ResponseEntity<Object> uploadFiles( + @RequestParam(value = "uploadFilePath") String uploadFilePath, + @RequestPart(value = "files") List<MultipartFile> multipartFiles) { + return ResponseEntity + .status(HttpStatus.OK) + .body(amazon3SService.uploadFiles(uploadFilePath, multipartFiles)); + } + + @DeleteMapping("/delete") + public ResponseEntity<Object> deleteFile( + @RequestParam(value = "uploadFilePath") String uploadFilePath, + @RequestParam(value = "uuidFileName") String uuidFileName) { + return ResponseEntity + .status(HttpStatus.OK) + .body(amazon3SService.deleteFile(uploadFilePath, uuidFileName)); + } + + @GetMapping("/get") + public void getFile(HttpServletResponse response, + @RequestParam String uploadFilePath, + @RequestParam String uuidFileName) { + try { + InputStream inputStream = amazon3SService.getFile(uploadFilePath, uuidFileName); + if (inputStream != null) { + byte[] buffer = new byte[4048]; + int bytesRead; + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + + try (OutputStream outStream = response.getOutputStream()) { + while ((bytesRead = inputStream.read(buffer)) != -1) { + outStream.write(buffer, 0, bytesRead); + } + } + } else { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/umc/spring/file/domain/S3FileDto.java b/src/main/java/umc/spring/file/domain/S3FileDto.java new file mode 100644 index 0000000000000000000000000000000000000000..24d16f4f3eec360e28a3753b2d952853e77c2d7d --- /dev/null +++ b/src/main/java/umc/spring/file/domain/S3FileDto.java @@ -0,0 +1,18 @@ +package umc.spring.file.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Builder +public class S3FileDto { + + private String originalFileName; + private String uploadFileName; + private String uploadFilePath; + private String uploadFileUrl; +} \ No newline at end of file diff --git a/src/main/java/umc/spring/file/service/AmazonS3Service.java b/src/main/java/umc/spring/file/service/AmazonS3Service.java new file mode 100644 index 0000000000000000000000000000000000000000..0118d97888faeb256bb46ed5b35d25dd5b263350 --- /dev/null +++ b/src/main/java/umc/spring/file/service/AmazonS3Service.java @@ -0,0 +1,135 @@ +package umc.spring.file.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.S3Object; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import umc.spring.file.domain.S3FileDto; + +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.*; + +@Slf4j +@RequiredArgsConstructor +@Service +public class AmazonS3Service { + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + private final AmazonS3 amazonS3; + + /** + * S3로 파일 업로드 + */ + public List<S3FileDto> uploadFiles(String uploadFilePath, List<MultipartFile> multipartFiles) { + + List<S3FileDto> s3files = new ArrayList<>(); + +// String uploadFilePath = fileType + "/" + getFolderName(); + + for (MultipartFile multipartFile : multipartFiles) { + + String originalFileName = multipartFile.getOriginalFilename(); + String uploadFileName = getUuidFileName(originalFileName); + String uploadFileUrl = ""; + + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + + try (InputStream inputStream = multipartFile.getInputStream()) { + + String keyName = uploadFilePath + "/" + uploadFileName; // ex) 구분/파일.확장자 + + // S3에 폴더 및 파일 업로드 + // 외부에 공개하는 파일인 경우 Public Read 권한을 추가, ACL 확인 + amazonS3.putObject( + new PutObjectRequest(bucketName, keyName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + + + // S3에 업로드한 폴더 및 파일 URL + uploadFileUrl = amazonS3.getUrl(bucketName, keyName).toString(); + + } catch (IOException e) { + e.printStackTrace(); + log.error("Filed upload failed", e); + } + + s3files.add( + S3FileDto.builder() + .originalFileName(originalFileName) + .uploadFileName(uploadFileName) + .uploadFilePath(uploadFilePath) + .uploadFileUrl(uploadFileUrl) + .build()); + } + + return s3files; + } + + /** + * S3에 업로드된 파일 삭제 + */ + public String deleteFile(String uploadFilePath, String uuidFileName) { + + String result = "success"; + + try { + String keyName = uploadFilePath + "/" + uuidFileName; // ex) 구분/파일.확장자 + boolean isObjectExist = amazonS3.doesObjectExist(bucketName, keyName); + if (isObjectExist) { + amazonS3.deleteObject(bucketName, keyName); + } else { + result = "file not found"; + } + } catch (Exception e) { + log.debug("Delete File failed", e); + } + + return result; + } + + /** + * S3에서 파일 읽어오기 + */ + public InputStream getFile(String uploadFilePath, String uuidFileName) { + String keyName = uploadFilePath + "/" + uuidFileName; // ex) 구분/파일.확장자 + boolean isObjectExist = amazonS3.doesObjectExist(bucketName, keyName); + + if (isObjectExist) { + S3Object s3object = amazonS3.getObject(bucketName, keyName); + return s3object.getObjectContent(); + } else { + log.error("File not found"); + return null; + } + } + + /** + * UUID 파일명 반환 + */ + public String getUuidFileName(String fileName) { + String ext = fileName.substring(fileName.indexOf(".") + 1); + return UUID.randomUUID().toString() + "." + ext; + } + + /** + * 년/월/일 폴더명 반환 + */ + private String getFolderName() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + Date date = new Date(); + String str = sdf.format(date); + return str.replace("-", "/"); + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/file/service/S3Service.java b/src/main/java/umc/spring/file/service/S3Service.java new file mode 100644 index 0000000000000000000000000000000000000000..cce2f50900abfb34664e28e705dcc7040e117fb3 --- /dev/null +++ b/src/main/java/umc/spring/file/service/S3Service.java @@ -0,0 +1,76 @@ +package umc.spring.file.service; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Date; + +/* S3Service.java */ +@Slf4j +@RequiredArgsConstructor +@Service +public class S3Service { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + + /* 1. 파일 업로드 */ + public String upload(MultipartFile multipartFile, String s3FileName) throws IOException { + // 메타데이터 생성 + ObjectMetadata objMeta = new ObjectMetadata(); + objMeta.setContentLength(multipartFile.getInputStream().available()); + // putObject(버킷명, 파일명, 파일데이터, 메타데이터)로 S3에 객체 등록 + amazonS3.putObject(new PutObjectRequest(bucket, s3FileName, multipartFile.getInputStream(), objMeta) + .withCannedAcl(CannedAccessControlList.PublicRead)); + // 등록된 객체의 url 반환 (decoder: url 안의 한글 or 특수문자 깨짐 방지) + return URLDecoder.decode(amazonS3.getUrl(bucket, s3FileName).toString(), "utf-8"); + } + + /* 2. 파일 삭제 */ + public void delete (String keyName) { + try { + // deleteObject(버킷명, 키값)으로 객체 삭제 + amazonS3.deleteObject(bucket, keyName); + } catch (AmazonServiceException e) { + log.error(e.toString()); + } + } + + /* 3. 파일의 presigned URL 반환 */ + public String getPresignedURL (String keyName) { + String preSignedURL = ""; + // presigned URL이 유효하게 동작할 만료기한 설정 (2분) + Date expiration = new Date(); + Long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 2; + expiration.setTime(expTimeMillis); + + try { + // presigned URL 발급 + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, keyName) + .withMethod(HttpMethod.GET) + .withExpiration(expiration); + URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest); + preSignedURL = url.toString(); + } catch (Exception e) { + log.error(e.toString()); + } + + return preSignedURL; + } +} \ No newline at end of file diff --git a/src/main/resources/application-aws.properties b/src/main/resources/application-aws.properties new file mode 100644 index 0000000000000000000000000000000000000000..d9eb924fd0d117367c27d5d29cae9d18e8c6769e --- /dev/null +++ b/src/main/resources/application-aws.properties @@ -0,0 +1,5 @@ +cloud.aws.s3.bucket=anak-s3 +cloud.aws.stack.auto=false +cloud.aws.region.static=ap-northeast-2 +cloud.aws.credentials.accessKey=AKIAUK4OA4SHAQ4Y76RL +cloud.aws.credentials.secretKey=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 \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..2a40ba3846f941e37e08a5e9b96d0cf1b55b67fc --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,28 @@ +#spring.config.activate.on-profile=aws +spring.profiles.active=aws + +# mySql +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/study?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=UTC +spring.datasource.username=root +spring.datasource.password=1234 +spring.jpa.properties.hibernate.jdbc.batch_size=100 + +# jpa +spring.jpa.database=mysql +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.open-in-view=false + +# MULTIPART (MultipartProperties) +# Enable multipart uploads +spring.servlet.multipart.enabled=true +# Threshold after which files are written to disk. +spring.servlet.multipart.file-size-threshold=2KB +# Max file size. +spring.servlet.multipart.max-file-size=200MB +# Max Request Size +spring.servlet.multipart.max-request-size=215MB + +jwt.secret=secret