diff --git a/build.gradle b/build.gradle
index 7a24ce8..8015e02 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,10 +61,15 @@ dependencies {
implementation 'org.commonmark:commonmark:0.22.0'
implementation 'org.jsoup:jsoup:1.17.2'
+ implementation 'io.minio:minio:8.5.11'
+
compileOnly 'org.jetbrains:annotations:24.1.0'
// Custom testing
testRuntimeOnly 'com.h2database:h2'
+ testImplementation 'org.testcontainers:testcontainers:1.20.0'
+ testImplementation 'org.testcontainers:junit-jupiter:1.20.0'
+ testImplementation "org.testcontainers:minio:1.20.0"
testFixturesImplementation 'org.hamcrest:hamcrest:2.2'
}
diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/image/S3ImageServiceTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/image/S3ImageServiceTests.java
new file mode 100644
index 0000000..69feb5e
--- /dev/null
+++ b/src/integrationTest/java/app/mealsmadeeasy/api/image/S3ImageServiceTests.java
@@ -0,0 +1,88 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.user.User;
+import app.mealsmadeeasy.api.user.UserCreateException;
+import app.mealsmadeeasy.api.user.UserService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.MinIOContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static app.mealsmadeeasy.api.matchers.Matchers.isUser;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.notNullValue;
+
+@Testcontainers
+@SpringBootTest
+public class S3ImageServiceTests {
+
+ @Container
+ private static final MinIOContainer container = new MinIOContainer(
+ DockerImageName.parse("minio/minio:latest")
+ );
+
+ @DynamicPropertySource
+ public static void minioProperties(DynamicPropertyRegistry registry) {
+ registry.add("app.mealsmadeeasy.api.minio.bucketName", () -> "test-bucket");
+ registry.add("app.mealsmadeeasy.api.minio.endpoint", container::getS3URL);
+ registry.add("app.mealsmadeeasy.api.minio.accessKey", container::getUserName);
+ registry.add("app.mealsmadeeasy.api.minio.secretKey", container::getPassword);
+ }
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private ImageService imageService;
+
+ private User createTestUser(String username) {
+ try {
+ return this.userService.createUser(username, username + "@test.com", "test");
+ } catch (UserCreateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void smokeScreen() {}
+
+ @Test
+ @DirtiesContext
+ public void simpleCreate() {
+ try (final InputStream hal9000 = S3ImageServiceTests.class.getResourceAsStream("HAL9000.svg")) {
+ final User owner = this.createTestUser("imageOwner");
+ final Image image = this.imageService.create(
+ owner,
+ "HAL9000.svg",
+ hal9000,
+ "image/svg+xml",
+ 27881L
+ );
+ assertThat(image.getOwner(), isUser(owner));
+ assertThat(image.getCreated(), is(notNullValue()));
+ assertThat(image.getModified(), is(nullValue()));
+ assertThat(image.getUserFilename(), is("HAL9000.svg"));
+ assertThat(image.getMimeType(), is("image/svg+xml"));
+ assertThat(image.getAlt(), is(nullValue()));
+ assertThat(image.getCaption(), is(nullValue()));
+ assertThat(image.getInternalUrl(), is(notNullValue()));
+ assertThat(image.isPublic(), is(false));
+ assertThat(image.getViewers(), is(empty()));
+ } catch (IOException | ImageException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/s3/MinioS3ManagerTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/s3/MinioS3ManagerTests.java
new file mode 100644
index 0000000..c89f695
--- /dev/null
+++ b/src/integrationTest/java/app/mealsmadeeasy/api/s3/MinioS3ManagerTests.java
@@ -0,0 +1,68 @@
+package app.mealsmadeeasy.api.s3;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MinIOContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@Testcontainers
+public class MinioS3ManagerTests {
+
+ @Container
+ private static final MinIOContainer container = new MinIOContainer(
+ DockerImageName.parse("minio/minio:latest")
+ );
+
+ private MinioS3Manager s3Manager;
+
+ @BeforeEach
+ public void beforeEach() {
+ this.s3Manager = new MinioS3Manager();
+ this.s3Manager.setEndpoint(container.getS3URL());
+ this.s3Manager.setAccessKey(container.getUserName());
+ this.s3Manager.setSecretKey(container.getPassword());
+ }
+
+ @Test
+ public void smokeScreen() {}
+
+ @Test
+ public void simpleStore() {
+ try (final InputStream hal9000 = MinioS3ManagerTests.class.getResourceAsStream("HAL9000.svg")) {
+ final String result = this.s3Manager.store(
+ "test-images",
+ "HAL9000.svg",
+ "image/svg+xml",
+ hal9000,
+ 27881L
+ );
+ assertEquals("HAL9000.svg", result);
+ } catch (IOException ioException) {
+ throw new RuntimeException(ioException);
+ }
+ }
+
+ @Test
+ public void simpleDelete() {
+ try (final InputStream hal9000 = MinioS3ManagerTests.class.getResourceAsStream("HAL9000.svg")) {
+ final String objectName = this.s3Manager.store(
+ "test-images",
+ "HAL9000.svg",
+ "image/svg+xml",
+ hal9000,
+ 27881L
+ );
+ this.s3Manager.delete("test-images", objectName);
+ } catch (IOException ioException) {
+ throw new RuntimeException(ioException);
+ }
+ }
+
+}
diff --git a/src/integrationTest/resources/app/mealsmadeeasy/api/image/HAL9000.svg b/src/integrationTest/resources/app/mealsmadeeasy/api/image/HAL9000.svg
new file mode 100644
index 0000000..fcd4f45
--- /dev/null
+++ b/src/integrationTest/resources/app/mealsmadeeasy/api/image/HAL9000.svg
@@ -0,0 +1,741 @@
+
+
+
+
diff --git a/src/integrationTest/resources/app/mealsmadeeasy/api/s3/HAL9000.svg b/src/integrationTest/resources/app/mealsmadeeasy/api/s3/HAL9000.svg
new file mode 100644
index 0000000..fcd4f45
--- /dev/null
+++ b/src/integrationTest/resources/app/mealsmadeeasy/api/s3/HAL9000.svg
@@ -0,0 +1,741 @@
+
+
+
+
diff --git a/src/integrationTest/resources/application.properties b/src/integrationTest/resources/application.properties
index 1a0c6f4..db534b8 100644
--- a/src/integrationTest/resources/application.properties
+++ b/src/integrationTest/resources/application.properties
@@ -4,3 +4,6 @@ spring.datasource.username=sa
spring.datasource.password=sa
app.mealsmadeeasy.api.security.access-token-lifetime=60
app.mealsmadeeasy.api.security.refresh-token-lifetime=120
+app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000
+app.mealsmadeeasy.api.minio.accessKey=minio-root
+app.mealsmadeeasy.api.minio.secretKey=test0123
diff --git a/src/main/java/app/mealsmadeeasy/api/image/Image.java b/src/main/java/app/mealsmadeeasy/api/image/Image.java
new file mode 100644
index 0000000..ccc7c64
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/Image.java
@@ -0,0 +1,23 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.user.User;
+import app.mealsmadeeasy.api.user.UserEntity;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+public interface Image {
+ Long getId();
+ LocalDateTime getCreated();
+ @Nullable LocalDateTime getModified();
+ String getUserFilename();
+ String getMimeType();
+ @Nullable String getAlt();
+ @Nullable String getCaption();
+ String getObjectName();
+ String getInternalUrl();
+ User getOwner();
+ boolean isPublic();
+ Set getViewers();
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageEntity.java b/src/main/java/app/mealsmadeeasy/api/image/ImageEntity.java
new file mode 100644
index 0000000..a852279
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/ImageEntity.java
@@ -0,0 +1,164 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.user.User;
+import app.mealsmadeeasy.api.user.UserEntity;
+import jakarta.persistence.*;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity(name = "Image")
+public class ImageEntity implements Image {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(nullable = false, updatable = false)
+ private Long id;
+
+ @Column(nullable = false)
+ private LocalDateTime created = LocalDateTime.now();
+
+ private LocalDateTime modified;
+
+ @Column(nullable = false)
+ private String userFilename;
+
+ @Column(nullable = false)
+ private String mimeType;
+
+ private String alt;
+
+ private String caption;
+
+ @Column(nullable = false)
+ private String objectName;
+
+ @Column(nullable = false)
+ private String internalUrl;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "owner_id", nullable = false)
+ private UserEntity owner;
+
+ @Column(nullable = false)
+ private Boolean isPublic = false;
+
+ @ManyToMany
+ private Set viewers = new HashSet<>();
+
+ @Override
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ @Override
+ public LocalDateTime getCreated() {
+ return this.created;
+ }
+
+ public void setCreated(LocalDateTime created) {
+ this.created = created;
+ }
+
+ @Override
+ public @Nullable LocalDateTime getModified() {
+ return this.modified;
+ }
+
+ public void setModified(LocalDateTime modified) {
+ this.modified = modified;
+ }
+
+ @Override
+ public String getUserFilename() {
+ return this.userFilename;
+ }
+
+ public void setUserFilename(String userFilename) {
+ this.userFilename = userFilename;
+ }
+
+ @Override
+ public String getMimeType() {
+ return this.mimeType;
+ }
+
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public @Nullable String getAlt() {
+ return this.alt;
+ }
+
+ public void setAlt(String alt) {
+ this.alt = alt;
+ }
+
+ @Override
+ public @Nullable String getCaption() {
+ return this.caption;
+ }
+
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ @Override
+ public String getObjectName() {
+ return this.objectName;
+ }
+
+ public void setObjectName(String objectName) {
+ this.objectName = objectName;
+ }
+
+ @Override
+ public String getInternalUrl() {
+ return this.internalUrl;
+ }
+
+ public void setInternalUrl(String internalUrl) {
+ this.internalUrl = internalUrl;
+ }
+
+ @Override
+ public User getOwner() {
+ return this.owner;
+ }
+
+ public void setOwner(UserEntity owner) {
+ this.owner = owner;
+ }
+
+ @Override
+ public boolean isPublic() {
+ return this.isPublic;
+ }
+
+ public void setPublic(Boolean aPublic) {
+ isPublic = aPublic;
+ }
+
+ @Override
+ public Set getViewers() {
+ return this.viewers;
+ }
+
+ public void setViewers(Set viewers) {
+ this.viewers = viewers;
+ }
+
+ @Override
+ public String toString() {
+ return "ImageEntity(" + this.id + ", " + this.internalUrl + ")";
+ }
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageException.java b/src/main/java/app/mealsmadeeasy/api/image/ImageException.java
new file mode 100644
index 0000000..9a000d2
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/ImageException.java
@@ -0,0 +1,25 @@
+package app.mealsmadeeasy.api.image;
+
+public class ImageException extends Exception {
+
+ public enum Type {
+ INVALID_ID, UNKNOWN_MIME_TYPE
+ }
+
+ private final Type type;
+
+ public ImageException(Type type, String message, Throwable cause) {
+ super(message, cause);
+ this.type = type;
+ }
+
+ public ImageException(Type type, String message) {
+ super(message);
+ this.type = type;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageRepository.java b/src/main/java/app/mealsmadeeasy/api/image/ImageRepository.java
new file mode 100644
index 0000000..6e89877
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/ImageRepository.java
@@ -0,0 +1,13 @@
+package app.mealsmadeeasy.api.image;
+
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+public interface ImageRepository extends JpaRepository {
+
+ @Query("SELECT image FROM Image image WHERE image.id = ?1")
+ @EntityGraph(attributePaths = { "viewers" })
+ ImageEntity getByIdWithViewers(long id);
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java
new file mode 100644
index 0000000..8790a8a
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurity.java
@@ -0,0 +1,9 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.user.User;
+import org.jetbrains.annotations.Nullable;
+
+public interface ImageSecurity {
+ boolean isViewableBy(Image image, @Nullable User viewer);
+ boolean isOwner(Image image, @Nullable User user);
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java
new file mode 100644
index 0000000..3d08d7f
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/ImageSecurityImpl.java
@@ -0,0 +1,47 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.user.User;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+@Component("imageSecurity")
+public class ImageSecurityImpl implements ImageSecurity {
+
+ private final ImageRepository imageRepository;
+
+ public ImageSecurityImpl(ImageRepository imageRepository) {
+ this.imageRepository = imageRepository;
+ }
+
+ @Override
+ public boolean isViewableBy(Image image, @Nullable User viewer) {
+ if (image.isPublic()) {
+ // public image
+ return true;
+ } else if (viewer == null) {
+ // non-public and no principal
+ return false;
+ } else if (Objects.equals(image.getOwner().getId(), viewer.getId())) {
+ // is owner
+ return true;
+ } else {
+ // check if viewer
+ final ImageEntity withViewers = this.imageRepository.getByIdWithViewers(image.getId());
+ for (final User user : withViewers.getViewers()) {
+ if (user.getId() != null && user.getId().equals(viewer.getId())) {
+ return true;
+ }
+ }
+ }
+ // non-public and not viewer
+ return false;
+ }
+
+ @Override
+ public boolean isOwner(Image image, @Nullable User user) {
+ return image.getOwner() != null && user != null && Objects.equals(image.getOwner().getId(), user.getId());
+ }
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageService.java b/src/main/java/app/mealsmadeeasy/api/image/ImageService.java
new file mode 100644
index 0000000..4b49cf3
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/ImageService.java
@@ -0,0 +1,28 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.user.User;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public interface ImageService {
+
+ Image create(User owner, String userFilename, InputStream inputStream, String mimeType, long objectSize)
+ throws IOException, ImageException;
+
+ Image getById(long id);
+ Image getById(long id, User viewer);
+
+ List getImagesOwnedBy(User user);
+
+ Image updateOwner(Image image, User oldOwner, User newOwner);
+
+ Image setAlt(Image image, User owner, String alt);
+ Image setCaption(Image image, User owner, String caption);
+ Image setPublic(Image image, User owner, boolean isPublic);
+
+ void deleteImage(Image image, User owner) throws IOException;
+ void deleteById(long id, User owner) throws IOException;
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java b/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java
new file mode 100644
index 0000000..1136620
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java
@@ -0,0 +1,103 @@
+package app.mealsmadeeasy.api.image;
+
+import app.mealsmadeeasy.api.s3.S3Manager;
+import app.mealsmadeeasy.api.user.User;
+import app.mealsmadeeasy.api.user.UserEntity;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class S3ImageService implements ImageService {
+
+ private final S3Manager s3Manager;
+ private final ImageRepository imageRepository;
+
+ public S3ImageService(S3Manager s3Manager, ImageRepository imageRepository) {
+ this.s3Manager = s3Manager;
+ this.imageRepository = imageRepository;
+ }
+
+ private String getExtension(String mimeType) throws ImageException {
+ return switch (mimeType) {
+ case "image/svg+xml" -> "svg";
+ default -> throw new ImageException(
+ ImageException.Type.UNKNOWN_MIME_TYPE,
+ "unknown mime type: " + mimeType
+ );
+ };
+ }
+
+ @Override
+ public Image create(User owner, String userFilename, InputStream inputStream, String mimeType, long objectSize)
+ throws IOException, ImageException {
+ final String uuid = UUID.randomUUID().toString();
+ final String extension = this.getExtension(mimeType);
+ final String filename = uuid + "." + extension;
+ final String objectName = this.s3Manager.store("images", filename, mimeType, inputStream, objectSize);
+
+ final ImageEntity draft = new ImageEntity();
+ draft.setOwner((UserEntity) owner);
+ draft.setUserFilename(userFilename);
+ draft.setMimeType(mimeType);
+ draft.setObjectName(objectName);
+ draft.setInternalUrl(this.s3Manager.getUrl("images", objectName));
+ return this.imageRepository.save(draft);
+ }
+
+ @Override
+ @PostAuthorize("returnObject.isPublic")
+ public Image getById(long id) {
+ return this.imageRepository.getReferenceById(id);
+ }
+
+ @Override
+ @PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
+ public Image getById(long id, User viewer) {
+ return this.imageRepository.getReferenceById(id);
+ }
+
+ @Override
+ public List getImagesOwnedBy(User user) {
+ return List.of();
+ }
+
+ @Override
+ public Image updateOwner(Image image, User oldOwner, User newOwner) {
+ return null;
+ }
+
+ @Override
+ public Image setAlt(Image image, User owner, String alt) {
+ return null;
+ }
+
+ @Override
+ public Image setCaption(Image image, User owner, String caption) {
+ return null;
+ }
+
+ @Override
+ public Image setPublic(Image image, User owner, boolean isPublic) {
+ return null;
+ }
+
+ @Override
+ @PreAuthorize("@imageSecurity.isOwner(image, owner)")
+ public void deleteImage(Image image, User owner) throws IOException {
+ this.imageRepository.delete((ImageEntity) image);
+ this.s3Manager.delete("images", image.getObjectName()); // TODO
+ }
+
+ @Override
+ public void deleteById(long id, User owner) throws IOException {
+ final ImageEntity toDelete = this.imageRepository.getReferenceById(id);
+ this.deleteImage(toDelete, owner);
+ }
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/s3/MinioS3Manager.java b/src/main/java/app/mealsmadeeasy/api/s3/MinioS3Manager.java
new file mode 100644
index 0000000..11ee66f
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/s3/MinioS3Manager.java
@@ -0,0 +1,112 @@
+package app.mealsmadeeasy.api.s3;
+
+import io.minio.*;
+import io.minio.errors.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+@Component
+public class MinioS3Manager implements S3Manager {
+
+ @Value("${app.mealsmadeeasy.api.minio.endpoint}")
+ private String endpoint;
+
+ @Value("${app.mealsmadeeasy.api.minio.accessKey}")
+ private String accessKey;
+
+ @Value("${app.mealsmadeeasy.api.minio.secretKey}")
+ private String secretKey;
+
+ @Override
+ public String store(
+ String bucketName,
+ String filename,
+ String mimeType,
+ InputStream inputStream,
+ long objectSize
+ ) throws IOException {
+ try (final MinioClient client = MinioClient.builder()
+ .endpoint(this.endpoint)
+ .credentials(this.accessKey, this.secretKey)
+ .build()) {
+ final boolean bucketExists = client.bucketExists(BucketExistsArgs.builder()
+ .bucket(bucketName)
+ .build());
+ if (!bucketExists) {
+ client.makeBucket(MakeBucketArgs.builder()
+ .bucket(bucketName)
+ .build());
+ }
+
+ final ObjectWriteResponse response = client.putObject(
+ PutObjectArgs.builder()
+ .bucket(bucketName)
+ .stream(inputStream, objectSize, -1)
+ .contentType(mimeType)
+ .object(filename)
+ .build()
+ );
+ return response.object();
+ } catch (ErrorResponseException | XmlParserException | InsufficientDataException | InternalException |
+ InvalidKeyException | InvalidResponseException | NoSuchAlgorithmException | ServerException e) {
+ throw new IOException(e);
+ } catch (Exception minioBuildException) {
+ throw new RuntimeException(minioBuildException);
+ }
+ }
+
+ @Override
+ public void delete(String bucketName, String objectName) throws IOException {
+ try (final MinioClient client = MinioClient.builder()
+ .endpoint(this.endpoint)
+ .credentials(this.accessKey, this.secretKey)
+ .build()) {
+ client.removeObject(
+ RemoveObjectArgs.builder()
+ .bucket(bucketName)
+ .object(objectName)
+ .build()
+ );
+ } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
+ InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
+ throw new IOException(e);
+ } catch (Exception minioBuildException) {
+ throw new RuntimeException(minioBuildException);
+ }
+ }
+
+ @Override
+ public String getUrl(String bucketName, String objectName) {
+ return this.endpoint + "/" + bucketName + "/" + objectName;
+ }
+
+ public String getEndpoint() {
+ return this.endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public String getAccessKey() {
+ return this.accessKey;
+ }
+
+ public void setAccessKey(String accessKey) {
+ this.accessKey = accessKey;
+ }
+
+ public String getSecretKey() {
+ return this.secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+}
diff --git a/src/main/java/app/mealsmadeeasy/api/s3/S3Manager.java b/src/main/java/app/mealsmadeeasy/api/s3/S3Manager.java
new file mode 100644
index 0000000..144b3f7
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/s3/S3Manager.java
@@ -0,0 +1,29 @@
+package app.mealsmadeeasy.api.s3;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface S3Manager {
+
+ /**
+ * @param bucket the target bucket in which to store the content
+ * @param filename the filename to store, usually a uuid + appropriate extension
+ * @param mimeType the mimeType of the content
+ * @param inputStream the content
+ * @param size the size of the content
+ * @return the object name
+ * @throws IOException if there is an exception with the backing service
+ */
+ String store(
+ String bucket,
+ String filename,
+ String mimeType,
+ InputStream inputStream,
+ long size
+ ) throws IOException;
+
+ void delete(String bucketName, String objectName) throws IOException;
+
+ String getUrl(String bucketName, String objectName);
+
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index dd957bf..23237ab 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -6,3 +6,6 @@ spring.datasource.password=devpass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
app.mealsmadeeasy.api.security.access-token-lifetime=60
app.mealsmadeeasy.api.security.refresh-token-lifetime=120
+app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000
+app.mealsmadeeasy.api.minio.accessKey=minio-root
+app.mealsmadeeasy.api.minio.secretKey=test0123
\ No newline at end of file