Implemented ImageController.putImage and related.

This commit is contained in:
Jesse Brault 2024-07-25 09:49:43 -05:00
parent 6f7016f870
commit 9976b7337f
8 changed files with 223 additions and 9 deletions

View File

@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@ -21,7 +22,9 @@ import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.io.InputStream;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@Testcontainers
@ -73,7 +76,6 @@ public class ImageControllerTests {
owner,
USER_FILENAME,
hal9000,
"image/svg+xml",
27881L
);
}
@ -133,4 +135,39 @@ public class ImageControllerTests {
this.doGetImageTestWithViewer(accessToken);
}
@Test
@DirtiesContext
public void putImage() throws Exception {
final User owner = this.createTestUser("imageOwner");
final String accessToken = this.getAccessToken(owner.getUsername());
try (final InputStream hal9000 = getHal9000()) {
final MockMultipartFile mockMultipartFile = new MockMultipartFile(
"image", "HAL9000.svg", "image/svg+xml", hal9000
);
this.mockMvc.perform(
multipart("/images")
.file(mockMultipartFile)
.param("filename", "HAL9000.svg")
.param("alt", "HAL 9000")
.param("caption", "HAL 9000, from 2001: A Space Odyssey")
.param("isPublic", "true")
.header("Authorization", "Bearer " + accessToken)
.with(req -> {
req.setMethod("PUT");
return req;
})
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.created").exists())
.andExpect(jsonPath("$.modified").value(nullValue()))
.andExpect(jsonPath("$.filename").value(USER_FILENAME))
.andExpect(jsonPath("$.mimeType").value("image/svg+xml"))
.andExpect(jsonPath("$.alt").value("HAL 9000"))
.andExpect(jsonPath("$.caption").value("HAL 9000, from 2001: A Space Odyssey"))
.andExpect(jsonPath("$.isPublic").value(true))
.andExpect(jsonPath("$.owner.username").value("imageOwner"))
.andExpect(jsonPath("$.owner.id").value(owner.getId()));
}
}
}

View File

@ -68,7 +68,6 @@ public class S3ImageServiceTests {
owner,
USER_FILENAME,
hal9000,
"image/svg+xml",
27881L
);
}

View File

@ -51,7 +51,6 @@ public class DevConfiguration {
testUser,
"HAL9000.svg",
inputStream,
"image/svg+xml",
27881L
);
this.imageService.setPublic(image, testUser, true);

View File

@ -1,15 +1,16 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserService;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
@ -18,6 +19,23 @@ import java.io.InputStream;
@RequestMapping("/images")
public class ImageController {
private static ImageView getView(Image image, User owner) {
final ImageView imageView = new ImageView();
imageView.setCreated(image.getCreated());
imageView.setFilename(image.getUserFilename());
imageView.setMimeType(image.getMimeType());
imageView.setAlt(image.getAlt());
imageView.setCaption(image.getCaption());
imageView.setIsPublic(image.isPublic());
final UserInfoView userInfoView = new UserInfoView();
userInfoView.setId(owner.getId());
userInfoView.setUsername(owner.getUsername());
imageView.setOwner(userInfoView);
return imageView;
}
private final ImageService imageService;
private final UserService userService;
@ -40,4 +58,36 @@ public class ImageController {
.body(new InputStreamResource(imageInputStream));
}
@PutMapping
public ResponseEntity<ImageView> putImage(
@RequestParam MultipartFile image,
@RequestParam String filename,
@RequestParam(required = false) String alt,
@RequestParam(required = false) String caption,
@RequestParam(required = false) Boolean isPublic,
@AuthenticationPrincipal User principal
) throws IOException, ImageException {
if (principal == null) {
throw new AccessDeniedException("Must be logged in.");
}
Image saved = this.imageService.create(
principal,
filename,
image.getInputStream(),
image.getSize()
);
if (alt != null) {
saved = this.imageService.setAlt(saved, principal, alt);
}
if (caption != null) {
saved = this.imageService.setCaption(saved, principal, caption);
}
if (isPublic != null) {
saved = this.imageService.setPublic(saved, principal, isPublic);
}
return ResponseEntity.status(201).body(getView(saved, principal));
}
}

View File

@ -9,7 +9,7 @@ import java.util.List;
public interface ImageService {
Image create(User owner, String userFilename, InputStream inputStream, String mimeType, long objectSize)
Image create(User owner, String userFilename, InputStream inputStream, long objectSize)
throws IOException, ImageException;
Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException;

View File

@ -13,10 +13,14 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class S3ImageService implements ImageService {
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$");
private final S3Manager s3Manager;
private final S3ImageRepository imageRepository;
private final String imageBucketName;
@ -31,8 +35,25 @@ public class S3ImageService implements ImageService {
this.imageBucketName = imageBucketName;
}
private String getMimeType(String userFilename) {
final Matcher m = extensionPattern.matcher(userFilename);
if (m.matches()) {
final String extension = m.group(1);
return switch (extension) {
case "jpg", "jpeg" -> "image/jpeg";
case "png" -> "image/png";
case "svg" -> "image/svg+xml";
default -> throw new IllegalArgumentException("Cannot determine mime type for extension: " + extension);
};
} else {
throw new IllegalArgumentException("Cannot determine mime type for filename: " + userFilename);
}
}
private String getExtension(String mimeType) throws ImageException {
return switch (mimeType) {
case "image/jpeg" -> "jpg";
case "image/png" -> "png";
case "image/svg+xml" -> "svg";
default -> throw new ImageException(
ImageException.Type.UNKNOWN_MIME_TYPE,
@ -42,8 +63,9 @@ public class S3ImageService implements ImageService {
}
@Override
public Image create(User owner, String userFilename, InputStream inputStream, String mimeType, long objectSize)
public Image create(User owner, String userFilename, InputStream inputStream, long objectSize)
throws IOException, ImageException {
final String mimeType = this.getMimeType(userFilename);
final String uuid = UUID.randomUUID().toString();
final String extension = this.getExtension(mimeType);
final String filename = uuid + "." + extension;

View File

@ -0,0 +1,83 @@
package app.mealsmadeeasy.api.image.view;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
public class ImageView {
private LocalDateTime created;
private @Nullable LocalDateTime modified;
private String filename;
private String mimeType;
private @Nullable String alt;
private @Nullable String caption;
private UserInfoView owner;
private boolean isPublic;
public LocalDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
this.created = created;
}
public @Nullable LocalDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable LocalDateTime modified) {
this.modified = modified;
}
public String getFilename() {
return this.filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getMimeType() {
return this.mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public String getAlt() {
return this.alt;
}
public void setAlt(String alt) {
this.alt = alt;
}
public String getCaption() {
return this.caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public UserInfoView getOwner() {
return this.owner;
}
public void setOwner(UserInfoView owner) {
this.owner = owner;
}
public boolean getIsPublic() {
return this.isPublic;
}
public void setIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}
}

View File

@ -0,0 +1,24 @@
package app.mealsmadeeasy.api.user.view;
public class UserInfoView {
private long id;
private String username;
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
}