Skip to content

Commit

Permalink
[#91] 영상 업로드 로직 변경(presigned url 발행 후 저장 방식) (#98)
Browse files Browse the repository at this point in the history
* refactor: 기록 생성 api에서 video multipart 대신 videoUrl 받도록 수정

* refactor: s3Service video upload 로직 제거 및 presignedUrl 생성 로직 추가

* refactor: s3Service 로직 변경에 따른 RecordService 리팩토링

* feat: s3 presignedUrl 생성 api 추가

* fix: presingedUrl에서 videoUrl 추출하는 과정 중 발생한 버그 수정

* add: signIn, signUp response body에 memberId 추가 (#103)

* fix: @ModelAttribute -> @requestbody로 변경
  • Loading branch information
Bellroute authored Aug 7, 2024
1 parent c749510 commit a30c49f
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -32,7 +33,7 @@ public class RecordController {
@PostMapping("/records")
public ResponseEntity<Map<String, Long>> create(
@LoginMember Member loginMember,
@ModelAttribute RecordCreateRequest request) throws IOException {
@RequestBody RecordCreateRequest request) throws IOException {

Record record = recordService.createRecord(loginMember, request);
return ResponseEntity.ok().body(Map.of("recordId", record.getId()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ public class RecordCreateRequest {
private Long levelId;

@NotNull
private MultipartFile video;
private String videoUrl;

public RecordCreateRequest(Long gymId, Long levelId, MultipartFile video) {
public RecordCreateRequest(Long gymId, Long levelId, String videoUrl) {
this.gymId = gymId;
this.levelId = levelId;
this.video = video;
this.videoUrl = videoUrl;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
@Service
public class RecordService {
Expand All @@ -44,8 +46,14 @@ public Record createRecord(Member loginMember, RecordCreateRequest request) thro
Level level = levelRepository.findById(request.getLevelId())
.orElseThrow(() -> new EntityNotFoundException(request.getLevelId() + "is not found"));

String videoUrl = s3Service.uploadVideoFile(request.getVideo());
String thumbnailImageUrl = s3Service.uploadImageFile(thumbnailExtractor.extractImage(videoUrl));
String videoUrl = request.getVideoUrl();
String thumbnailImageUrl = "";
try {
thumbnailImageUrl = s3Service.uploadThumbnailImageFile(thumbnailExtractor.extractImage(videoUrl));
} catch (Exception e) {
log.error("error: ", e);
// TODO 썸네일 이미지 저장 안되었을 때 처리
}

Record record = Record.builder()
.member(loginMember)
Expand All @@ -56,8 +64,7 @@ public Record createRecord(Member loginMember, RecordCreateRequest request) thro
.thumbnailUrl(thumbnailImageUrl)
.build();

Record save = recordRepository.save(record);
return save;
return recordRepository.save(record);
}

@Transactional
Expand Down
78 changes: 38 additions & 40 deletions src/main/java/com/climingo/climingoApi/upload/S3Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.climingo.climingoApi.upload.api.request.PresignedUrlCreateRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@Service
Expand All @@ -29,52 +26,53 @@ public class S3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Transactional(propagation = Propagation.REQUIRED)
public String uploadVideoFile(MultipartFile videoFile) throws IOException {
StringBuilder fileName = new StringBuilder();
fileName.append("비디오_")
.append(LocalDateTime.now())
.append(".")
.append(StringUtils.getFilenameExtension(videoFile.getOriginalFilename()));
public String uploadThumbnailImageFile(File image) {
String fileName = new StringBuilder().append("썸네일_")
.append(LocalDateTime.now())
.append(".")
.append(StringUtils.getFilenameExtension(image.getName()))
.toString();

s3Client.putObject(new PutObjectRequest(bucket, fileName, image).withCannedAcl(
CannedAccessControlList.PublicRead));

File file = convertMultipartFileToFile(videoFile);
s3Client.putObject(new PutObjectRequest(bucket, fileName.toString(), file).withCannedAcl(CannedAccessControlList.PublicRead));
file.delete();
image.delete();

URL videoUrl = generatePermanentPresignedUrl(fileName.toString());
URL url = s3Client.getUrl(bucket, fileName);

return videoUrl.toString().substring(0, videoUrl.toString().indexOf("?"));
return url.toString();
}

private File convertMultipartFileToFile(MultipartFile file) throws IOException {
File convertedFile = new File(file.getOriginalFilename());
FileOutputStream fos = new FileOutputStream(convertedFile);
fos.write(file.getBytes());
fos.close();
return convertedFile;
public URL generatePresignedUrl(PresignedUrlCreateRequest request) {
String fileName = parseFileName("비디오_", request.getFileName(), request.getExtension());
return s3Client.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucket, fileName));
}

public URL generatePermanentPresignedUrl(String objectKey) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, objectKey)
.withMethod(HttpMethod.GET);

return s3Client.generatePresignedUrl(generatePresignedUrlRequest);
private String parseFileName(String prefix, String fileName, String extension) {
return prefix
+ fileName
+ "_"
+ LocalDateTime.now()
+ "."
+ extension;
}

public String uploadImageFile(File image) {
StringBuilder fileName = new StringBuilder();
fileName.append("썸네일_")
.append(LocalDateTime.now())
.append(".")
.append(StringUtils.getFilenameExtension(image.getName()));
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(
bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPreSignedUrlExpiration());
generatePresignedUrlRequest.addRequestParameter("x-amz-acl", "public-read");

s3Client.putObject(new PutObjectRequest(bucket, fileName.toString(), image).withCannedAcl(
CannedAccessControlList.PublicRead));
image.delete();
return generatePresignedUrlRequest;
}

URL url = generatePermanentPresignedUrl(fileName.toString());
private Date getPreSignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 3; // 3분
expiration.setTime(expTimeMillis);

return url.toString().substring(0, url.toString().indexOf("?"));
return expiration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
Expand Down Expand Up @@ -37,7 +38,9 @@ public void init() {
}

public File extractImage(String videoPath) throws IOException {
File thumbnail = File.createTempFile("temp" + LocalDateTime.now(), ".jpg");
File thumbnail = new File("thumbnail_"
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))
+ ".jpg");

try {
FFmpegBuilder builder = new FFmpegBuilder()
Expand All @@ -46,11 +49,12 @@ public File extractImage(String videoPath) throws IOException {
.addExtraArgs("-ss", "00:00:02")
.addOutput(thumbnail.getAbsolutePath())
.setFrames(100)
.addExtraArgs("-update", "1")
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
} catch (Exception e) {
log.info(e.getMessage());
log.error("Error extracting thumbnail from " + videoPath, e);
}
return thumbnail;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.climingo.climingoApi.upload.api;

import com.climingo.climingoApi.upload.S3Service;
import com.climingo.climingoApi.upload.api.request.PresignedUrlCreateRequest;
import com.climingo.climingoApi.upload.api.response.PresignedUrlResponse;
import lombok.RequiredArgsConstructor;
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.RestController;

@RestController
@RequiredArgsConstructor
public class S3Controller {

private final S3Service s3Service;

@PostMapping("/s3/presigned-url")
public ResponseEntity<PresignedUrlResponse> generatePresignedUrl(
@RequestBody PresignedUrlCreateRequest request) {
String presignedUrl = s3Service.generatePresignedUrl(request).toString();
String videoUrl = parseUrlWithoutQuery(presignedUrl);

return ResponseEntity.ok(new PresignedUrlResponse(presignedUrl, videoUrl));
}

private String parseUrlWithoutQuery(String url) {
int queryIndex = url.indexOf("?");

if (queryIndex != -1) {
return url.substring(0, queryIndex);
}

return url;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.climingo.climingoApi.upload.api.request;

import jakarta.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class PresignedUrlCreateRequest {

@NotNull
private String fileName;

@NotNull
private String extension;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.climingo.climingoApi.upload.api.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class PresignedUrlResponse {

private final String presignedUrl;
private final String videoUrl;
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ void setUp() throws IOException {
levelRepository = mock(LevelRepository.class);
recordRepository = mock(RecordRepository.class);

when(s3Service.uploadVideoFile(any())).thenReturn("http://mock-video-url");
when(s3Service.uploadImageFile(any())).thenReturn("http://mock-thumbnail-url");
when(s3Service.uploadThumbnailImageFile(any())).thenReturn("http://mock-thumbnail-url");
when(thumbnailExtractor.extractImage(any())).thenReturn(mock(File.class));

recordService = new RecordService(
Expand All @@ -59,12 +58,12 @@ void setUp() throws IOException {
void create_test() throws IOException {
Long mockGymId = 1L;
Long mockLevelId = 1L;
MultipartFile mockVideo = mock(MultipartFile.class);
String mockVideoUrl = "http://mock-video-url";

Member loginMember = Member.builder()
.id(99999L)
.build();
RecordCreateRequest request = new RecordCreateRequest(mockGymId, mockLevelId, mockVideo);
RecordCreateRequest request = new RecordCreateRequest(mockGymId, mockLevelId, mockVideoUrl);

Record expected = Record.builder()
.member(loginMember)
Expand Down

0 comments on commit a30c49f

Please sign in to comment.