From e3a879b55772da6fa1ae95246e27f62d314a9f19 Mon Sep 17 00:00:00 2001 From: joshua Date: Sun, 21 Sep 2025 11:35:55 +0800 Subject: [PATCH 1/3] refactor: mr comments --- pom.xml | 5 + .../backend/event/domain/models/Agenda.java | 6 +- .../backend/event/domain/models/Event.java | 4 +- .../backend/event/utils/DateUtils.java | 9 -- .../markguiang/backend/event/utils/Utils.java | 18 +++ .../storage/StorageService.java | 103 +++++++++++++++++- .../storage/base/ObjectStore.java | 3 +- .../storage/base/StoreProperties.java | 65 +++++++++++ 8 files changed, 197 insertions(+), 16 deletions(-) delete mode 100644 src/main/java/com/markguiang/backend/event/utils/DateUtils.java create mode 100644 src/main/java/com/markguiang/backend/event/utils/Utils.java create mode 100644 src/main/java/com/markguiang/backend/infrastructure/storage/base/StoreProperties.java diff --git a/pom.xml b/pom.xml index 4132276..8795b04 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,11 @@ firebase-admin 9.5.0 + + com.google.cloud + google-cloud-storage + 2.55.0 + diff --git a/src/main/java/com/markguiang/backend/event/domain/models/Agenda.java b/src/main/java/com/markguiang/backend/event/domain/models/Agenda.java index 57e4c79..b7dc5ed 100644 --- a/src/main/java/com/markguiang/backend/event/domain/models/Agenda.java +++ b/src/main/java/com/markguiang/backend/event/domain/models/Agenda.java @@ -2,7 +2,7 @@ import com.markguiang.backend.base.exceptions.InvalidDateRangeException; import com.markguiang.backend.base.model.ValueObject; -import com.markguiang.backend.event.utils.DateUtils; +import com.markguiang.backend.event.utils.Utils; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Comparator; @@ -32,8 +32,8 @@ public static boolean hasOverlappingTimes(List agendaList) { public static boolean allOnDate(List agendaList, OffsetDateTime date) { Objects.requireNonNull(date); for (Agenda agenda : agendaList) { - if (!DateUtils.onSameDate(agenda.getStartDate(), date) - || !DateUtils.onSameDate(agenda.getEndDate(), date)) { + if (!Utils.onSameDate(agenda.getStartDate(), date) + || !Utils.onSameDate(agenda.getEndDate(), date)) { return false; } } diff --git a/src/main/java/com/markguiang/backend/event/domain/models/Event.java b/src/main/java/com/markguiang/backend/event/domain/models/Event.java index 8dbcb35..d779f62 100644 --- a/src/main/java/com/markguiang/backend/event/domain/models/Event.java +++ b/src/main/java/com/markguiang/backend/event/domain/models/Event.java @@ -4,7 +4,7 @@ import com.markguiang.backend.base.model.IdentifiableDomainObject; import com.markguiang.backend.event.exceptions.DayNotFoundException; import com.markguiang.backend.event.exceptions.DaysOnSameDateException; -import com.markguiang.backend.event.utils.DateUtils; +import com.markguiang.backend.event.utils.Utils; import java.net.URI; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -155,7 +155,7 @@ public List getDays() { private Day getDayWithDate(OffsetDateTime date) { for (Day day : days) { - if (DateUtils.onSameDate(day.getDate(), date)) { + if (Utils.onSameDate(day.getDate(), date)) { return day; } } diff --git a/src/main/java/com/markguiang/backend/event/utils/DateUtils.java b/src/main/java/com/markguiang/backend/event/utils/DateUtils.java deleted file mode 100644 index dcf3bea..0000000 --- a/src/main/java/com/markguiang/backend/event/utils/DateUtils.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.markguiang.backend.event.utils; - -import java.time.OffsetDateTime; - -public class DateUtils { - public static boolean onSameDate(OffsetDateTime a, OffsetDateTime b) { - return a.toLocalDate().equals(b.toLocalDate()); - } -} diff --git a/src/main/java/com/markguiang/backend/event/utils/Utils.java b/src/main/java/com/markguiang/backend/event/utils/Utils.java new file mode 100644 index 0000000..e94052d --- /dev/null +++ b/src/main/java/com/markguiang/backend/event/utils/Utils.java @@ -0,0 +1,18 @@ +package com.markguiang.backend.event.utils; + +import java.time.OffsetDateTime; + +public class Utils { + public static boolean onSameDate(OffsetDateTime a, OffsetDateTime b) { + return a.toLocalDate().equals(b.toLocalDate()); + } + + public static String concatenateStr(String... s) { + StringBuilder sb = new StringBuilder(); + for (String string : s) { + sb.append(string); + } + + return sb.toString(); + } +} diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java index 99426da..f1dc742 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java @@ -2,16 +2,60 @@ import com.markguiang.backend.infrastructure.storage.base.DirectObjectStore; import com.markguiang.backend.infrastructure.storage.base.ObjectStore; + +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.WriteChannel; +import com.google.cloud.storage.*; + +import com.markguiang.backend.infrastructure.storage.base.StoreProperties; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import static com.markguiang.backend.event.utils.Utils.concatenateStr; + @Service public class StorageService { private final ObjectStore os; + @Value("${GCP_PROJECTID}") + private String projectId; + + @Value("${GCP_BUCKET_PUBLIC}") + private String publicBucket; + + @Value("${GCP_BUCKET_PRIVATE}") + private String privateBucket; + + @Value("${GOOGLE_CLOUD_STORAGE_JSON}") + private String sa; + + private static final String SUFFIX = "/"; + private static final String BUCKET_PATH_EVENT = "event/"; + + private static final String BUCKET_PATH_USER = "users/"; + + private StoreProperties initProperties(Boolean isPublic) { + return StoreProperties + .builder() + .projectId(projectId) + .bucket(isPublic ? publicBucket : privateBucket) + .sa(sa) + .build(); + } + public StorageService(ObjectStore os) { this.os = os; } @@ -34,11 +78,68 @@ public InputStream fetch(URI presignedUrl) throws IOException { throw new UnsupportedOperationException("direct-storage-not-supported-by-this-implementation"); } - public URI generatePresignedUrlForUpload(String key) { + public URL generatePresignedUrlForUpload(String key) { return os.generatePresignedUrlForUpload(key); } public URI generatePresignedUrlForDownload(String key) { return os.generatePresignedUrlForDownload(key); } + + public URL generateV4PutObjectSignedUrl(StoreProperties properties, String path, String contentType) throws IOException { + + Storage storage = StorageOptions.newBuilder().setProjectId(properties.getProjectId()).build().getService(); + + BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(properties.getBucket(), path)).build(); + + Map extensionHeaders = new HashMap<>(); + extensionHeaders.put("Content-Type", contentType); + + return storage.signUrl( + blobInfo, + 15, + TimeUnit.MINUTES, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withExtHeaders(extensionHeaders), + Storage.SignUrlOption.withV4Signature(), + Storage.SignUrlOption.signWith( + ServiceAccountCredentials + .fromStream(new ByteArrayInputStream(properties.getSa().getBytes(StandardCharsets.UTF_8))) + ) + ); + } + + public Map generatePresignedUrl(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException { + String filePath; + String filename = RandomStringUtils.randomAlphabetic(20) + "." + fileExtension; + + filePath = concatenateStr( BUCKET_PATH_EVENT, id.toString(), SUFFIX, filename); + + HashMap map = new HashMap<>(); + + map.put(filePath, generateV4PutObjectSignedUrl(initProperties(isPublic), filePath, fileType)); + + return map; + } + + public URL generateSignedUrl(String objectPath) throws IOException { + Storage storage = StorageOptions.newBuilder() + .setProjectId(projectId) + .setCredentials( + ServiceAccountCredentials.fromStream( + new ByteArrayInputStream(sa.getBytes(StandardCharsets.UTF_8)) // since sa is a JSON string + ) + ) + .build() + .getService(); + + BlobInfo blobInfo = BlobInfo.newBuilder(privateBucket, objectPath).build(); + + return storage.signUrl( + blobInfo, + 15, + TimeUnit.MINUTES, + Storage.SignUrlOption.withV4Signature() + ); + } } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java index 87388db..67a0f40 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java @@ -1,9 +1,10 @@ package com.markguiang.backend.infrastructure.storage.base; import java.net.URI; +import java.net.URL; public interface ObjectStore { public URI generatePresignedUrlForDownload(String key); - public URI generatePresignedUrlForUpload(String key); + public URL generatePresignedUrlForUpload(String key); } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/base/StoreProperties.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StoreProperties.java new file mode 100644 index 0000000..b4b7d1a --- /dev/null +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StoreProperties.java @@ -0,0 +1,65 @@ +package com.markguiang.backend.infrastructure.storage.base; + +public class StoreProperties { + private String projectId; + private String bucket; + private String sa; + + public String getProjectId() { + return projectId; + } + + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + public String getBucket() { + return bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + public String getSa() { + return sa; + } + + public void setSa(String sa) { + this.sa = sa; + } + + // Manual Builder + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String projectId; + private String bucket; + private String sa; + + public Builder projectId(String projectId) { + this.projectId = projectId; + return this; + } + + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + public Builder sa(String sa) { + this.sa = sa; + return this; + } + + public StoreProperties build() { + StoreProperties props = new StoreProperties(); + props.setProjectId(projectId); + props.setBucket(bucket); + props.setSa(sa); + return props; + } + } +} From a2f258e1083f34d734332ad8ac25853d431a2afd Mon Sep 17 00:00:00 2001 From: joshuakudo Date: Sun, 28 Sep 2025 14:25:36 +0800 Subject: [PATCH 2/3] feat: upload image api --- .../domain/adapters/http/EventController.java | 2 +- .../adapters/jdbi/mappers/EventReducer.java | 12 +- .../event/domain/ports/EventService.java | 3 +- .../storage/LocalObjectStore.java | 103 +++++++++++++++++- .../storage/StorageService.java | 89 +-------------- .../storage/base/ObjectStore.java | 6 +- .../storage/base/StorageDTO.java | 7 ++ .../controller/DevStorageController.java | 22 ++-- .../storage/controller/StorageController.java | 21 ++-- 9 files changed, 152 insertions(+), 113 deletions(-) create mode 100644 src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java diff --git a/src/main/java/com/markguiang/backend/event/domain/adapters/http/EventController.java b/src/main/java/com/markguiang/backend/event/domain/adapters/http/EventController.java index 7560c0e..9a31ab9 100644 --- a/src/main/java/com/markguiang/backend/event/domain/adapters/http/EventController.java +++ b/src/main/java/com/markguiang/backend/event/domain/adapters/http/EventController.java @@ -95,7 +95,7 @@ public void updateDayDetails( @PreAuthorize("hasAuthority(T(com.markguiang.backend.role.domain.Role.Authority).WRITE.name())") @PutMapping("/image-url/{eventId}") - public void updateImageUrl(@PathVariable UUID eventId, URI imageUrl) throws IOException { + public void updateImageUrl(@PathVariable UUID eventId, @RequestParam("path") URI imageUrl) throws IOException { eventService.updateEventImage(eventId, imageUrl); } } diff --git a/src/main/java/com/markguiang/backend/event/domain/adapters/jdbi/mappers/EventReducer.java b/src/main/java/com/markguiang/backend/event/domain/adapters/jdbi/mappers/EventReducer.java index 37c2741..68c960b 100644 --- a/src/main/java/com/markguiang/backend/event/domain/adapters/jdbi/mappers/EventReducer.java +++ b/src/main/java/com/markguiang/backend/event/domain/adapters/jdbi/mappers/EventReducer.java @@ -18,8 +18,16 @@ public class EventReducer implements LinkedHashMapRowReducer { @Override public void accumulate(Map map, RowView rowView) { UUID eventId = rowView.getColumn("event_id", UUID.class); + String statusStr = rowView.getColumn("status", String.class); + EventStatus status; - Event event = + if (statusStr != null) { + status = EventStatus.valueOf(statusStr); + } else { + status = null; + } + + Event event = map.computeIfAbsent( eventId, id -> @@ -30,7 +38,7 @@ public void accumulate(Map map, RowView rowView) { rowView.getColumn("description", String.class), rowView.getColumn("location", String.class), rowView.getColumn("img_url", URI.class), - EventStatus.valueOf(rowView.getColumn("status", String.class)), + status, new ArrayList<>())); UUID dayId = rowView.getColumn("day_id", UUID.class); diff --git a/src/main/java/com/markguiang/backend/event/domain/ports/EventService.java b/src/main/java/com/markguiang/backend/event/domain/ports/EventService.java index b335454..ccae912 100644 --- a/src/main/java/com/markguiang/backend/event/domain/ports/EventService.java +++ b/src/main/java/com/markguiang/backend/event/domain/ports/EventService.java @@ -21,7 +21,8 @@ public EventService(EventRepository er) { } public Event getEventOrThrow(UUID eventID) { - return er.findByID(eventID).orElseThrow(() -> new EventDoesNotExistException(eventID)); + return er.findByID(eventID).orElseThrow(() -> + new EventDoesNotExistException(eventID)); } public Event getEvent(UUID eventID) { diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java b/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java index bd809e7..8334019 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java @@ -1,20 +1,62 @@ package com.markguiang.backend.infrastructure.storage; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.storage.*; import com.markguiang.backend.infrastructure.storage.base.DirectObjectStore; +import com.markguiang.backend.infrastructure.storage.base.StorageDTO; +import com.markguiang.backend.infrastructure.storage.base.StoreProperties; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static com.markguiang.backend.event.utils.Utils.concatenateStr; +import static org.springframework.data.jpa.domain.AbstractPersistable_.id; @Component public class LocalObjectStore implements DirectObjectStore { private final Path storagePath = Paths.get("store").toAbsolutePath().normalize(); + @Value("${GCP_PROJECTID}") + private String projectId; + + @Value("${GCP_BUCKET_PUBLIC}") + private String publicBucket; + + @Value("${GCP_BUCKET_PRIVATE}") + private String privateBucket; + + @Value("${GOOGLE_CLOUD_STORAGE_JSON}") + private String sa; + + private static final String SUFFIX = "/"; + private static final String BUCKET_PATH_EVENT = "event/"; + + private static final String BUCKET_PATH_USER = "users/"; + + private StoreProperties initProperties(Boolean isPublic) { + return StoreProperties + .builder() + .projectId(projectId) + .bucket(isPublic ? publicBucket : privateBucket) + .sa(sa) + .build(); + } + public LocalObjectStore() throws IOException { Files.createDirectories(storagePath); } @@ -32,11 +74,62 @@ public InputStream fetch(URI presignedUrl) throws IOException { return Files.newInputStream(source); } - public URI generatePresignedUrlForDownload(String key) { - return URI.create(key); + public URL generatePresignedUrlForDownload(String key) throws IOException { + return generateSignedUrl(key); } - public URI generatePresignedUrlForUpload(String key) { - return URI.create(key); - } + public StorageDTO generatePresignedUrlForUpload( UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException { + String filename = RandomStringUtils.randomAlphabetic(20) + "." + fileExtension; + + String filePath = concatenateStr(BUCKET_PATH_EVENT, id.toString(), SUFFIX, filename); + + URL signedUrl = generateV4PutObjectSignedUrl(initProperties(isPublic), filePath, fileType); + + return new StorageDTO(filePath, signedUrl); + } + + + private URL generateV4PutObjectSignedUrl(StoreProperties properties, String path, String contentType) throws IOException { + + Storage storage = StorageOptions.newBuilder().setProjectId(properties.getProjectId()).build().getService(); + + BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(properties.getBucket(), path)).build(); + + Map extensionHeaders = new HashMap<>(); + extensionHeaders.put("Content-Type", contentType); + + return storage.signUrl( + blobInfo, + 15, + TimeUnit.MINUTES, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withExtHeaders(extensionHeaders), + Storage.SignUrlOption.withV4Signature(), + Storage.SignUrlOption.signWith( + ServiceAccountCredentials + .fromStream(new ByteArrayInputStream(properties.getSa().getBytes(StandardCharsets.UTF_8))) + ) + ); + } + + public URL generateSignedUrl(String objectPath) throws IOException { + Storage storage = StorageOptions.newBuilder() + .setProjectId(projectId) + .setCredentials( + ServiceAccountCredentials.fromStream( + new ByteArrayInputStream(sa.getBytes(StandardCharsets.UTF_8)) // since sa is a JSON string + ) + ) + .build() + .getService(); + + BlobInfo blobInfo = BlobInfo.newBuilder(privateBucket, objectPath).build(); + + return storage.signUrl( + blobInfo, + 15, + TimeUnit.MINUTES, + Storage.SignUrlOption.withV4Signature() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java index f1dc742..ea2e21f 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java @@ -18,6 +18,7 @@ import com.google.cloud.WriteChannel; import com.google.cloud.storage.*; +import com.markguiang.backend.infrastructure.storage.base.StorageDTO; import com.markguiang.backend.infrastructure.storage.base.StoreProperties; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; @@ -30,32 +31,6 @@ public class StorageService { private final ObjectStore os; - @Value("${GCP_PROJECTID}") - private String projectId; - - @Value("${GCP_BUCKET_PUBLIC}") - private String publicBucket; - - @Value("${GCP_BUCKET_PRIVATE}") - private String privateBucket; - - @Value("${GOOGLE_CLOUD_STORAGE_JSON}") - private String sa; - - private static final String SUFFIX = "/"; - private static final String BUCKET_PATH_EVENT = "event/"; - - private static final String BUCKET_PATH_USER = "users/"; - - private StoreProperties initProperties(Boolean isPublic) { - return StoreProperties - .builder() - .projectId(projectId) - .bucket(isPublic ? publicBucket : privateBucket) - .sa(sa) - .build(); - } - public StorageService(ObjectStore os) { this.os = os; } @@ -78,68 +53,12 @@ public InputStream fetch(URI presignedUrl) throws IOException { throw new UnsupportedOperationException("direct-storage-not-supported-by-this-implementation"); } - public URL generatePresignedUrlForUpload(String key) { - return os.generatePresignedUrlForUpload(key); + public StorageDTO generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, Boolean isPublic) throws IOException { + return os.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); } - public URI generatePresignedUrlForDownload(String key) { + public URL generatePresignedUrlForDownload(String key) throws IOException { return os.generatePresignedUrlForDownload(key); } - public URL generateV4PutObjectSignedUrl(StoreProperties properties, String path, String contentType) throws IOException { - - Storage storage = StorageOptions.newBuilder().setProjectId(properties.getProjectId()).build().getService(); - - BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(properties.getBucket(), path)).build(); - - Map extensionHeaders = new HashMap<>(); - extensionHeaders.put("Content-Type", contentType); - - return storage.signUrl( - blobInfo, - 15, - TimeUnit.MINUTES, - Storage.SignUrlOption.httpMethod(HttpMethod.PUT), - Storage.SignUrlOption.withExtHeaders(extensionHeaders), - Storage.SignUrlOption.withV4Signature(), - Storage.SignUrlOption.signWith( - ServiceAccountCredentials - .fromStream(new ByteArrayInputStream(properties.getSa().getBytes(StandardCharsets.UTF_8))) - ) - ); - } - - public Map generatePresignedUrl(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException { - String filePath; - String filename = RandomStringUtils.randomAlphabetic(20) + "." + fileExtension; - - filePath = concatenateStr( BUCKET_PATH_EVENT, id.toString(), SUFFIX, filename); - - HashMap map = new HashMap<>(); - - map.put(filePath, generateV4PutObjectSignedUrl(initProperties(isPublic), filePath, fileType)); - - return map; - } - - public URL generateSignedUrl(String objectPath) throws IOException { - Storage storage = StorageOptions.newBuilder() - .setProjectId(projectId) - .setCredentials( - ServiceAccountCredentials.fromStream( - new ByteArrayInputStream(sa.getBytes(StandardCharsets.UTF_8)) // since sa is a JSON string - ) - ) - .build() - .getService(); - - BlobInfo blobInfo = BlobInfo.newBuilder(privateBucket, objectPath).build(); - - return storage.signUrl( - blobInfo, - 15, - TimeUnit.MINUTES, - Storage.SignUrlOption.withV4Signature() - ); - } } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java index 67a0f40..758bcfc 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java @@ -1,10 +1,12 @@ package com.markguiang.backend.infrastructure.storage.base; +import java.io.IOException; import java.net.URI; import java.net.URL; +import java.util.UUID; public interface ObjectStore { - public URI generatePresignedUrlForDownload(String key); + public URL generatePresignedUrlForDownload(String key) throws IOException; - public URL generatePresignedUrlForUpload(String key); + public StorageDTO generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException; } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java new file mode 100644 index 0000000..631e527 --- /dev/null +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java @@ -0,0 +1,7 @@ +package com.markguiang.backend.infrastructure.storage.base; + +import java.net.URL; + +public record StorageDTO(String filePath, URL url) { + +} diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java index 5bf07e3..2689a04 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java @@ -3,8 +3,10 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URL; import com.markguiang.backend.infrastructure.storage.StorageService; +import com.markguiang.backend.infrastructure.storage.base.StorageDTO; import org.hibernate.validator.constraints.UUID; import org.springframework.context.annotation.Profile; import org.springframework.core.io.InputStreamResource; @@ -12,13 +14,10 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +@RestController @RequestMapping("/storage") @Profile("dev") public class DevStorageController { @@ -28,14 +27,17 @@ public DevStorageController(StorageService ss) { this.ss = ss; } - @PreAuthorize("hasAuthority('permission:write')") - @GetMapping("/presigned-url/{eventId}") - public URI generatePresignedUrlForUpload(@PathVariable UUID eventId) { - return ss.generatePresignedUrlForUpload(eventId.toString()); + @PreAuthorize("permitAll()") + @PutMapping("/presigned-url/upload/{key}") + public StorageDTO generatePresignedUrlForUpload(@PathVariable("key") java.util.UUID id, + @RequestParam("fileType") String fileType, + @RequestParam("fileExtension") String fileExtension, + @RequestParam(name = "public", required = false, defaultValue = "true") Boolean isPublic) throws IOException { + return ss.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); } @GetMapping("/presigned-url/{eventId}") - public URI generatePresignedUrlForDownload(@PathVariable UUID eventId) { + public URL generatePresignedUrlForDownload(@PathVariable UUID eventId) throws IOException { return ss.generatePresignedUrlForDownload(eventId.toString()); } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java index 9844456..4328a24 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java @@ -1,14 +1,18 @@ package com.markguiang.backend.infrastructure.storage.controller; import com.markguiang.backend.infrastructure.storage.StorageService; + +import java.io.IOException; import java.net.URI; +import java.net.URL; import java.util.UUID; + +import com.markguiang.backend.infrastructure.storage.base.StorageDTO; import org.springframework.context.annotation.Profile; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; +@RestController @RequestMapping("/storage") @PreAuthorize("hasAuthority(T(com.markguiang.backend.role.domain.Role.Authority).WRITE.name())") @Profile("!dev") @@ -19,13 +23,16 @@ public StorageController(StorageService ss) { this.ss = ss; } - @GetMapping("/presigned-url/upload/{key}") - public URI generatePresignedUrlForUpload(@PathVariable UUID key) { - return ss.generatePresignedUrlForUpload(key.toString()); + @PutMapping("/presigned-url/upload/{key}") + public StorageDTO generatePresignedUrlForUpload(@PathVariable("key") java.util.UUID id, + @RequestParam("fileType") String fileType, + @RequestParam("fileExtension") String fileExtension, + @RequestParam(name = "public", required = false, defaultValue = "true") Boolean isPublic) throws IOException { + return ss.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); } @GetMapping("/presigned-url/dowload/{key}") - public URI generatePresignedUrlForDownload(@PathVariable UUID key) { + public URL generatePresignedUrlForDownload(@PathVariable UUID key) throws IOException { return ss.generatePresignedUrlForDownload(key.toString()); } } From bf8eeaa2a708f8b7190980988f4c52dd5b30ffb9 Mon Sep 17 00:00:00 2001 From: joshuakudo Date: Sun, 28 Sep 2025 17:44:03 +0800 Subject: [PATCH 3/3] refactor: resolved MR comments --- .../storage/GCPObjectStore.java | 111 ++++++++++++++++++ .../storage/LocalObjectStore.java | 104 ++-------------- .../infrastructure/storage/StorageConfig.java | 4 +- .../storage/StorageService.java | 26 ++-- .../storage/base/ObjectStore.java | 3 +- .../{StorageDTO.java => StorageDetails.java} | 3 +- .../controller/DevStorageController.java | 10 +- .../storage/controller/StorageController.java | 11 +- 8 files changed, 141 insertions(+), 131 deletions(-) create mode 100644 src/main/java/com/markguiang/backend/infrastructure/storage/GCPObjectStore.java rename src/main/java/com/markguiang/backend/infrastructure/storage/base/{StorageDTO.java => StorageDetails.java} (59%) diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/GCPObjectStore.java b/src/main/java/com/markguiang/backend/infrastructure/storage/GCPObjectStore.java new file mode 100644 index 0000000..3c92de0 --- /dev/null +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/GCPObjectStore.java @@ -0,0 +1,111 @@ +package com.markguiang.backend.infrastructure.storage; + +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.storage.*; +import com.markguiang.backend.infrastructure.storage.base.ObjectStore; +import com.markguiang.backend.infrastructure.storage.base.StorageDetails; +import com.markguiang.backend.infrastructure.storage.base.StoreProperties; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static com.markguiang.backend.event.utils.Utils.concatenateStr; + +@Component +@Primary +public class GCPObjectStore implements ObjectStore { + @Value("${GCP_PROJECTID}") + private String projectId; + + @Value("${GCP_BUCKET_PUBLIC}") + private String publicBucket; + + @Value("${GCP_BUCKET_PRIVATE}") + private String privateBucket; + + @Value("${GOOGLE_CLOUD_STORAGE_JSON}") + private String sa; + + private static final String SUFFIX = "/"; + private static final String BUCKET_PATH_EVENT = "event/"; + + private static final String BUCKET_PATH_USER = "users/"; + + private StoreProperties initProperties(Boolean isPublic) { + return StoreProperties + .builder() + .projectId(projectId) + .bucket(isPublic ? publicBucket : privateBucket) + .sa(sa) + .build(); + } + + public URL generatePresignedUrlForDownload(String key) throws IOException { + return generateSignedUrl(key); + } + + public StorageDetails generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException { + String filename = RandomStringUtils.randomAlphabetic(20) + "." + fileExtension; + + String filePath = concatenateStr(BUCKET_PATH_EVENT, id.toString(), SUFFIX, filename); + + URL signedUrl = generateV4PutObjectSignedUrl(initProperties(isPublic), filePath, fileType); + + return new StorageDetails(filePath, signedUrl); + } + + + private URL generateV4PutObjectSignedUrl(StoreProperties properties, String path, String contentType) throws IOException { + + Storage storage = StorageOptions.newBuilder().setProjectId(properties.getProjectId()).build().getService(); + + BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(properties.getBucket(), path)).build(); + + Map extensionHeaders = new HashMap<>(); + extensionHeaders.put("Content-Type", contentType); + + return storage.signUrl( + blobInfo, + 15, + TimeUnit.MINUTES, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withExtHeaders(extensionHeaders), + Storage.SignUrlOption.withV4Signature(), + Storage.SignUrlOption.signWith( + ServiceAccountCredentials + .fromStream(new ByteArrayInputStream(properties.getSa().getBytes(StandardCharsets.UTF_8))) + ) + ); + } + + public URL generateSignedUrl(String objectPath) throws IOException { + Storage storage = StorageOptions.newBuilder() + .setProjectId(projectId) + .setCredentials( + ServiceAccountCredentials.fromStream( + new ByteArrayInputStream(sa.getBytes(StandardCharsets.UTF_8)) // since sa is a JSON string + ) + ) + .build() + .getService(); + + BlobInfo blobInfo = BlobInfo.newBuilder(privateBucket, objectPath).build(); + + return storage.signUrl( + blobInfo, + 15, + TimeUnit.MINUTES, + Storage.SignUrlOption.withV4Signature() + ); + } +} diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java b/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java index 8334019..1bbefe5 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/LocalObjectStore.java @@ -1,62 +1,24 @@ package com.markguiang.backend.infrastructure.storage; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.storage.*; import com.markguiang.backend.infrastructure.storage.base.DirectObjectStore; -import com.markguiang.backend.infrastructure.storage.base.StorageDTO; -import com.markguiang.backend.infrastructure.storage.base.StoreProperties; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.beans.factory.annotation.Value; +import com.markguiang.backend.infrastructure.storage.base.StorageDetails; import org.springframework.stereotype.Component; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import static com.markguiang.backend.event.utils.Utils.concatenateStr; -import static org.springframework.data.jpa.domain.AbstractPersistable_.id; @Component public class LocalObjectStore implements DirectObjectStore { private final Path storagePath = Paths.get("store").toAbsolutePath().normalize(); - @Value("${GCP_PROJECTID}") - private String projectId; - - @Value("${GCP_BUCKET_PUBLIC}") - private String publicBucket; - - @Value("${GCP_BUCKET_PRIVATE}") - private String privateBucket; - - @Value("${GOOGLE_CLOUD_STORAGE_JSON}") - private String sa; - - private static final String SUFFIX = "/"; - private static final String BUCKET_PATH_EVENT = "event/"; - - private static final String BUCKET_PATH_USER = "users/"; - - private StoreProperties initProperties(Boolean isPublic) { - return StoreProperties - .builder() - .projectId(projectId) - .bucket(isPublic ? publicBucket : privateBucket) - .sa(sa) - .build(); - } - public LocalObjectStore() throws IOException { Files.createDirectories(storagePath); } @@ -74,62 +36,14 @@ public InputStream fetch(URI presignedUrl) throws IOException { return Files.newInputStream(source); } - public URL generatePresignedUrlForDownload(String key) throws IOException { - return generateSignedUrl(key); + public URL generatePresignedUrlForDownload(String key) throws MalformedURLException { + return URI.create(key).toURL(); } - public StorageDTO generatePresignedUrlForUpload( UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException { - String filename = RandomStringUtils.randomAlphabetic(20) + "." + fileExtension; - - String filePath = concatenateStr(BUCKET_PATH_EVENT, id.toString(), SUFFIX, filename); - - URL signedUrl = generateV4PutObjectSignedUrl(initProperties(isPublic), filePath, fileType); + public StorageDetails generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException { + String key = "/event/" + id + "/upload." + fileExtension; + URL presignedUrl = URI.create(key).toURL(); - return new StorageDTO(filePath, signedUrl); - } - - - private URL generateV4PutObjectSignedUrl(StoreProperties properties, String path, String contentType) throws IOException { - - Storage storage = StorageOptions.newBuilder().setProjectId(properties.getProjectId()).build().getService(); - - BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(properties.getBucket(), path)).build(); - - Map extensionHeaders = new HashMap<>(); - extensionHeaders.put("Content-Type", contentType); - - return storage.signUrl( - blobInfo, - 15, - TimeUnit.MINUTES, - Storage.SignUrlOption.httpMethod(HttpMethod.PUT), - Storage.SignUrlOption.withExtHeaders(extensionHeaders), - Storage.SignUrlOption.withV4Signature(), - Storage.SignUrlOption.signWith( - ServiceAccountCredentials - .fromStream(new ByteArrayInputStream(properties.getSa().getBytes(StandardCharsets.UTF_8))) - ) - ); - } - - public URL generateSignedUrl(String objectPath) throws IOException { - Storage storage = StorageOptions.newBuilder() - .setProjectId(projectId) - .setCredentials( - ServiceAccountCredentials.fromStream( - new ByteArrayInputStream(sa.getBytes(StandardCharsets.UTF_8)) // since sa is a JSON string - ) - ) - .build() - .getService(); - - BlobInfo blobInfo = BlobInfo.newBuilder(privateBucket, objectPath).build(); - - return storage.signUrl( - blobInfo, - 15, - TimeUnit.MINUTES, - Storage.SignUrlOption.withV4Signature() - ); - } + return new StorageDetails(key, presignedUrl); + } } \ No newline at end of file diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageConfig.java b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageConfig.java index 4b866f6..b2af4cb 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageConfig.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageConfig.java @@ -8,7 +8,7 @@ public class StorageConfig { @Bean - public StorageService storageService(ObjectStore objectStore) { - return new StorageService(objectStore); + public StorageService storageService(ObjectStore objectStore, GCPObjectStore gcpObjectStore) { + return new StorageService(objectStore, gcpObjectStore); } } \ No newline at end of file diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java index ea2e21f..fcab56f 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java @@ -3,36 +3,24 @@ import com.markguiang.backend.infrastructure.storage.base.DirectObjectStore; import com.markguiang.backend.infrastructure.storage.base.ObjectStore; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; -import java.util.concurrent.TimeUnit; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.WriteChannel; -import com.google.cloud.storage.*; - -import com.markguiang.backend.infrastructure.storage.base.StorageDTO; -import com.markguiang.backend.infrastructure.storage.base.StoreProperties; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.beans.factory.annotation.Value; +import com.markguiang.backend.infrastructure.storage.base.StorageDetails; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import static com.markguiang.backend.event.utils.Utils.concatenateStr; - @Service public class StorageService { private final ObjectStore os; + private final GCPObjectStore gcpObjectStore; - public StorageService(ObjectStore os) { + public StorageService(ObjectStore os, GCPObjectStore gcpObjectStore) { this.os = os; + this.gcpObjectStore = gcpObjectStore; } public URI store(InputStream is, URI presignedUrl) throws IOException { @@ -53,12 +41,12 @@ public InputStream fetch(URI presignedUrl) throws IOException { throw new UnsupportedOperationException("direct-storage-not-supported-by-this-implementation"); } - public StorageDTO generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, Boolean isPublic) throws IOException { - return os.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); + public StorageDetails generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, Boolean isPublic) throws IOException { + return gcpObjectStore.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); } public URL generatePresignedUrlForDownload(String key) throws IOException { - return os.generatePresignedUrlForDownload(key); + return gcpObjectStore.generatePresignedUrlForDownload(key); } } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java index 758bcfc..23fda21 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/ObjectStore.java @@ -1,12 +1,11 @@ package com.markguiang.backend.infrastructure.storage.base; import java.io.IOException; -import java.net.URI; import java.net.URL; import java.util.UUID; public interface ObjectStore { public URL generatePresignedUrlForDownload(String key) throws IOException; - public StorageDTO generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException; + public StorageDetails generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, boolean isPublic) throws IOException; } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDetails.java similarity index 59% rename from src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java rename to src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDetails.java index 631e527..1b18092 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDTO.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDetails.java @@ -2,6 +2,5 @@ import java.net.URL; -public record StorageDTO(String filePath, URL url) { - +public record StorageDetails(String filePath, URL url) { } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java index 2689a04..2981d0f 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/DevStorageController.java @@ -6,7 +6,7 @@ import java.net.URL; import com.markguiang.backend.infrastructure.storage.StorageService; -import com.markguiang.backend.infrastructure.storage.base.StorageDTO; +import com.markguiang.backend.infrastructure.storage.base.StorageDetails; import org.hibernate.validator.constraints.UUID; import org.springframework.context.annotation.Profile; import org.springframework.core.io.InputStreamResource; @@ -29,10 +29,10 @@ public DevStorageController(StorageService ss) { @PreAuthorize("permitAll()") @PutMapping("/presigned-url/upload/{key}") - public StorageDTO generatePresignedUrlForUpload(@PathVariable("key") java.util.UUID id, - @RequestParam("fileType") String fileType, - @RequestParam("fileExtension") String fileExtension, - @RequestParam(name = "public", required = false, defaultValue = "true") Boolean isPublic) throws IOException { + public StorageDetails generatePresignedUrlForUpload(@PathVariable("key") java.util.UUID id, + @RequestParam("fileType") String fileType, + @RequestParam("fileExtension") String fileExtension, + @RequestParam(name = "public", required = false, defaultValue = "true") Boolean isPublic) throws IOException { return ss.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); } diff --git a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java index 4328a24..2af81fd 100644 --- a/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java +++ b/src/main/java/com/markguiang/backend/infrastructure/storage/controller/StorageController.java @@ -3,11 +3,10 @@ import com.markguiang.backend.infrastructure.storage.StorageService; import java.io.IOException; -import java.net.URI; import java.net.URL; import java.util.UUID; -import com.markguiang.backend.infrastructure.storage.base.StorageDTO; +import com.markguiang.backend.infrastructure.storage.base.StorageDetails; import org.springframework.context.annotation.Profile; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -24,10 +23,10 @@ public StorageController(StorageService ss) { } @PutMapping("/presigned-url/upload/{key}") - public StorageDTO generatePresignedUrlForUpload(@PathVariable("key") java.util.UUID id, - @RequestParam("fileType") String fileType, - @RequestParam("fileExtension") String fileExtension, - @RequestParam(name = "public", required = false, defaultValue = "true") Boolean isPublic) throws IOException { + public StorageDetails generatePresignedUrlForUpload(@PathVariable("key") java.util.UUID id, + @RequestParam("fileType") String fileType, + @RequestParam("fileExtension") String fileExtension, + @RequestParam(name = "public", required = false, defaultValue = "true") Boolean isPublic) throws IOException { return ss.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic); }