Skip to content

Commit

Permalink
Merge pull request #405 from Namo-log/feature/404
Browse files Browse the repository at this point in the history
[Feature/404] S3 멀티파트 업로드를 위한 Presigned URL 생성 및 완료 요청 API 구현
  • Loading branch information
hosunglee222 authored Nov 4, 2024
2 parents cec4480 + fc7bd19 commit bff2fa8
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

import static com.namo.spring.core.common.code.status.ErrorStatus.*;

import java.net.URL;
import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.namo.spring.application.external.global.annotation.swagger.ApiErrorCodes;
import com.namo.spring.core.common.response.ResponseDto;
import com.namo.spring.core.infra.common.aws.dto.MultipartCompleteRequest;
import com.namo.spring.core.infra.common.aws.dto.MultipartStartResponse;
import com.namo.spring.core.infra.common.aws.s3.S3Uploader;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -33,10 +40,45 @@ public class S3Controller {
INTERNET_SERVER_ERROR})
@GetMapping("/generate-presigned-url")
public ResponseDto<String> generatePresignedUrl(
@Parameter(description = "이미지 종류입니다 {activity: 활동 이미지, diary: 일기 이미지, cover: 커버 이미지, profile: 프로필 이미지} 입력 가능합니다.", example = "activity")
@Parameter(description = "업로드할 이미지의 종류를 선택해 주세요. 예: 'activity' (활동 이미지), 'diary' (일기 이미지), 'cover' (커버 이미지), 'profile' (프로필 이미지)",
example = "activity")
@RequestParam String prefix,
@Parameter(description = "업로드할 파일의 이름을 입력해 주세요.", example = "example.jpg")
@RequestParam String fileName) {
String preSignedUrl = s3Service.getPreSignedUrl(prefix, fileName);
return ResponseDto.onSuccess(preSignedUrl);
}

@PostMapping("/pre-signed/start")
@Operation(
summary = "S3 Presigned URLs 생성 요청 (Part 업로드용)",
description = "이 API는 클라이언트가 큰 파일을 여러 파트로 나누어 S3에 업로드할 수 있도록 각 파트에 대한 Presigned URL을 생성합니다. \n"
+ "파일을 원하는 만큼의 파트로 나누기 위해 partCount에 파트 수를 입력해 주세요. 반환된 각 URL을 사용해 각 파트를 업로드한 후, "
+ "업로드가 완료되면 반드시 '/pre-signed/complete' API를 호출하여 최종 업로드를 완료해야 합니다. 그렇지 않으면 S3에 임시로 저장된 파일이 일정 기간(7일) 후 삭제될 수 있습니다."
)
public ResponseDto<MultipartStartResponse> start(
@Parameter(description = "업로드할 파일의 이름을 입력해 주세요.", example = "example.jpg")
@RequestParam String fileName,
@Parameter(description = "업로드할 이미지의 종류를 선택해 주세요. 예: 'activity' (활동 이미지), 'diary' (일기 이미지), 'cover' (커버 이미지), 'profile' (프로필 이미지)",
example = "activity")
@RequestParam String prefix,
@Parameter(description = "파일을 몇 개의 파트로 나누어 업로드할지 입력해 주세요.", example = "3")
@RequestParam int partCount
) {
return ResponseDto.onSuccess(s3Service.getPreSignedUrls(prefix, fileName, partCount));
}

