Compare commits

...

4 Commits

Author SHA1 Message Date
Jesse Brault
0a619c5d41 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	build.gradle
2025-12-13 12:17:49 -06:00
Jesse Brault
2505a7ee9e Implement db storage of image height/width, and add to ImageView. 2025-12-13 12:15:10 -06:00
Jesse Brault
a24f4192a9 Add localhost:4200 for new Angular frontend. 2025-12-13 11:41:41 -06:00
Jesse Brault
e9b106d5de Add dev compose file. 2025-12-13 11:41:26 -06:00
7 changed files with 108 additions and 1 deletions

View File

@ -66,6 +66,11 @@ dependencies {
compileOnly 'org.jetbrains:annotations:26.0.1' compileOnly 'org.jetbrains:annotations:26.0.1'
// https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-webp
runtimeOnly 'com.twelvemonkeys.imageio:imageio-webp:3.12.0'
compileOnly 'org.jetbrains:annotations:24.1.0'
// Custom testing // Custom testing
testRuntimeOnly 'com.h2database:h2' testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.testcontainers:testcontainers:1.20.4' testImplementation 'org.testcontainers:testcontainers:1.20.4'

39
compose.dev.yaml Normal file
View File

@ -0,0 +1,39 @@
name: meals-made-easy-api-dev
services:
db:
image: mysql:latest
ports:
- '55001:3306'
- '55000:33060'
env_file: .env
environment:
MYSQL_DATABASE: meals_made_easy_api
MYSQL_USER: meals-made-easy-api-user
healthcheck:
test: mysqladmin ping -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
interval: 5s
timeout: 10s
retries: 10
volumes:
- mysql-data:/var/lib/mysql
minio:
image: minio/minio:latest
ports:
- 9000:9000
- 9001:9001
env_file:
- .env
environment:
MINIO_ROOT_USER: minio-root
volumes:
- minio-data:/data
command:
- server
- /data
- --console-address
- :9001
profiles:
- deps
volumes:
mysql-data:
minio-data:

View File

@ -16,7 +16,7 @@ public class MvcConfiguration {
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedMethods("GET", "POST", "PUT", "DELETE") .allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedOrigins("http://localhost:5173") .allowedOrigins("http://localhost:5173", "http://localhost:4200")
.allowCredentials(true); .allowCredentials(true);
} }

View File

@ -16,5 +16,7 @@ public interface Image {
@Nullable String getCaption(); @Nullable String getCaption();
User getOwner(); User getOwner();
boolean isPublic(); boolean isPublic();
int getHeight();
int getWidth();
Set<User> getViewers(); Set<User> getViewers();
} }

View File

@ -35,6 +35,12 @@ public class S3ImageEntity implements Image {
@Column(nullable = false) @Column(nullable = false)
private String objectName; private String objectName;
@Column(nullable = false)
private int height;
@Column(nullable = false)
private int width;
@ManyToOne(optional = false) @ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false) @JoinColumn(name = "owner_id", nullable = false)
private UserEntity owner; private UserEntity owner;
@ -116,6 +122,24 @@ public class S3ImageEntity implements Image {
this.objectName = objectName; this.objectName = objectName;
} }
@Override
public int getHeight() {
return this.height;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getWidth() {
return this.width;
}
public void setWidth(int width) {
this.width = width;
}
@Override @Override
public User getOwner() { public User getOwner() {
return this.owner; return this.owner;

View File

@ -7,11 +7,15 @@ import app.mealsmadeeasy.api.s3.S3Manager;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -23,6 +27,7 @@ import java.util.regex.Pattern;
public class S3ImageService implements ImageService { public class S3ImageService implements ImageService {
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$"); private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$");
private static final Logger logger = LoggerFactory.getLogger(S3ImageService.class);
private final S3Manager s3Manager; private final S3Manager s3Manager;
private final S3ImageRepository imageRepository; private final S3ImageRepository imageRepository;
@ -112,11 +117,23 @@ public class S3ImageService implements ImageService {
this.imageBucketName, filename, mimeType, inputStream, objectSize this.imageBucketName, filename, mimeType, inputStream, objectSize
); );
final int height, width;
try (final InputStream imageContent = this.s3Manager.load(this.imageBucketName, objectName)) {
final BufferedImage bufferedImage = ImageIO.read(imageContent);
if (bufferedImage == null) {
logger.error("ImageIO could not read image: {} ({})", userFilename, objectName);
}
height = bufferedImage.getHeight();
width = bufferedImage.getWidth();
}
final S3ImageEntity draft = new S3ImageEntity(); final S3ImageEntity draft = new S3ImageEntity();
draft.setOwner((UserEntity) owner); draft.setOwner((UserEntity) owner);
draft.setUserFilename(userFilename); draft.setUserFilename(userFilename);
draft.setMimeType(mimeType); draft.setMimeType(mimeType);
draft.setObjectName(objectName); draft.setObjectName(objectName);
draft.setHeight(height);
draft.setWidth(width);
this.transferFromSpec(draft, createSpec); this.transferFromSpec(draft, createSpec);
return this.imageRepository.save(draft); return this.imageRepository.save(draft);
} }

View File

@ -21,6 +21,8 @@ public class ImageView {
view.setCaption(image.getCaption()); view.setCaption(image.getCaption());
view.setOwner(UserInfoView.from(image.getOwner())); view.setOwner(UserInfoView.from(image.getOwner()));
view.setIsPublic(image.isPublic()); view.setIsPublic(image.isPublic());
view.setHeight(image.getHeight());
view.setWidth(image.getWidth());
if (includeViewers) { if (includeViewers) {
view.setViewers(image.getViewers().stream() view.setViewers(image.getViewers().stream()
.map(UserInfoView::from) .map(UserInfoView::from)
@ -39,6 +41,8 @@ public class ImageView {
private @Nullable String caption; private @Nullable String caption;
private UserInfoView owner; private UserInfoView owner;
private boolean isPublic; private boolean isPublic;
private int height;
private int width;
private @Nullable Set<UserInfoView> viewers; private @Nullable Set<UserInfoView> viewers;
public String getUrl() { public String getUrl() {
@ -113,6 +117,22 @@ public class ImageView {
this.isPublic = isPublic; this.isPublic = isPublic;
} }
public int getHeight() {
return this.height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWidth() {
return this.width;
}
public void setWidth(int width) {
this.width = width;
}
public @Nullable Set<UserInfoView> getViewers() { public @Nullable Set<UserInfoView> getViewers() {
return this.viewers; return this.viewers;
} }