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/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/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/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/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/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 bd809e7..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,15 +1,19 @@
package com.markguiang.backend.infrastructure.storage;
import com.markguiang.backend.infrastructure.storage.base.DirectObjectStore;
+import com.markguiang.backend.infrastructure.storage.base.StorageDetails;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
+import java.net.MalformedURLException;
import java.net.URI;
+import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+import java.util.UUID;
@Component
public class LocalObjectStore implements DirectObjectStore {
@@ -32,11 +36,14 @@ 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 MalformedURLException {
+ return URI.create(key).toURL();
}
- public URI generatePresignedUrlForUpload(String key) {
- return URI.create(key);
+ 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 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 99426da..fcab56f 100644
--- a/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java
+++ b/src/main/java/com/markguiang/backend/infrastructure/storage/StorageService.java
@@ -2,18 +2,25 @@
import com.markguiang.backend.infrastructure.storage.base.DirectObjectStore;
import com.markguiang.backend.infrastructure.storage.base.ObjectStore;
+
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.net.URL;
+import java.util.UUID;
+
+import com.markguiang.backend.infrastructure.storage.base.StorageDetails;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@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 {
@@ -34,11 +41,12 @@ public InputStream fetch(URI presignedUrl) throws IOException {
throw new UnsupportedOperationException("direct-storage-not-supported-by-this-implementation");
}
- public URI generatePresignedUrlForUpload(String key) {
- return os.generatePresignedUrlForUpload(key);
+ public StorageDetails generatePresignedUrlForUpload(UUID id, String fileType, String fileExtension, Boolean isPublic) throws IOException {
+ return gcpObjectStore.generatePresignedUrlForUpload(id, fileType, fileExtension, isPublic);
}
- public URI generatePresignedUrlForDownload(String key) {
- return os.generatePresignedUrlForDownload(key);
+ public URL generatePresignedUrlForDownload(String key) throws IOException {
+ 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 87388db..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,9 +1,11 @@
package com.markguiang.backend.infrastructure.storage.base;
-import java.net.URI;
+import java.io.IOException;
+import java.net.URL;
+import java.util.UUID;
public interface ObjectStore {
- public URI generatePresignedUrlForDownload(String key);
+ public URL generatePresignedUrlForDownload(String key) throws IOException;
- public URI generatePresignedUrlForUpload(String key);
+ 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/StorageDetails.java b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDetails.java
new file mode 100644
index 0000000..1b18092
--- /dev/null
+++ b/src/main/java/com/markguiang/backend/infrastructure/storage/base/StorageDetails.java
@@ -0,0 +1,6 @@
+package com.markguiang.backend.infrastructure.storage.base;
+
+import java.net.URL;
+
+public record StorageDetails(String filePath, URL url) {
+}
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;
+ }
+ }
+}
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..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
@@ -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.StorageDetails;
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 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);
}
@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..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
@@ -1,14 +1,17 @@
package com.markguiang.backend.infrastructure.storage.controller;
import com.markguiang.backend.infrastructure.storage.StorageService;
-import java.net.URI;
+
+import java.io.IOException;
+import java.net.URL;
import java.util.UUID;
+
+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.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 +22,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 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);
}
@GetMapping("/presigned-url/dowload/{key}")
- public URI generatePresignedUrlForDownload(@PathVariable UUID key) {
+ public URL generatePresignedUrlForDownload(@PathVariable UUID key) throws IOException {
return ss.generatePresignedUrlForDownload(key.toString());
}
}