Skip to content

Commit

Permalink
Add the capability to upload article images
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammed-ezzedine committed Dec 31, 2023
1 parent da3c3c6 commit f0f1064
Show file tree
Hide file tree
Showing 22 changed files with 559 additions and 1 deletion.
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-hateoas")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")
implementation("org.hashids:hashids:1.0.3")

Expand All @@ -34,6 +36,7 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("com.google.jimfs:jimfs:1.1");
testImplementation("org.testcontainers:junit-jupiter")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ public class WebConfiguration implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*");
registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("*").allowCredentials(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package me.ezzedine.mohammed.personalspace.article.api.image;

import me.ezzedine.mohammed.personalspace.article.core.image.FailedToUploadImageException;
import me.ezzedine.mohammed.personalspace.article.core.image.ImageDoesNotExistException;
import me.ezzedine.mohammed.personalspace.article.core.image.ImageNameAlreadyExistsException;
import me.ezzedine.mohammed.personalspace.config.security.AdminAccess;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RequestMapping("articles/images")
public interface ArticleImageApi {

@PostMapping
@AdminAccess
ResponseEntity<UploadImageApiResponse> uploadImage(@RequestParam("upload") MultipartFile image) throws ImageNameAlreadyExistsException, FailedToUploadImageException;

@GetMapping("{name}")
ResponseEntity<Resource> serveImage(@PathVariable String name) throws ImageDoesNotExistException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package me.ezzedine.mohammed.personalspace.article.api.image;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.ezzedine.mohammed.personalspace.article.core.image.ArticleImageService;
import me.ezzedine.mohammed.personalspace.article.core.image.FailedToUploadImageException;
import me.ezzedine.mohammed.personalspace.article.core.image.ImageDoesNotExistException;
import me.ezzedine.mohammed.personalspace.article.core.image.ImageNameAlreadyExistsException;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@Slf4j
@RestController
@RequiredArgsConstructor
public class ArticleImageController implements ArticleImageApi {

private final ArticleImageService imageService;

@Override
public ResponseEntity<UploadImageApiResponse> uploadImage(MultipartFile image) throws ImageNameAlreadyExistsException, FailedToUploadImageException {
log.info("Received a request to upload the image {}", image.getOriginalFilename());
try {
imageService.upload(image.getOriginalFilename(), image.getBytes());
} catch (IOException e) {
throw new FailedToUploadImageException(e.getMessage());
}

UploadImageApiResponse response = UploadImageApiResponse.builder().url(getImageLink(image)).build();
return ResponseEntity.ok().body(response);
}

@SneakyThrows
private static String getImageLink(MultipartFile image) {
return linkTo(methodOn(ArticleImageController.class).serveImage(image.getOriginalFilename())).toUri().toString();
}

@Override
public ResponseEntity<Resource> serveImage(String name) throws ImageDoesNotExistException {
log.info("Received a request to serve the image {}", name);
Resource resource = imageService.serveImage(name);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", resource.getFilename()))
.body(resource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.ezzedine.mohammed.personalspace.article.api.image;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class UploadImageApiResponse {
private String url;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.ezzedine.mohammed.personalspace.article.api.image.advice;

import me.ezzedine.mohammed.personalspace.article.core.image.FailedToUploadImageException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class FailedToUploadImageAdvice extends ResponseEntityExceptionHandler {

@ExceptionHandler(FailedToUploadImageException.class)
protected ResponseEntity<Object> handle(FailedToUploadImageException exception, WebRequest request) {
return handleExceptionInternal(exception, exception.getMessage(), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.ezzedine.mohammed.personalspace.article.api.image.advice;

import me.ezzedine.mohammed.personalspace.article.core.image.ImageDoesNotExistException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ImageDoesNotExistAdvice extends ResponseEntityExceptionHandler {

@ExceptionHandler(ImageDoesNotExistException.class)
protected ResponseEntity<Object> handle(ImageDoesNotExistException exception, WebRequest request) {
return handleExceptionInternal(exception, exception.getMessage(), new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.ezzedine.mohammed.personalspace.article.api.image.advice;

import me.ezzedine.mohammed.personalspace.article.core.image.ImageNameAlreadyExistsException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ImageNameAlreadyExistsAdvice extends ResponseEntityExceptionHandler {

@ExceptionHandler(ImageNameAlreadyExistsException.class)
protected ResponseEntity<Object> handle(ImageNameAlreadyExistsException exception, WebRequest request) {
return handleExceptionInternal(exception, exception.getMessage(), new HttpHeaders(), HttpStatus.CONFLICT, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

import java.nio.file.Path;

public interface ArticleImagePathResolver {
Path resolve(String imageName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

import org.springframework.core.io.Resource;

public interface ArticleImageService {
void upload(String name, byte[] content) throws ImageNameAlreadyExistsException, FailedToUploadImageException;
Resource serveImage(String name) throws ImageDoesNotExistException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

public interface ArticleImagesStoragePathFactory {
String get();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.nio.file.Path;
import java.nio.file.Paths;

@Service
@RequiredArgsConstructor
public class ConcreteArticleImagePathResolver implements ArticleImagePathResolver {

private final ArticleImagesStoragePathFactory storagePathFactory;

@Override
public Path resolve(String imageName) {
return Paths.get(storagePathFactory.get(), imageName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;

@Service
@RequiredArgsConstructor
public class ConcreteArticleImageService implements ArticleImageService {

private final ArticleImagePathResolver pathResolver;

@Override
public void upload(String name, byte[] content) throws ImageNameAlreadyExistsException, FailedToUploadImageException {
Path imagePath = pathResolver.resolve(name);

if (Files.exists(imagePath)) {
throw new ImageNameAlreadyExistsException(name);
}

try {
Files.createDirectories(imagePath.getParent());
Files.write(imagePath, content);
} catch (IOException e) {
throw new FailedToUploadImageException(e.getMessage());
}
}

@Override
@SneakyThrows(MalformedURLException.class)
public Resource serveImage(String name) throws ImageDoesNotExistException {
Path imagePath = pathResolver.resolve(name);
if (Files.notExists(imagePath)) {
throw new ImageDoesNotExistException(name);
}

return new UrlResource(imagePath.toUri());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

import org.springframework.stereotype.Service;

import java.nio.file.Path;
import java.nio.file.Paths;

@Service
public class ConcreteArticleImagesStoragePathFactory implements ArticleImagesStoragePathFactory {

@Override
public String get() {
Path path = Paths.get(System.getProperty("user.dir"), "article-images");
return path.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

public class FailedToUploadImageException extends Exception {
public FailedToUploadImageException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

public class ImageDoesNotExistException extends Exception {
public ImageDoesNotExistException(String name) {
super(String.format("Image '%s' does not exist in the storage", name));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.ezzedine.mohammed.personalspace.article.core.image;

public class ImageNameAlreadyExistsException extends Exception {
public ImageNameAlreadyExistsException(String name) {
super(String.format("Image name %s already exists.", name));
}
}
Loading

0 comments on commit f0f1064

Please sign in to comment.