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 @@ + + + + + HAL9000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + HAL9000 + + + + MorningLemon + + + German + + + HAL + 9000 + HAL9000 + robot + space + + + The famous red eye of HAL 9000 from Stanley Kubricks Film "2001: A Space Odyssey". + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + HAL9000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + HAL9000 + + + + MorningLemon + + + German + + + HAL + 9000 + HAL9000 + robot + space + + + The famous red eye of HAL 9000 from Stanley Kubricks Film "2001: A Space Odyssey". + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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