@PostMapping("/pre-signed/complete")
@Operation(
summary = "S3 멀티파트 업로드 완료 요청",
description = "이 API는 S3에 멀티파트 업로드를 완료하는 요청을 보냅니다. 클라이언트는 모든 파트를 Presigned URLs을 통해 업로드한 후 이 API를 호출하여 업로드를 최종 완료해야 합니다. \n"
+ "업로드가 완료되지 않으면 임시 저장된 파일이 일정 기간(7일) 후 삭제될 수 있습니다. "
+ "partETags example 입니다 : \"[{\\\"partNumber\\\": 1, \\\"eTag\\\": \\\"etag_value_1\\\"}, {\\\"partNumber\\\": 2, \\\"eTag\\\": \\\"etag_value_2\\\"}]\")"
)
public ResponseDto<String> complete(
@Parameter(description = "업로드 완료를 위한 요청 정보를 JSON 형식으로 전달합니다. 이 정보에는 파일 이름, 업로드 ID, 각 파트의 ETag 정보가 포함되어야 합니다.")
@RequestBody MultipartCompleteRequest request
) {
return ResponseDto.onSuccess(s3Service.completeMultipartUpload(request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.namo.spring.core.infra.common.aws.dto;

import java.util.List;

import com.amazonaws.services.s3.model.PartETag;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class MultipartCompleteRequest {
private String fileName;
private String uploadId;
private List<PartETag> partETags;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.namo.spring.core.infra.common.aws.dto;

import java.net.URL;
import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class MultipartStartResponse {
private String uploadId;
private List<URL> presignedUrls;
private String fileName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.stereotype.Component;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.Headers;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadResult;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PartETag;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.namo.spring.core.infra.common.aws.dto.MultipartCompleteRequest;
import com.namo.spring.core.infra.common.aws.dto.MultipartStartResponse;
import com.namo.spring.core.infra.common.constant.FilePath;
import com.namo.spring.core.infra.config.AwsS3Config;

Expand All @@ -28,6 +37,7 @@ public class S3Uploader {
private final AmazonS3Client amazonS3Client;
private final AwsS3Config awsS3Config;

// v1
public void uploadFile(InputStream inputStream, ObjectMetadata objectMeTadata, String fileName) {
amazonS3Client.putObject(
new PutObjectRequest(awsS3Config.getBucketName(), fileName, inputStream, objectMeTadata)
Expand All @@ -46,6 +56,55 @@ public void delete(String key) {
amazonS3Client.deleteObject(deleteObjectRequest);
}

/**
* Part 업로드를 위한 PresignedURLs 발급
* @param prefix
* @param fileName
* @param partCount
* @return
*/
public MultipartStartResponse getPreSignedUrls(String prefix, String fileName, int partCount) {
String uniqueFileName = createPath(prefix, fileName);
String uploadId = initiateMultipartUpload(uniqueFileName);
List<URL> presignedUrls = generatePartPresignedUrls(uniqueFileName, uploadId, partCount);
return new MultipartStartResponse(uploadId, presignedUrls, uniqueFileName);
}

/**
* S3에 Multipart Upload를 초기화하고 uploadId를 반환
*/
private String initiateMultipartUpload(String fileName) {
InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(awsS3Config.getBucketName(), fileName)
.withCannedACL(CannedAccessControlList.PublicRead);
InitiateMultipartUploadResult initResult = amazonS3Client.initiateMultipartUpload(initRequest);

return initResult.getUploadId();
}

/**
* 각 파트별 업로드를 위한 presigned URL 목록 생성
*/
private List<URL> generatePartPresignedUrls(String fileName, String uploadId, int partCount) {
return IntStream.rangeClosed(1, partCount)
.mapToObj(partNumber -> generatePresignedUrl(fileName, uploadId, partNumber))
.collect(Collectors.toList());
}

/**
* 단일 파트에 대한 presigned URL 생성
*/
private URL generatePresignedUrl(String fileName, String uploadId, int partNumber) {
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(
awsS3Config.getBucketName(), fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPreSignedUrlExpiration());

request.addRequestParameter("partNumber", String.valueOf(partNumber));
request.addRequestParameter("uploadId", uploadId);

return amazonS3Client.generatePresignedUrl(request);
}

/**
* presigned url 발급
*
Expand Down Expand Up @@ -115,6 +174,21 @@ private String createPath(String prefix, String fileName) {

return String.format("%s/%s", filepath, fileId + fileName);
}

/**
* part 업로드 완료 처리 (합침, 태깅으로 삭제 방지)
* @return
*/
public String completeMultipartUpload(MultipartCompleteRequest request) {
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
awsS3Config.getBucketName(),
request.getFileName(),
request.getUploadId(),
request.getPartETags()
);

return amazonS3Client.completeMultipartUpload(completeRequest).getLocation();
}
}


0 comments on commit bff2fa8

Please sign in to comment.