diff --git a/.gitignore b/.gitignore index 3d2334c..969462d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ build/ **/docker/postgresql/pgdata **/node_modules -**/.angular \ No newline at end of file +**/.angular + +**/pictures-folder diff --git a/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java b/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java new file mode 100644 index 0000000..71c2d0f --- /dev/null +++ b/codiki-application/src/main/java/org/codiki/application/picture/PictureUseCases.java @@ -0,0 +1,33 @@ +package org.codiki.application.picture; + +import java.io.File; +import java.util.UUID; + +import static org.codiki.domain.picture.model.builder.PictureBuilder.aPicture; +import org.codiki.domain.picture.model.Picture; +import org.codiki.domain.picture.port.PicturePort; +import org.springframework.stereotype.Service; + +@Service +public class PictureUseCases { + private final PicturePort picturePort; + + public PictureUseCases(PicturePort picturePort) { + this.picturePort = picturePort; + } + + public Picture createPicture(File pictureFile) { + Picture newPicture = aPicture() + .withId(UUID.randomUUID()) + .withContentFile(pictureFile) + .build(); + + picturePort.save(newPicture); + + return newPicture; + } + + public void deletePicture(UUID pictureId) { + picturePort.deleteById(pictureId); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureContentLoadingErrorException.java b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureContentLoadingErrorException.java new file mode 100644 index 0000000..c246ab2 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureContentLoadingErrorException.java @@ -0,0 +1,11 @@ +package org.codiki.domain.picture.exception; + +import java.util.UUID; + +import org.codiki.domain.exception.FunctionnalException; + +public class PictureContentLoadingErrorException extends FunctionnalException { + public PictureContentLoadingErrorException(UUID pictureId) { + super(String.format("An error occurred while loading picture content (picture id=%s).", pictureId)); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureStorageErrorException.java b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureStorageErrorException.java new file mode 100644 index 0000000..4525ee2 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/exception/PictureStorageErrorException.java @@ -0,0 +1,9 @@ +package org.codiki.domain.picture.exception; + +import org.codiki.domain.exception.FunctionnalException; + +public class PictureStorageErrorException extends FunctionnalException { + public PictureStorageErrorException() { + super("An error occurred while storing picture content file."); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/model/Picture.java b/codiki-domain/src/main/java/org/codiki/domain/picture/model/Picture.java new file mode 100644 index 0000000..5aa9f86 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/model/Picture.java @@ -0,0 +1,9 @@ +package org.codiki.domain.picture.model; + +import java.io.File; +import java.util.UUID; + +public record Picture( + UUID id, + File contentFile +) {} diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/model/builder/PictureBuilder.java b/codiki-domain/src/main/java/org/codiki/domain/picture/model/builder/PictureBuilder.java new file mode 100644 index 0000000..6b3b0fe --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/model/builder/PictureBuilder.java @@ -0,0 +1,38 @@ +package org.codiki.domain.picture.model.builder; + +import java.io.File; +import java.util.UUID; + +import org.codiki.domain.picture.model.Picture; + +public class PictureBuilder { + private UUID id; + private File contentFile; + + private PictureBuilder() {} + + public static PictureBuilder aPicture() { + return new PictureBuilder(); + } + + public PictureBuilder basedOn(Picture picture) { + id = picture.id(); + contentFile = picture.contentFile(); + return this; + } + + public PictureBuilder withId(UUID id) { + this.id = id; + return this; + } + + + public PictureBuilder withContentFile(File contentFile) { + this.contentFile = contentFile; + return this; + } + + public Picture build() { + return new Picture(id, contentFile); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/picture/port/PicturePort.java b/codiki-domain/src/main/java/org/codiki/domain/picture/port/PicturePort.java new file mode 100644 index 0000000..535f431 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/picture/port/PicturePort.java @@ -0,0 +1,16 @@ +package org.codiki.domain.picture.port; + +import java.util.Optional; +import java.util.UUID; + +import org.codiki.domain.picture.model.Picture; + +public interface PicturePort { + boolean existsById(UUID pictureId); + + Optional findById(UUID pictureId); + + void save(Picture picture); + + void deleteById(UUID pictureId); +} diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java b/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java new file mode 100644 index 0000000..900bf46 --- /dev/null +++ b/codiki-exposition/src/main/java/org/codiki/exposition/picture/MultipartFileConverter.java @@ -0,0 +1,28 @@ +package org.codiki.exposition.picture; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class MultipartFileConverter { + private final String tempPicturesForlderPath; + + public MultipartFileConverter(@Value("${application.pictures.temp-path}") String tempPicturesForlderPath) { + this.tempPicturesForlderPath = tempPicturesForlderPath; + } + + public File transformToFile(MultipartFile fileContent) { + File pictureFile = new File(String.format("%s/%s", tempPicturesForlderPath, UUID.randomUUID())); + try { + fileContent.transferTo(pictureFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + return pictureFile; + } +} diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java b/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java new file mode 100644 index 0000000..3cb07e0 --- /dev/null +++ b/codiki-exposition/src/main/java/org/codiki/exposition/picture/PictureController.java @@ -0,0 +1,35 @@ +package org.codiki.exposition.picture; + +import java.io.File; +import java.util.UUID; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; +import org.codiki.application.picture.PictureUseCases; +import org.codiki.domain.picture.model.Picture; +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.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/pictures") +public class PictureController { + private final MultipartFileConverter multipartFileConverter; + private final PictureUseCases pictureUseCases; + + public PictureController( + MultipartFileConverter multipartFileConverter, + PictureUseCases pictureUseCases + ) { + this.multipartFileConverter = multipartFileConverter; + this.pictureUseCases = pictureUseCases; + } + + @PostMapping(consumes = MULTIPART_FORM_DATA_VALUE) + public UUID uploadPicture(@RequestParam("file") MultipartFile fileContent) { + File pictureFile = multipartFileConverter.transformToFile(fileContent); + Picture newPicture = pictureUseCases.createPicture(pictureFile); + return newPicture.id(); + } +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java new file mode 100644 index 0000000..0407dc8 --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/PictureJpaAdapter.java @@ -0,0 +1,57 @@ +package org.codiki.infrastructure.picture; + +import java.io.File; +import java.util.Optional; +import java.util.UUID; + +import org.codiki.domain.picture.exception.PictureStorageErrorException; +import org.codiki.domain.picture.model.Picture; +import org.codiki.domain.picture.port.PicturePort; +import org.codiki.infrastructure.picture.model.PictureEntity; +import org.codiki.infrastructure.picture.repository.PictureRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import jakarta.transaction.Transactional; + +@Component +public class PictureJpaAdapter implements PicturePort { + private final PictureRepository repository; + private final String pictureFolderPath; + + public PictureJpaAdapter( + PictureRepository repository, + @Value("${application.pictures.path}") String pictureFolderPath + ) { + this.repository = repository; + this.pictureFolderPath = pictureFolderPath; + } + + @Override + public boolean existsById(UUID pictureId) { + return repository.existsById(pictureId); + } + + @Override + public Optional findById(UUID pictureId) { + return repository.findById(pictureId) + .map(PictureEntity::toDomain); + } + + @Override + @Transactional + public void save(Picture picture) { + PictureEntity pictureEntity = new PictureEntity(picture); + repository.save(pictureEntity); + + boolean isMoved = picture.contentFile().renameTo(new File(String.format("%s/%s", pictureFolderPath, picture.id()))); + if (!isMoved) { + throw new PictureStorageErrorException(); + } + } + + @Override + public void deleteById(UUID pictureId) { + repository.deleteById(pictureId); + } +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/model/PictureEntity.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/model/PictureEntity.java new file mode 100644 index 0000000..ae6d752 --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/model/PictureEntity.java @@ -0,0 +1,32 @@ +package org.codiki.infrastructure.picture.model; + +import java.util.UUID; + +import org.codiki.domain.picture.model.Picture; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "picture") +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class PictureEntity { + @Id + private UUID id; + + public PictureEntity(Picture picture) { + id = picture.id(); + } + + public Picture toDomain() { + return new Picture(id, null); + } +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/repository/PictureRepository.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/repository/PictureRepository.java new file mode 100644 index 0000000..89a035e --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/picture/repository/PictureRepository.java @@ -0,0 +1,9 @@ +package org.codiki.infrastructure.picture.repository; + +import java.util.UUID; + +import org.codiki.infrastructure.picture.model.PictureEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PictureRepository extends JpaRepository { +} diff --git a/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql b/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql index 6cea0b3..187a55c 100644 --- a/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql +++ b/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql @@ -47,3 +47,8 @@ CREATE TABLE IF NOT EXISTS publication ( ); CREATE INDEX publication_author_id_idx ON publication (author_id); CREATE INDEX publication_category_id_idx ON publication (category_id); + +CREATE TABLE IF NOT EXISTS picture ( + id UUID NOT NULL, + CONSTRAINT picture_pk PRIMARY KEY (id) +); diff --git a/codiki-launcher/src/main/resources/application-local.yml b/codiki-launcher/src/main/resources/application-local.yml new file mode 100644 index 0000000..6d2da3c --- /dev/null +++ b/codiki-launcher/src/main/resources/application-local.yml @@ -0,0 +1,10 @@ +application: + pictures: + path: /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/ + temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/temp/ + +spring: + servlet: + multipart: + max-file-size: 1MB + max-request-size: 1MB diff --git a/codiki-launcher/src/main/resources/application.yml b/codiki-launcher/src/main/resources/application.yml index 3334012..117634a 100644 --- a/codiki-launcher/src/main/resources/application.yml +++ b/codiki-launcher/src/main/resources/application.yml @@ -5,12 +5,17 @@ application: expirationDelayInMinutes: 30 refreshToken: expirationDelayInDays: 7 + pictures: + path: /opt/codiki/pictures/ + temp-path: /opt/codiki/pictures/temp/ logging: level: org.springframework.security: DEBUG server: + http2: + enabled: true error: whitelabel: enabled: false # Disable html error responses. @@ -22,3 +27,7 @@ spring: url: jdbc:postgresql://localhost:50001/codiki_db username: codiki_user password: password + servlet: + multipart: + max-file-size: 150MB + max-request-size: 151MB diff --git a/rest-client-collection/Codiki/environments/localhost.bru b/rest-client-collection/Codiki/environments/localhost.bru index d5669d1..9e658b6 100644 --- a/rest-client-collection/Codiki/environments/localhost.bru +++ b/rest-client-collection/Codiki/environments/localhost.bru @@ -1,6 +1,6 @@ vars { url: http://localhost:8080 publicationId: fce1de27-11c6-4deb-a248-b63288c00037 - bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNWExM2RjNy0wMjlkLTRlYWItYTYzZC1jMWU5NmY5MDI0MWQiLCJleHAiOjE3MTAyNjU4MTZ9.t8tZce0gAXZ_DC2QEsdvJn6m-Ykjou1v4zDUIPWhzfWYR-JTeiFsfa68jkFwK2WT1aMvZppnVuc991g-yAjrPg + bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTAzMzc2Njd9.ExV8xDeqqKk5WjIVb16NBqF1gPoRqx7uL4jQIhWjjY0QVhB5EAGdHMIbLr4s9Ck2f6z8U4sRlpPAQquDOr_9NA categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9 }