Implemented ImageController.putImage and related.
This commit is contained in:
parent
6f7016f870
commit
9976b7337f
@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||||
import org.springframework.test.context.DynamicPropertySource;
|
import org.springframework.test.context.DynamicPropertySource;
|
||||||
@ -21,7 +22,9 @@ import org.testcontainers.utility.DockerImageName;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
@Testcontainers
|
@Testcontainers
|
||||||
@ -73,7 +76,6 @@ public class ImageControllerTests {
|
|||||||
owner,
|
owner,
|
||||||
USER_FILENAME,
|
USER_FILENAME,
|
||||||
hal9000,
|
hal9000,
|
||||||
"image/svg+xml",
|
|
||||||
27881L
|
27881L
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -133,4 +135,39 @@ public class ImageControllerTests {
|
|||||||
this.doGetImageTestWithViewer(accessToken);
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,6 @@ public class S3ImageServiceTests {
|
|||||||
owner,
|
owner,
|
||||||
USER_FILENAME,
|
USER_FILENAME,
|
||||||
hal9000,
|
hal9000,
|
||||||
"image/svg+xml",
|
|
||||||
27881L
|
27881L
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,6 @@ public class DevConfiguration {
|
|||||||
testUser,
|
testUser,
|
||||||
"HAL9000.svg",
|
"HAL9000.svg",
|
||||||
inputStream,
|
inputStream,
|
||||||
"image/svg+xml",
|
|
||||||
27881L
|
27881L
|
||||||
);
|
);
|
||||||
this.imageService.setPublic(image, testUser, true);
|
this.imageService.setPublic(image, testUser, true);
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
package app.mealsmadeeasy.api.image;
|
package app.mealsmadeeasy.api.image;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.user.UserService;
|
import app.mealsmadeeasy.api.user.UserService;
|
||||||
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -18,6 +19,23 @@ import java.io.InputStream;
|
|||||||
@RequestMapping("/images")
|
@RequestMapping("/images")
|
||||||
public class ImageController {
|
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 ImageService imageService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
@ -40,4 +58,36 @@ public class ImageController {
|
|||||||
.body(new InputStreamResource(imageInputStream));
|
.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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface ImageService {
|
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;
|
throws IOException, ImageException;
|
||||||
|
|
||||||
Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException;
|
Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException;
|
||||||
|
@ -13,10 +13,14 @@ import java.io.InputStream;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class S3ImageService implements ImageService {
|
public class S3ImageService implements ImageService {
|
||||||
|
|
||||||
|
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$");
|
||||||
|
|
||||||
private final S3Manager s3Manager;
|
private final S3Manager s3Manager;
|
||||||
private final S3ImageRepository imageRepository;
|
private final S3ImageRepository imageRepository;
|
||||||
private final String imageBucketName;
|
private final String imageBucketName;
|
||||||
@ -31,8 +35,25 @@ public class S3ImageService implements ImageService {
|
|||||||
this.imageBucketName = imageBucketName;
|
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 {
|
private String getExtension(String mimeType) throws ImageException {
|
||||||
return switch (mimeType) {
|
return switch (mimeType) {
|
||||||
|
case "image/jpeg" -> "jpg";
|
||||||
|
case "image/png" -> "png";
|
||||||
case "image/svg+xml" -> "svg";
|
case "image/svg+xml" -> "svg";
|
||||||
default -> throw new ImageException(
|
default -> throw new ImageException(
|
||||||
ImageException.Type.UNKNOWN_MIME_TYPE,
|
ImageException.Type.UNKNOWN_MIME_TYPE,
|
||||||
@ -42,8 +63,9 @@ public class S3ImageService implements ImageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
throws IOException, ImageException {
|
||||||
|
final String mimeType = this.getMimeType(userFilename);
|
||||||
final String uuid = UUID.randomUUID().toString();
|
final String uuid = UUID.randomUUID().toString();
|
||||||
final String extension = this.getExtension(mimeType);
|
final String extension = this.getExtension(mimeType);
|
||||||
final String filename = uuid + "." + extension;
|
final String filename = uuid + "." + extension;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user