Skip to content

Commit

Permalink
Merge pull request #52 from Team-BomBomBom/feat/generate_presigned_ur…
Browse files Browse the repository at this point in the history
…l#BBB-125

Feat: #BBB-125 AWS S3 multipart 방식 μ˜μƒ μ—…λ‘œλ“œ API κ΅¬ν˜„
  • Loading branch information
platinouss authored Nov 14, 2024
2 parents a630abd + a7e37a5 commit 7368e5b
Show file tree
Hide file tree
Showing 27 changed files with 480 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ jobs:
- name: Test And Build with Gradle Wrapper
run: |
ACCESS_TOKEN_EXPIRE=300000000 JWT_SECRET_KEY=abcadsadsaqwdwqdfasdasd3r3214t4tk4ninifnewfokncknwfnopefw MYSQL_DATABASE=bombombom MYSQL_HOST=localhost MYSQL_PASSWORD=root MYSQL_USERNAME=root REFRESH_TOKEN_EXPIRE=7120000 TEST_MYSQL_DATABASE=test PORT=8080 LOG_LEVEL=DEBUG NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }} NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }} ELASTICSEARCH_URI=localhost:9200 TEST_ELASTICSEARCH_URI=localhost:9200 FRONT_SERVER_ORIGIN=http://localhost:3000 REDIS_HOST=localhost REDIS_PORT=6379 TEST_REDIS_HOST=localhost TEST_REDIS_PORT=6379 REDIS_SSL_ENABLED=false ./gradlew build
ACCESS_TOKEN_EXPIRE=300000000 JWT_SECRET_KEY=abcadsadsaqwdwqdfasdasd3r3214t4tk4ninifnewfokncknwfnopefw MYSQL_DATABASE=bombombom MYSQL_HOST=localhost MYSQL_PASSWORD=root MYSQL_USERNAME=root REFRESH_TOKEN_EXPIRE=7120000 TEST_MYSQL_DATABASE=test PORT=8080 LOG_LEVEL=DEBUG NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }} NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }} ELASTICSEARCH_URI=localhost:9200 TEST_ELASTICSEARCH_URI=localhost:9200 FRONT_SERVER_ORIGIN=http://localhost:3000 REDIS_HOST=localhost REDIS_PORT=6379 TEST_REDIS_HOST=localhost TEST_REDIS_PORT=6379 REDIS_SSL_ENABLED=false AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} AWS_S3_BUCKET=${{ secrets.AWS_S3_BUCKET }} ./gradlew build
1 change: 1 addition & 0 deletions app/external-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dependencies {
implementation project(':support:naver-client')
implementation project(':support:solvedac-client')
implementation project(':core')
implementation project(':infra:s3')

//impl
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.bombombom.devs.external.global.web.LoginUser;
import com.bombombom.devs.external.study.controller.dto.request.AddAssignmentRequest;
import com.bombombom.devs.external.study.controller.dto.request.CheckAlgorithmProblemSolvedRequest;
import com.bombombom.devs.external.study.controller.dto.request.CheckVideoUploadStatusRequest;
import com.bombombom.devs.external.study.controller.dto.request.ConfigureStudyRequest;
import com.bombombom.devs.external.study.controller.dto.request.DeleteAssignmentRequest;
import com.bombombom.devs.external.study.controller.dto.request.EditAssignmentRequest;
Expand Down Expand Up @@ -270,4 +271,11 @@ public ResponseEntity<List<StudyResponse>> ownedStudies(
).toList());
}

