meals-made-easy-api/src/main/java/app/mealsmadeeasy/api/image/S3ImageService.java

212 lines
8.3 KiB
Java

package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.image.spec.ImageUpdateInfoSpec;
import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.s3.S3Manager;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
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;
private final String baseUrl;
public S3ImageService(
S3Manager s3Manager,
S3ImageRepository imageRepository,
@Value("${app.mealsmadeeasy.api.images.bucketName}") String imageBucketName,
@Value("${app.mealsmadeeasy.api.baseUrl}") String baseUrl
) {
this.s3Manager = s3Manager;
this.imageRepository = imageRepository;
this.imageBucketName = imageBucketName;
this.baseUrl = baseUrl;
}
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";
case "webp" -> "image/webp";
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";
case "image/webp" -> "webp";
default -> throw new ImageException(
ImageException.Type.UNKNOWN_MIME_TYPE,
"Unknown mime type: " + mimeType
);
};
}
private boolean transferFromSpec(S3ImageEntity entity, ImageCreateInfoSpec spec) {
boolean didTransfer = false;
if (spec.getAlt() != null) {
entity.setAlt(spec.getAlt());
didTransfer = true;
}
if (spec.getCaption() != null) {
entity.setCaption(spec.getCaption());
didTransfer = true;
}
if (spec.getPublic() != null) {
entity.setPublic(spec.getPublic());
didTransfer = true;
}
final @Nullable Set<User> viewersToAdd = spec.getViewersToAdd();
if (viewersToAdd != null) {
final Set<UserEntity> viewers = new HashSet<>(entity.getViewerEntities());
for (final User viewerToAdd : spec.getViewersToAdd()) {
viewers.add((UserEntity) viewerToAdd);
}
entity.setViewers(viewers);
didTransfer = true;
}
return didTransfer;
}
@Override
public Image create(
User owner,
String userFilename,
InputStream inputStream,
long objectSize,
ImageCreateInfoSpec createSpec
) 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;
final String objectName = this.s3Manager.store(
this.imageBucketName, filename, mimeType, inputStream, objectSize
);
final S3ImageEntity draft = new S3ImageEntity();
draft.setOwner((UserEntity) owner);
draft.setUserFilename(userFilename);
draft.setMimeType(mimeType);
draft.setObjectName(objectName);
this.transferFromSpec(draft, createSpec);
return this.imageRepository.save(draft);
}
@Override
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
public Image getById(long id, @Nullable User viewer) throws ImageException {
return this.imageRepository.findById(id).orElseThrow(() -> new ImageException(
ImageException.Type.INVALID_ID, "No Image with id: " + id
));
}
@Override
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
public Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException {
return this.imageRepository.findByOwnerAndUserFilename((UserEntity) owner, filename)
.orElseThrow(() -> new ImageException(
ImageException.Type.IMAGE_NOT_FOUND,
"No such image for owner " + owner + " with filename " + filename
));
}
@Override
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
public Image getByUsernameAndFilename(String username, String filename, User viewer) throws ImageException {
return this.imageRepository.findByOwnerUsernameAndFilename(username, filename).orElseThrow(
() -> new ImageException(
ImageException.Type.INVALID_USERNAME_OR_FILENAME,
"No such Image for username " + username + " and filename " + filename
)
);
}
@Override
@PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)")
public InputStream getImageContent(Image image, User viewer) throws IOException {
return this.s3Manager.load(this.imageBucketName, ((S3ImageEntity) image).getObjectName());
}
@Override
public List<Image> getImagesOwnedBy(User user) {
return new ArrayList<>(this.imageRepository.findAllByOwner((UserEntity) user));
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public Image update(final Image image, User modifier, ImageUpdateInfoSpec updateSpec) {
S3ImageEntity entity = (S3ImageEntity) image;
boolean didUpdate = this.transferFromSpec(entity, updateSpec);
final @Nullable Boolean clearAllViewers = updateSpec.getClearAllViewers();
if (clearAllViewers != null && clearAllViewers) {
entity.setViewers(new HashSet<>());
didUpdate = true;
} else {
final @Nullable Set<User> viewersToRemove = updateSpec.getViewersToRemove();
if (viewersToRemove != null) {
final Set<UserEntity> currentViewers = new HashSet<>(entity.getViewerEntities());
for (final User toRemove : updateSpec.getViewersToRemove()) {
currentViewers.remove((UserEntity) toRemove);
}
entity.setViewers(currentViewers);
didUpdate = true;
}
}
if (didUpdate) {
entity.setModified(LocalDateTime.now());
}
return this.imageRepository.save(entity);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public void deleteImage(Image image, User modifier) throws IOException {
final S3ImageEntity imageEntity = (S3ImageEntity) image;
this.imageRepository.delete(imageEntity);
this.s3Manager.delete("images", imageEntity.getObjectName());
}
private String getImageUrl(Image image) {
return this.baseUrl + "/images/" + image.getOwner().getUsername() + "/" + image.getUserFilename();
}
@Override
public ImageView toImageView(Image image, @Nullable User viewer) {
if (viewer != null && image.getOwner().getUsername().equals(viewer.getUsername())) {
return ImageView.from(image, this.getImageUrl(image), true);
} else {
return ImageView.from(image, this.getImageUrl(image), false);
}
}
}