feat: add scheduled task for cleaning up unused S3 images

This commit is contained in:
Rodrigo Verdiani 2025-10-27 16:12:33 -03:00
parent 6b2a77b391
commit 581f436c5f
5 changed files with 101 additions and 6 deletions

View File

@ -1,9 +1,8 @@
package com.magamochi.mangamochi.client; package com.magamochi.mangamochi.client;
import io.github.resilience4j.retry.annotation.Retry;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;

View File

@ -1,10 +1,9 @@
package com.magamochi.mangamochi.client; package com.magamochi.mangamochi.client;
import com.magamochi.mangamochi.model.dto.MangaDexMangaDTO; import com.magamochi.mangamochi.model.dto.MangaDexMangaDTO;
import io.github.resilience4j.retry.annotation.Retry;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;

View File

@ -1,14 +1,19 @@
package com.magamochi.mangamochi.service; package com.magamochi.mangamochi.service;
import static java.util.Objects.nonNull;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -34,4 +39,43 @@ public class S3Service {
return s3Client.getObject(request); return s3Client.getObject(request);
} }
public void deleteObjects(Set<String> objectKeys) {
if (CollectionUtils.isEmpty(objectKeys)) {
throw new IllegalArgumentException("Object key list cannot be null or empty");
}
var objectsToDelete =
objectKeys.stream().map(key -> ObjectIdentifier.builder().key(key).build()).toList();
var deleteRequest =
DeleteObjectsRequest.builder()
.bucket(bucket)
.delete(Delete.builder().objects(objectsToDelete).build())
.build();
s3Client.deleteObjects(deleteRequest);
}
public List<String> listAllObjectKeys() {
var keys = new ArrayList<String>();
String continuationToken = null;
do {
var requestBuilder = ListObjectsV2Request.builder().bucket(bucket).maxKeys(1000);
if (nonNull(continuationToken)) {
requestBuilder.continuationToken(continuationToken);
}
var response = s3Client.listObjectsV2(requestBuilder.build());
response.contents().forEach(s3Object -> keys.add(s3Object.key()));
continuationToken = response.isTruncated() ? response.nextContinuationToken() : null;
} while (nonNull(continuationToken));
return keys;
}
} }

View File

@ -0,0 +1,50 @@
package com.magamochi.mangamochi.task;
import com.magamochi.mangamochi.model.entity.Image;
import com.magamochi.mangamochi.model.repository.*;
import com.magamochi.mangamochi.service.S3Service;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Log4j2
@Component
@RequiredArgsConstructor
public class ImageCleanupTask {
@Value("${image-service.clean-up-enabled}")
private Boolean cleanUpEnabled;
private final S3Service s3Service;
private final ImageRepository imageRepository;
@Scheduled(cron = "@weekly")
public void cleanupImages() {
if (!cleanUpEnabled) {
log.info("S3 Image cleanup disabled.");
return;
}
log.info("Getting unused S3 object keys to remove.");
var imageKeys = s3Service.listAllObjectKeys();
var existingImages =
imageRepository.findAll().parallelStream()
.map(Image::getFileKey)
.collect(Collectors.toSet());
var keysToRemove =
imageKeys.parallelStream()
.filter(imageKey -> !existingImages.contains(imageKey))
.collect(Collectors.toSet());
log.info("Removing {} objects from S3 storage", keysToRemove.size());
s3Service.deleteObjects(keysToRemove);
log.info("Image cleanup finished.");
}
}

View File

@ -67,3 +67,6 @@ resilience4j:
seconds: 5 seconds: 5
retry-exceptions: retry-exceptions:
- feign.FeignException - feign.FeignException
image-service:
clean-up-enabled: ${IMAGE_SERVICE_CLEAN_UP_ENABLED:false}