@PostMapping("/{id}/upload-status")
public ResponseEntity<Void> checkVideoUploadStatus(@PathVariable("id") Long studyId,
@RequestBody CheckVideoUploadStatusRequest request, @LoginUser AppUserDetails userDetails) {
bookStudyService.verifyAssignmentVideoComplete(userDetails.getId(), studyId,
request.assignmentId());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.bombombom.devs.external.study.controller.dto.request;

public record CheckVideoUploadStatusRequest(
Long assignmentId
) {

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bombombom.devs.external.study.service;

import com.bombombom.devs.S3MultipartUploadClient;
import com.bombombom.devs.book.model.Book;
import com.bombombom.devs.book.repository.BookRepository;
import com.bombombom.devs.core.exception.BusinessRuleException;
Expand All @@ -9,6 +10,7 @@
import com.bombombom.devs.core.exception.NotFoundException;
import com.bombombom.devs.core.util.Clock;
import com.bombombom.devs.core.util.Util;
import com.bombombom.devs.dto.IsUploadCompleteRequest;
import com.bombombom.devs.external.study.controller.dto.request.EditAssignmentRequest.AssignmentInfo;
import com.bombombom.devs.external.study.service.dto.command.AddAssignmentCommand;
import com.bombombom.devs.external.study.service.dto.command.DeleteAssignmentCommand;
Expand Down Expand Up @@ -65,11 +67,11 @@ public class BookStudyService implements StudyProgressService {
private final UserRepository userRepository;
private final UserStudyRepository userStudyRepository;
private final AssignmentRepository assignmentRepository;

private final UserAssignmentRepository userAssignmentRepository;
private final VideoRepository videoRepository;
private final ProblemRepository problemRepository;
private final AssignmentVoteRepository assignmentVoteRepository;
private final S3MultipartUploadClient s3MultipartUploadClient;

@Override
public StudyType getStudyType() {
Expand Down Expand Up @@ -498,4 +500,18 @@ public AssignmentVoteResult voteAssignment(Long userId, Long studyId,
return AssignmentVoteResult.fromEntity(assignmentVoteRepository.save(vote));

}

public void verifyAssignmentVideoComplete(Long userId, Long studyId, Long assignmentId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(ErrorCode.STUDY_NOT_FOUND));
Assignment assignment = assignmentRepository.findById(assignmentId)
.orElseThrow(() -> new NotFoundException(ErrorCode.ASSIGNMENT_NOT_FOUND));
if (!s3MultipartUploadClient.isUploadComplete(
IsUploadCompleteRequest.of(studyId, assignmentId))) {
throw new NotFoundException(ErrorCode.UPLOADED_VIDEO_NOT_FOUND);
}
videoRepository.save(Video.toEntity(user, assignment));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bombombom.devs.external.video.controller;

import com.bombombom.devs.S3MultipartUploadClient;
import com.bombombom.devs.dto.FinishMultipartUploadResponse;
import com.bombombom.devs.dto.GeneratePresignedUrlResponse;
import com.bombombom.devs.dto.InitiateMultipartUploadResponse;
import com.bombombom.devs.external.video.controller.dto.CompleteVideoUploadRequest;
import com.bombombom.devs.external.video.controller.dto.CompleteVideoUploadResponse;
import com.bombombom.devs.external.video.controller.dto.GenerateUploadUrlRequest;
import com.bombombom.devs.external.video.controller.dto.GenerateUploadUrlResponse;
import com.bombombom.devs.external.video.controller.dto.InitiateUploadRequest;
import com.bombombom.devs.external.video.controller.dto.InitiateUploadResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/videos")
public class VideoController {

private final S3MultipartUploadClient s3MultipartUploadClient;

@PostMapping("/initiate-upload")
public ResponseEntity<InitiateUploadResponse> initiateUpload(
@RequestBody InitiateUploadRequest request) {
InitiateMultipartUploadResponse response = s3MultipartUploadClient.initiate(
request.toS3ClientDto());
return ResponseEntity.ok(InitiateUploadResponse.fromS3ClientResponse(response));
}

@PostMapping("/presigned-url")
public ResponseEntity<GenerateUploadUrlResponse> generateMultipartUploadUrl(
@RequestBody GenerateUploadUrlRequest request) {
GeneratePresignedUrlResponse response = s3MultipartUploadClient.generatePresignedUrl(
request.toS3ClientDto());
return ResponseEntity.ok().body(GenerateUploadUrlResponse.fromS3ClientResponse(response));
}

@PostMapping("/complete-upload")
public ResponseEntity<CompleteVideoUploadResponse> completeVideoUpload(
@RequestBody CompleteVideoUploadRequest request) {
FinishMultipartUploadResponse response = s3MultipartUploadClient.finishMultipartUpload(
request.toS3ClientDto());
return ResponseEntity.ok(CompleteVideoUploadResponse.fromS3ClientResponse(response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.bombombom.devs.external.video.controller.dto;

import com.bombombom.devs.dto.FinishMultipartUploadRequest;
import com.bombombom.devs.dto.FinishMultipartUploadRequest.Part;
import java.util.List;
import lombok.Builder;

@Builder
public record CompleteVideoUploadRequest(
String uploadId,
List<Part> parts,
long studyId,
long assignmentId
) {

public FinishMultipartUploadRequest toS3ClientDto() {
return FinishMultipartUploadRequest.builder()
.uploadId(uploadId)
.objectName("task/" + studyId + "/" + assignmentId)
.parts(parts)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.bombombom.devs.external.video.controller.dto;

import com.bombombom.devs.dto.FinishMultipartUploadResponse;
import lombok.Builder;

@Builder
public record CompleteVideoUploadResponse(
String bucketName,
String objectName
) {

public static CompleteVideoUploadResponse fromS3ClientResponse(
FinishMultipartUploadResponse response) {
return CompleteVideoUploadResponse.builder()
.bucketName(response.bucket())
.objectName(response.key())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.bombombom.devs.external.video.controller.dto;

import com.bombombom.devs.dto.GeneratePresignedUrlRequest;
import jakarta.validation.constraints.Min;

public record GenerateUploadUrlRequest(
String uploadId,
@Min(0) int partNumber,
long partSize,
@Min(0) long studyId,
@Min(0) long assignmentId
) {

public GeneratePresignedUrlRequest toS3ClientDto() {
return GeneratePresignedUrlRequest.builder()
.uploadId(uploadId)
.partNumber(partNumber)
.contentType("video/*")
.objectName("task/" + studyId + "/" + assignmentId)
.contentLength(partSize)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bombombom.devs.external.video.controller.dto;

import com.bombombom.devs.dto.GeneratePresignedUrlResponse;
import lombok.Builder;

@Builder
public record GenerateUploadUrlResponse(
String presignedUrl
) {

public static GenerateUploadUrlResponse fromS3ClientResponse(
GeneratePresignedUrlResponse response) {
return GenerateUploadUrlResponse.builder()
.presignedUrl(response.presignedUrl())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bombombom.devs.external.video.controller.dto;

import com.bombombom.devs.dto.InitiateMultipartUploadRequest;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;

public record InitiateUploadRequest(
@Min(0) long studyId,
@Min(0) long assignmentId,
@Pattern(regexp = "^video/.+", message = "λΉ„λ””μ˜€ 파일의 MIME νƒ€μž…λ§Œ κ°€λŠ₯ν•©λ‹ˆλ‹€.") String fileType
) {

public InitiateMultipartUploadRequest toS3ClientDto() {
return InitiateMultipartUploadRequest.builder()
.objectName("task/" + studyId + "/" + assignmentId)
.studyId(String.valueOf(studyId))
.assignmentId(String.valueOf(assignmentId))
.fileType(fileType)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bombombom.devs.external.video.controller.dto;

import com.bombombom.devs.dto.InitiateMultipartUploadResponse;
import lombok.Builder;

@Builder
public record InitiateUploadResponse(
String uploadId
) {

public static InitiateUploadResponse fromS3ClientResponse(
InitiateMultipartUploadResponse response) {
return InitiateUploadResponse.builder()
.uploadId(response.uploadId())
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public enum ErrorCode {
USER_ASSIGNMENT_NOT_FOUND(NOT_FOUND, 40407, "μœ μ €μ— 과제λ₯Ό ν• λ‹Ήν•œ 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
NEXT_ROUND_NOT_FOUND(NOT_FOUND, 40408, "λ‹€μŒ νšŒμ°¨κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
ASYMMETRIC_KEY_NOT_FOUND(NOT_FOUND, 40409, "ν•΄λ‹Ή version의 λΉ„λŒ€μΉ­ ν‚€κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
UPLOADED_VIDEO_NOT_FOUND(NOT_FOUND, 40410, "μ—…λ‘œλ“œλœ ν•΄μ„€ μ˜μƒμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),

// NOT_ACCEPTABLE 406
STUDY_STARTED(NOT_ACCEPTABLE, 40600, "μŠ€ν„°λ””κ°€ μ‹œμž‘ν•˜μ˜€μŠ΅λ‹ˆλ‹€."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@ public class Video extends BaseEntity {
foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
private Assignment assignment;


public static Video toEntity(User uploader, Assignment assignment) {
return Video.builder()
.uploader(uploader)
.assignment(assignment)
.build();
}
}
11 changes: 11 additions & 0 deletions domain/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ spring:
expire-length: ${ACCESS_TOKEN_EXPIRE}
refresh-token:
expire-length: ${REFRESH_TOKEN_EXPIRE}
cloud:
aws:
region:
static: ap-northeast-2
credentials:
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
s3:
bucket: ${AWS_S3_BUCKET}

resilience4j:
circuitbreaker:
failure-rate-threshold: 10
Expand All @@ -42,6 +52,7 @@ resilience4j:
retry:
wait-duration: 100
max-attempts: 2

naver:
url:
search:
Expand Down
11 changes: 11 additions & 0 deletions infra/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
subprojects {
dependencies {
compileOnly 'org.springframework:spring-context'
implementation project(':core')

testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'com.squareup.okhttp3:mockwebserver'
}
}
14 changes: 14 additions & 0 deletions infra/s3/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ext {
springCloudAwsVersion = '3.1.1'
}

dependencies {
compileOnly 'org.springframework:spring-context'

implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudAwsVersion}")
implementation 'software.amazon.awssdk:bom:2.27.17'
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'

testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
Loading

0 comments on commit 7368e5b

Please sign in to comment.