Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ use oracle object storage to upload image #426

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 52 additions & 51 deletions backend/build.gradle
Original file line number Diff line number Diff line change
@@ -1,87 +1,88 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.7'
id 'io.spring.dependency-management' version '1.1.5'
id 'jacoco'
id 'com.epages.restdocs-api-spec' version '0.18.2'
id 'java'
id 'org.springframework.boot' version '3.2.7'
id 'io.spring.dependency-management' version '1.1.5'
id 'jacoco'
id 'com.epages.restdocs-api-spec' version '0.18.2'
Comment on lines -2 to +6

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존은 tab 기반으로 gradle 작성했었고 변경된건 스페이스4개 기준이네요.
혹시 어떤게 컨벤션에 맞는건가요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gradle indent 컨벤션에 대해서 저희끼리 논의해봐야 할 것 같아요😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기를 보면 스페이스 4개가 컨벤션같아요~~

}

group = 'net.pengcook'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5'
implementation 'com.google.firebase:firebase-admin:9.3.0'
implementation 'com.auth0:java-jwt:4.4.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'com.github.loki4j:loki-logback-appender:1.5.1'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.zalando:logbook-spring-boot-starter:3.9.0'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5'
implementation 'com.google.firebase:firebase-admin:9.3.0'
implementation 'com.auth0:java-jwt:4.4.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'com.github.loki4j:loki-logback-appender:1.5.1'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.zalando:logbook-spring-boot-starter:3.9.0'

runtimeOnly 'mysql:mysql-connector-java:8.0.33'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java:8.0.33'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation platform('software.amazon.awssdk:bom:2.17.89')
implementation 'software.amazon.awssdk:s3'
implementation platform('com.oracle.oci.sdk:oci-java-sdk-bom:3.54.0')
implementation 'com.oracle.oci.sdk:oci-java-sdk-objectstorage'
implementation 'com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3'
}

test {
useJUnitPlatform()
systemProperty 'spring.profiles.active', 'test'
useJUnitPlatform()
systemProperty 'spring.profiles.active', 'test'
}

openapi3 {
servers = [{ url = "https://dev.pengcook.net" }, { url = "http://localhost:8080" }]
title = 'Pengcook API'
description = 'Pengcook API description'
version = '0.1.0'
format = 'yaml'
servers = [{ url = "https://dev.pengcook.net" }, { url = "http://localhost:8080" }]
title = 'Pengcook API'
description = 'Pengcook API description'
version = '0.1.0'
format = 'yaml'
}

tasks.register("copyOasToSwagger", Copy) {
dependsOn("openapi3")
dependsOn("openapi3")

from layout.buildDirectory.file("api-spec/openapi3.yaml").get()
into "src/main/resources/static"
from layout.buildDirectory.file("api-spec/openapi3.yaml").get()
into "src/main/resources/static"
}

tasks.named("jar") {
enabled = false
enabled = false
}

