Set up picture upload mechanism.

This commit is contained in:
Florian THIERRY
2024-03-13 14:26:28 +01:00
parent ed766d4c8c
commit 0b00f9b0aa
16 changed files with 305 additions and 2 deletions

2
.gitignore vendored
View File

@@ -36,3 +36,5 @@ build/
**/node_modules **/node_modules
**/.angular **/.angular
**/pictures-folder

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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.");
}
}

View File

@@ -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
) {}

View File

@@ -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);
}
}

View File

@@ -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<Picture> findById(UUID pictureId);
void save(Picture picture);
void deleteById(UUID pictureId);
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<Picture> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<PictureEntity, UUID> {
}

View File

@@ -47,3 +47,8 @@ CREATE TABLE IF NOT EXISTS publication (
); );
CREATE INDEX publication_author_id_idx ON publication (author_id); CREATE INDEX publication_author_id_idx ON publication (author_id);
CREATE INDEX publication_category_id_idx ON publication (category_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)
);

View File

@@ -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

View File

@@ -5,12 +5,17 @@ application:
expirationDelayInMinutes: 30 expirationDelayInMinutes: 30
refreshToken: refreshToken:
expirationDelayInDays: 7 expirationDelayInDays: 7
pictures:
path: /opt/codiki/pictures/
temp-path: /opt/codiki/pictures/temp/
logging: logging:
level: level:
org.springframework.security: DEBUG org.springframework.security: DEBUG
server: server:
http2:
enabled: true
error: error:
whitelabel: whitelabel:
enabled: false # Disable html error responses. enabled: false # Disable html error responses.
@@ -22,3 +27,7 @@ spring:
url: jdbc:postgresql://localhost:50001/codiki_db url: jdbc:postgresql://localhost:50001/codiki_db
username: codiki_user username: codiki_user
password: password password: password
servlet:
multipart:
max-file-size: 150MB
max-request-size: 151MB

View File

@@ -1,6 +1,6 @@
vars { vars {
url: http://localhost:8080 url: http://localhost:8080
publicationId: fce1de27-11c6-4deb-a248-b63288c00037 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 categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9
} }