s3 bucket integration for image storage

This commit is contained in:
Adrien
2026-04-04 13:26:55 +02:00
parent 5acfdd33c1
commit b154e29f2d
9 changed files with 195 additions and 91 deletions
@@ -0,0 +1,132 @@
package com.aiteacher.figure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.model.S3Object;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
public class S3FigureStorageService implements FigureStorageService {
private static final Logger log = LoggerFactory.getLogger(S3FigureStorageService.class);
private final S3Client s3;
private final S3Presigner presigner;
private final String bucket;
public S3FigureStorageService(
@Value("${app.figure-storage.endpoint}") String endpoint,
@Value("${app.figure-storage.region}") String region,
@Value("${app.figure-storage.bucket}") String bucket,
@Value("${app.figure-storage.access-key-id}") String accessKeyId,
@Value("${app.figure-storage.secret-access-key}") String secretKey) {
this.bucket = bucket;
URI endpointUri = URI.create(endpoint);
StaticCredentialsProvider credentials = StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretKey));
Region awsRegion = Region.of(region);
S3Configuration s3Config = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build();
this.s3 = S3Client.builder()
.endpointOverride(endpointUri)
.region(awsRegion)
.credentialsProvider(credentials)
.serviceConfiguration(s3Config)
.build();
this.presigner = S3Presigner.builder()
.endpointOverride(endpointUri)
.region(awsRegion)
.credentialsProvider(credentials)
.serviceConfiguration(s3Config)
.build();
}
@Override
public String save(UUID bookId, String figureId, BufferedImage image) {
String key = "figures/" + bookId + "/" + figureId + ".png";
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", out);
byte[] bytes = out.toByteArray();
s3.putObject(
PutObjectRequest.builder().bucket(bucket).key(key)
.contentType("image/png").contentLength((long) bytes.length).build(),
RequestBody.fromBytes(bytes));
return key;
} catch (IOException ex) {
throw new RuntimeException("Failed to encode figure " + figureId, ex);
} catch (S3Exception ex) {
throw new RuntimeException("Failed to upload figure " + figureId + " to S3", ex);
}
}
@Override
public byte[] getBytes(String key) {
try {
return s3.getObjectAsBytes(
GetObjectRequest.builder().bucket(bucket).key(key).build()).asByteArray();
} catch (S3Exception ex) {
throw new RuntimeException("Failed to download figure from S3: " + key, ex);
}
}
@Override
public String presignedUrl(String key) {
GetObjectPresignRequest request = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofHours(1))
.getObjectRequest(r -> r.bucket(bucket).key(key))
.build();
return presigner.presignGetObject(request).url().toString();
}
@Override
public void deleteAll(UUID bookId) {
String prefix = "figures/" + bookId + "/";
try {
List<ObjectIdentifier> toDelete = new ArrayList<>();
ListObjectsV2Request listRequest = ListObjectsV2Request.builder()
.bucket(bucket).prefix(prefix).build();
s3.listObjectsV2Paginator(listRequest).stream()
.flatMap(page -> page.contents().stream())
.map(S3Object::key)
.map(k -> ObjectIdentifier.builder().key(k).build())
.forEach(toDelete::add);
if (toDelete.isEmpty()) return;
s3.deleteObjects(DeleteObjectsRequest.builder()
.bucket(bucket)
.delete(Delete.builder().objects(toDelete).build())
.build());
log.info("Deleted {} figures from S3 for book {}", toDelete.size(), bookId);
} catch (S3Exception ex) {
log.warn("Could not fully delete figures for book {} from S3: {}", bookId, ex.getMessage());
}
}
}