jacocoTestReport {
dependsOn("test")
reports {
xml.required = true
csv.required = false
html.required = false
}
dependsOn("test")
reports {
xml.required = true
csv.required = false
html.required = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import net.pengcook.authentication.exception.DuplicationException;
import net.pengcook.authentication.exception.FirebaseTokenException;
import net.pengcook.authentication.exception.NoSuchUserException;
import net.pengcook.image.service.S3ClientService;
import net.pengcook.image.service.ImageClientService;
import net.pengcook.user.domain.User;
import net.pengcook.user.repository.UserRepository;
import org.springframework.stereotype.Service;
Expand All @@ -28,7 +28,7 @@ public class LoginService {
private final FirebaseAuth firebaseAuth;
private final UserRepository userRepository;
private final JwtTokenManager jwtTokenManager;
private final S3ClientService s3ClientService;
private final ImageClientService imageClientService;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


@Transactional(readOnly = true)
public GoogleLoginResponse loginWithGoogle(GoogleLoginRequest googleLoginRequest) {
Expand Down Expand Up @@ -90,7 +90,7 @@ private User createUser(GoogleSignUpRequest googleSignUpRequest) {
userImage = decodedToken.getPicture();
}
if (!userImage.startsWith("http")) {
userImage = s3ClientService.getImageUrl(userImage).url();
userImage = imageClientService.getImageUrl(userImage).url();
}

return new User(
Expand Down
58 changes: 58 additions & 0 deletions backend/src/main/java/net/pengcook/image/config/ImageConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.pengcook.image.config;

import com.oracle.bmc.Region;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider;
import com.oracle.bmc.objectstorage.ObjectStorage;
import com.oracle.bmc.objectstorage.ObjectStorageClient;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import net.pengcook.image.service.ImageClientService;
import net.pengcook.image.service.ObjectStorageClientService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ImageConfig {

@Value("${oracle.cloud.user-id}")
private String userId;

@Value("${oracle.cloud.tenancy-id}")
private String tenancyId;

@Value("${oracle.cloud.fingerprint}")
private String fingerprint;

@Value("${oracle.cloud.private-key}")
private String privateKey;

@Value("${oracle.cloud.region}")
private String region;

@Bean
public ObjectStorage objectStorage() {
AuthenticationDetailsProvider provider = SimpleAuthenticationDetailsProvider.builder()
.userId(userId)
.tenantId(tenancyId)
.fingerprint(fingerprint)
.privateKeySupplier(() -> {
try {
return new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("Failed to load private key", e);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외가 터졌을때 Oracle Object Storage 문제라는것을 바로 알 수 있게 메시지에 추가하는것도 좋아보입니다!
물론 에외에 이미 나와있을것 같아서 선택적으로 반영하면 좋을것 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가적으로 예외 메시지가 한글인 게 좋을 것 같아 이 코멘트를 반영하며 함께 수정했습니다!

})
.region(Region.fromRegionId(region))
.build();

return ObjectStorageClient.builder()
.build(provider);
}

@Bean
public ImageClientService imageClientService() {
return new ObjectStorageClientService(objectStorage());
}
}
34 changes: 0 additions & 34 deletions backend/src/main/java/net/pengcook/image/config/S3Config.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package net.pengcook.image.controller;

import lombok.RequiredArgsConstructor;
import net.pengcook.image.dto.PresignedUrlResponse;
import net.pengcook.image.service.S3ClientService;
import net.pengcook.image.dto.UploadUrlResponse;
import net.pengcook.image.service.ImageClientService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -11,12 +11,12 @@
@RestController
@RequestMapping("/image")
@RequiredArgsConstructor
public class S3Controller {
public class ImageController {

private final S3ClientService s3ClientService;
private final ImageClientService imageClientService;

@GetMapping
public PresignedUrlResponse getPresignedURL(@RequestParam String fileName) {
return s3ClientService.generatePresignedPutUrl(fileName);
public UploadUrlResponse getUploadURL(@RequestParam String fileName) {
return imageClientService.generateUploadUrl(fileName);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package net.pengcook.image.dto;

public record UploadUrlResponse(String url) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.pengcook.image.service;

import net.pengcook.image.dto.ImageUrlResponse;
import net.pengcook.image.dto.UploadUrlResponse;

public interface ImageClientService {

UploadUrlResponse generateUploadUrl(String fileName);

ImageUrlResponse getImageUrl(String fileName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package net.pengcook.image.service;

import com.oracle.bmc.objectstorage.ObjectStorage;
import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails;
import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails.AccessType;
import com.oracle.bmc.objectstorage.requests.CreatePreauthenticatedRequestRequest;
import java.time.Instant;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.pengcook.image.dto.ImageUrlResponse;
import net.pengcook.image.dto.UploadUrlResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class ObjectStorageClientService implements ImageClientService {

private final ObjectStorage client;

@Value("${oracle.cloud.bucket-name}")
private String bucketName;

@Value("${oracle.cloud.namespace}")
private String namespace;

@Value("${oracle.cloud.url-prefix}")
private String urlPrefix;

private static final int DURATION_SECONDS = 600;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 시간은 어떤값인가요? 600인 기준은 어떤 이유인가요?
또는 이 값을 yaml로 이동시키는것도 고려 해보셨나요!?

Copy link
Contributor

@tackyu tackyu Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지 업로드를 위해 제공하는 PAR의 만료 시간입니다.
펭쿡이 이전에 사용하던 presigned url에 부여하던 만료기한 규칙과 동일하게 적용했습니다.

yaml로 해당 값을 관리하는게 유지보수에 더 용이하겠네요! 감사합니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 동의해서 수정했습니다~~


public UploadUrlResponse generateUploadUrl(String fileName) {
CreatePreauthenticatedRequestDetails details = CreatePreauthenticatedRequestDetails.builder()
.accessType(AccessType.ObjectWrite)
.name("upload-" + fileName)
.timeExpires(Date.from(Instant.now().plusSeconds(DURATION_SECONDS)))
.objectName(fileName)
.build();

CreatePreauthenticatedRequestRequest request = CreatePreauthenticatedRequestRequest.builder()
.namespaceName(namespace)
.bucketName(bucketName)
.createPreauthenticatedRequestDetails(details)
.build();

String uploadUri = client.createPreauthenticatedRequest(request)
.getPreauthenticatedRequest()
.getAccessUri();

return new UploadUrlResponse(urlPrefix + uploadUri);
}

public ImageUrlResponse getImageUrl(String fileName) {
return new ImageUrlResponse(String.format("%s/n/%s/b/%s/o/%s",
client.getEndpoint(),
namespace,
bucketName,
fileName));
}
Comment on lines +57 to +62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/n/은 어떤 의미인가요?

Copy link
Contributor

@tackyu tackyu Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오라클에서 제공하는 url 형식입니다
리소스 식별하는 구분자로 사용됩니다!
/n namespace
/b buckeName
/o object

}
Loading