Skip to content

Commit

Permalink
feat: 스토리지 업로드 서버를 분리한다. (#579)
Browse files Browse the repository at this point in the history
* refactor: ThumbnailManager 인터페이스화

* chore: s3 업로드 프록시 서버 프로젝트 생성

* chore: RestAssured 의존성 추가

* chore: 저장소 설정값을 저장할 서브모듈 추가

* chore: 기본 프로파일 설정값 세팅

* feat: 업로드 기능 구현

* feat: 예외 핸들링 로직 작성

* test: Service 통합테스트, S3Uploader 단위 테스트 작성

* refactor: RequestMapping -> PostMapping 어노테이션으로 대체

* fix: StorageConfig의 local, test 설정 오류 수정

* feat: 삭제 기능 구현

* test: 삭제 관련 Service 및 단위 테스트 구현

* refactor: 예외의 로깅 레벨을 warn을 기본값으로 설정

* feat: logback 로깅 설정

* fix: 응답 헤더의 url이 잘못된 문제 해결

* chore: cloud.aws.stack.auto=false 부여

* chore: 배포 쉘스크립트 작성

* feat: S3Proxy를 이용해 파일 업로드하도록 변경

* refactor: Amazon Cloud 관련 의존성 및 객체 삭제

* refactor: S3ProxyUploader 예외 구체화

* fix: 스크립트 수정

* chore: gradle 버전 다운그레이드

* chore: API 문서화

* test: S3Proxy 단위 테스트 작성

* docs: API 문서 구체화

* refactor: API 문서 내용 일관화

* docs: API 문서 제목 설정

* test: 테스트에 사용할 샘플 png 파일 업로드

* fix: thumbnails 디렉토리명 수정

* test: S3Proxy 예외 케이스 추가

* chore: 주석처리한 의존성 삭제

* chore: 서브모듈 최신화

* refactor: 빈 생성자 파라미터에 final 속성 부여

* refactor: 예외 미발생 테스트코드들 assertDoesNotThrow() 이용

* refactor: S3Uploader MultiPartFile의 InputStream close() 메소드 호출

* fix: 병합으로 인한 컴파일 에러 해결

* style: S3ProxyUploader 상수 오타 수정

Co-authored-by: Jungseok Sung <[email protected]>

* fix: S3ProxyUploader 상수 오타 수정으로 인한 컴파일 오류 해결

* refactor: 썸네일 업로드할 디렉토리 네임을 config를 통해 받음

dev 서버의 이미지 유실을 방지합니다.

* chore: 서브모듈에 s3proxy 관련 설정 업로드

* chore: local, test 프로파일일 때 올라가는 썸네일 디렉토리명 변경

* refactor: ThumbnailManagerImpl 패키지 위치 이동

* refactor: 썸네일을 위해 변환된 파일 삭제 불가시 예외 발생

* refactor: S3ProxyUploader 패키지 변경

* refactor: S3ProxyUploaderTest 패키지 경로 이동

* test: ThumbnailManagerImpl에 대한 테스트 코드 작성

Co-authored-by: Jungseok Sung <[email protected]>
  • Loading branch information
tributetothemoon and sakjung authored Oct 5, 2021
1 parent 7ca0aca commit c5b1ae3
Show file tree
Hide file tree
Showing 60 changed files with 1,620 additions and 230 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.idea
.DS_Store


s3proxy/src/main/resources/static/docs/index.html
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
path = backend/src/main/resources/config
url = [email protected]:zzimkkong/config.git
branch = main
[submodule "s3proxy/src/main/resources/s3proxy-config"]
path = s3proxy/src/main/resources/s3proxy-config
url = [email protected]:zzimkkong/s3proxy-config.git
branch = main
4 changes: 1 addition & 3 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'


// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.woowacourse.zzimkkong.config;

import org.apache.http.HttpHeaders;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.woowacourse.zzimkkong.exception.infrastructure;

import org.springframework.http.HttpStatus;

public class CannotDeleteConvertedFileException extends InfrastructureMalfunctionException {
private static final String MESSAGE = "변환된 이미지를 삭제하는 데에 실패했습니다. 관리자에게 문의하세요.";

public CannotDeleteConvertedFileException() {
super(MESSAGE, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.woowacourse.zzimkkong.exception.infrastructure;

import org.springframework.http.HttpStatus;

public class S3ProxyRespondedFailException extends InfrastructureMalfunctionException {
private static final String MESSAGE = "이미지 버킷 업로드에 실패했습니다.";

public S3ProxyRespondedFailException() {
super(MESSAGE, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
public class S3UploadException extends InfrastructureMalfunctionException {
private static final String MESSAGE = "이미지 버킷 업로드에 실패했습니다.";

public S3UploadException() {
super(MESSAGE, HttpStatus.INTERNAL_SERVER_ERROR);
}

public S3UploadException(final Exception exception) {
super(MESSAGE, exception, HttpStatus.INTERNAL_SERVER_ERROR);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.woowacourse.zzimkkong.infrastructure.thumbnail;

import com.woowacourse.zzimkkong.exception.infrastructure.S3ProxyRespondedFailException;
import com.woowacourse.zzimkkong.exception.infrastructure.S3UploadException;
import com.woowacourse.zzimkkong.infrastructure.thumbnail.StorageUploader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Objects;

@Component
public class S3ProxyUploader implements StorageUploader {
private static final String PATH_DELIMITER = "/";
private static final String API_PATH = "/api/storage";
private static final String CONTENT_DISPOSITION_HEADER_VALUE_FORMAT = "form-data; name=file; filename=%s";

private final WebClient proxyServerClient;

public S3ProxyUploader(
@Value("${s3proxy.server-uri}") final String serverUri) {
this.proxyServerClient = WebClient.builder()
.baseUrl(serverUri)
.build();
}

@Override
public String upload(String directoryName, File uploadFile) {
try {
byte[] byteArrayOfFile = Files.readAllBytes(uploadFile.toPath());

MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
multipartBodyBuilder.part("file", new ByteArrayResource(byteArrayOfFile))
.header(HttpHeaders.CONTENT_DISPOSITION,
String.format(CONTENT_DISPOSITION_HEADER_VALUE_FORMAT, uploadFile.getName()));

return proxyServerClient
.method(HttpMethod.POST)
.uri(String.join(PATH_DELIMITER, API_PATH, directoryName))
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.CREATED)) {
String location = Objects.requireNonNull(
clientResponse.headers().asHttpHeaders().get(HttpHeaders.LOCATION))
.stream().findFirst()
.orElseThrow(S3UploadException::new);
return Mono.just(location);
}
return Mono.error(S3ProxyRespondedFailException::new);
})
.block();
} catch (IOException exception) {
throw new S3UploadException(exception);
}
}

@Override
public void delete(String directoryName, String fileName) {
proxyServerClient
.method(HttpMethod.DELETE)
.uri(String.join(PATH_DELIMITER, API_PATH, directoryName, fileName))
.retrieve()
.bodyToMono(String.class)
.block();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
package com.woowacourse.zzimkkong.infrastructure.thumbnail;

import com.woowacourse.zzimkkong.domain.Map;
import org.springframework.stereotype.Component;

import java.io.File;
public interface ThumbnailManager {
String uploadMapThumbnail(final String svgData, final Map map);

@Component
public class ThumbnailManager {
public static final String THUMBNAILS_DIRECTORY_NAME = "thumbnails";
public static final String THUMBNAIL_EXTENSION = ".png";
private static final String THUMBNAIL_FILE_FORMAT = "%s";

private final SvgConverter svgConverter;
private final StorageUploader storageUploader;

public ThumbnailManager(final SvgConverter svgConverter, final StorageUploader storageUploader) {
this.svgConverter = svgConverter;
this.storageUploader = storageUploader;
}

public String uploadMapThumbnail(final String svgData, final Map map) {
String fileName = makeThumbnailFileName(map);
File pngFile = svgConverter.convertSvgToPngFile(svgData, fileName);

String thumbnailUrl = storageUploader.upload(THUMBNAILS_DIRECTORY_NAME, pngFile);

pngFile.delete();
return thumbnailUrl;
}

public void deleteThumbnail(final Map map) {
String fileName = makeThumbnailFileName(map);
storageUploader.delete(THUMBNAILS_DIRECTORY_NAME, fileName + THUMBNAIL_EXTENSION);
}

private String makeThumbnailFileName(final Map map) {
return String.format(THUMBNAIL_FILE_FORMAT, map.getId().toString());
}
void deleteThumbnail(final Map map);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.woowacourse.zzimkkong.infrastructure.thumbnail;

import com.woowacourse.zzimkkong.domain.Map;
import com.woowacourse.zzimkkong.exception.infrastructure.CannotDeleteConvertedFileException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.File;

@Component
public class ThumbnailManagerImpl implements ThumbnailManager {
public static final String THUMBNAIL_EXTENSION = ".png";
private static final String THUMBNAIL_FILE_FORMAT = "%s";

private final SvgConverter svgConverter;
private final StorageUploader storageUploader;
private final String thumbnailsDirectoryName;

public ThumbnailManagerImpl(
final SvgConverter svgConverter,
final StorageUploader storageUploader,
@Value("${s3proxy.thumbnails-directory}") final String thumbnailsDirectoryName) {
this.svgConverter = svgConverter;
this.storageUploader = storageUploader;
this.thumbnailsDirectoryName = thumbnailsDirectoryName;

}

public String uploadMapThumbnail(final String svgData, final Map map) {
String fileName = makeThumbnailFileName(map);
File pngFile = svgConverter.convertSvgToPngFile(svgData, fileName);

String thumbnailUrl = storageUploader.upload(thumbnailsDirectoryName, pngFile);

if (!pngFile.delete()) {
throw new CannotDeleteConvertedFileException();
}
return thumbnailUrl;
}

public void deleteThumbnail(final Map map) {
String fileName = makeThumbnailFileName(map);
storageUploader.delete(thumbnailsDirectoryName, fileName + THUMBNAIL_EXTENSION);
}

private String makeThumbnailFileName(final Map map) {
return String.format(THUMBNAIL_FILE_FORMAT, map.getId().toString());
}
}
8 changes: 3 additions & 5 deletions backend/src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ spring.mvc.view.suffix=.html
jwt.token.secret-key=zzimkkong_secret_key_in_dev
jwt.token.expire-length=86400000

#s3
aws.s3.bucket_name=zzimkkong-thumbnail-dev
aws.s3.region=ap-northeast-2
aws.s3.url_replacement=https://d3tdpsdxqmqd52.cloudfront.net
cloud.aws.stack.auto=false
# s3
s3proxy.server-uri=http://52.78.88.220:8080
s3proxy.thumbnails-directory=thumbnails

# svg converter
converter.temp.location=/home/ubuntu/zzimkkong/tmp/
Expand Down
9 changes: 3 additions & 6 deletions backend/src/main/resources/application-local.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,9 @@ logging.level.jdbc.sqlonly=debug
jwt.token.secret-key=zzimkkong_secret_key_in_dev
jwt.token.expire-length=86400000

#s3
aws.s3.bucket_name=zzimkkong-personal
aws.s3.region=ap-northeast-2
aws.s3.url_replacement=https://zzimkkong-personal.s3.ap-northeast-2.amazonaws.com
cloud.aws.stack.auto=false
cloud.aws.region.static=ap-northeast-2
# s3
s3proxy.server-uri=http://52.78.88.220:8080
s3proxy.thumbnails-directory=thumbnails-local

# svg converter
converter.temp.location=src/main/resources/tmp/
Expand Down
9 changes: 3 additions & 6 deletions backend/src/main/resources/application-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ spring.mvc.view.suffix=.html
jwt.token.secret-key=zzimkkong_secret_key_in_dev
jwt.token.expire-length=86400000

#s3
aws.s3.bucket_name=zzimkkong-personal
aws.s3.region=ap-northeast-2
aws.s3.url_replacement=https://zzimkkong-personal.s3.ap-northeast-2.amazonaws.com
cloud.aws.stack.auto=false
cloud.aws.region.static=ap-northeast-2
# s3
s3proxy.server-uri=http://52.78.88.220:8080
s3proxy.thumbnails-directory=thumbnails-test

# svg converter
converter.temp.location=src/main/resources/tmp/
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/config
Loading

0 comments on commit c5b1ae3

Please sign in to comment.