forked from jsl/video_editing_poc
Assets and clips now carry expiration timestamps in the domain model:
- src/main/java/org/example/videoclips/domain/VideoAsset.java - src/main/java/org/example/videoclips/domain/Clip.java That metadata is mapped through the in-memory and JPA repository paths, including: - src/main/java/org/example/videoclips/infrastructure/InMemoryVideoAssetRepository.java - src/main/java/org/example/videoclips/persistence/JpaVideoClippingRepository.java - src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java - src/main/java/org/example/videoclips/persistence/entity/ClipEntity.java I also added scheduled retention cleanup in src/main/java/org/example/videoclips/operations/RetentionCleanupJob.java, with configuration in src/main/java/org/example/videoclips/config/ VideoClippingProperties.java and src/main/resources/application.properties. Right now it soft-expires assets by marking them deleted when their retention time passes. Clip retention timestamps are now assigned by both clipper adapters so the service has real expiration data to work with. What this does not do yet is physically delete expired clip objects or original source objects from object storage. The retention job is metadata-aware now, but object-store deletion still needs to be added through the storage port as the next step.
This commit is contained in:
parent
3d961f2adf
commit
8cb232d70e
|
|
@ -75,7 +75,8 @@ public class VideoAssetService {
|
|||
uploadId,
|
||||
null,
|
||||
now,
|
||||
null
|
||||
null,
|
||||
now.plus(properties.getCleanup().getSourceRetentionHours(), ChronoUnit.HOURS)
|
||||
);
|
||||
|
||||
repository.saveAsset(asset);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import org.example.videoclips.domain.JobEvent;
|
|||
import org.example.videoclips.domain.UploadSession;
|
||||
import org.example.videoclips.domain.VideoAsset;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -31,6 +32,10 @@ public interface VideoClippingRepository {
|
|||
|
||||
Clip findClipById(String clipId);
|
||||
|
||||
List<VideoAsset> findExpiredAssets(Instant cutoff);
|
||||
|
||||
List<Clip> findExpiredClips(Instant cutoff);
|
||||
|
||||
void saveJobEvent(JobEvent event);
|
||||
|
||||
List<JobEvent> findEventsByAggregateId(String aggregateId);
|
||||
|
|
|
|||
|
|
@ -229,6 +229,15 @@ public class VideoClippingProperties {
|
|||
@Min(1)
|
||||
private int localArtifactRetentionHours = 24;
|
||||
|
||||
@Min(30000)
|
||||
private long retentionPollIntervalMs = 3600000;
|
||||
|
||||
@Min(1)
|
||||
private int sourceRetentionHours = 168;
|
||||
|
||||
@Min(1)
|
||||
private int clipRetentionHours = 168;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
|
@ -252,5 +261,29 @@ public class VideoClippingProperties {
|
|||
public void setLocalArtifactRetentionHours(int localArtifactRetentionHours) {
|
||||
this.localArtifactRetentionHours = localArtifactRetentionHours;
|
||||
}
|
||||
|
||||
public long getRetentionPollIntervalMs() {
|
||||
return retentionPollIntervalMs;
|
||||
}
|
||||
|
||||
public void setRetentionPollIntervalMs(long retentionPollIntervalMs) {
|
||||
this.retentionPollIntervalMs = retentionPollIntervalMs;
|
||||
}
|
||||
|
||||
public int getSourceRetentionHours() {
|
||||
return sourceRetentionHours;
|
||||
}
|
||||
|
||||
public void setSourceRetentionHours(int sourceRetentionHours) {
|
||||
this.sourceRetentionHours = sourceRetentionHours;
|
||||
}
|
||||
|
||||
public int getClipRetentionHours() {
|
||||
return clipRetentionHours;
|
||||
}
|
||||
|
||||
public void setClipRetentionHours(int clipRetentionHours) {
|
||||
this.clipRetentionHours = clipRetentionHours;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public record Clip(
|
|||
long durationSeconds,
|
||||
String contentType,
|
||||
long contentLengthBytes,
|
||||
Instant createdAt
|
||||
Instant createdAt,
|
||||
Instant expiresAt
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ public record VideoAsset(
|
|||
String uploadId,
|
||||
Long sourceDurationSeconds,
|
||||
Instant createdAt,
|
||||
Instant uploadedAt
|
||||
Instant uploadedAt,
|
||||
Instant expiresAt
|
||||
) {
|
||||
public enum Status {
|
||||
PENDING_UPLOAD,
|
||||
|
|
@ -26,15 +27,20 @@ public record VideoAsset(
|
|||
|
||||
public VideoAsset withUploadCompleted(long durationSeconds, Instant uploadedAt) {
|
||||
return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, sourceObjectKey,
|
||||
Status.UPLOADED, uploadId, durationSeconds, createdAt, uploadedAt);
|
||||
Status.UPLOADED, uploadId, durationSeconds, createdAt, uploadedAt, expiresAt);
|
||||
}
|
||||
|
||||
public VideoAsset withStatus(Status status) {
|
||||
return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, sourceObjectKey,
|
||||
status, uploadId, sourceDurationSeconds, createdAt, uploadedAt);
|
||||
status, uploadId, sourceDurationSeconds, createdAt, uploadedAt, expiresAt);
|
||||
}
|
||||
|
||||
public VideoAsset withDeleted() {
|
||||
return withStatus(Status.DELETED);
|
||||
}
|
||||
|
||||
public VideoAsset withExpiresAt(Instant expiration) {
|
||||
return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, sourceObjectKey,
|
||||
status, uploadId, sourceDurationSeconds, createdAt, uploadedAt, expiration);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
|
@ -89,6 +90,28 @@ public class InMemoryVideoAssetRepository implements VideoClippingRepository {
|
|||
return clips.get(clipId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoAsset> findExpiredAssets(Instant cutoff) {
|
||||
List<VideoAsset> result = new ArrayList<>();
|
||||
for (VideoAsset asset : assets.values()) {
|
||||
if (asset.expiresAt() != null && asset.expiresAt().isBefore(cutoff) && asset.status() != VideoAsset.Status.DELETED) {
|
||||
result.add(asset);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Clip> findExpiredClips(Instant cutoff) {
|
||||
List<Clip> result = new ArrayList<>();
|
||||
for (Clip clip : clips.values()) {
|
||||
if (clip.expiresAt() != null && clip.expiresAt().isBefore(cutoff)) {
|
||||
result.add(clip);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveJobEvent(JobEvent event) {
|
||||
jobEvents.put(event.id(), event);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package org.example.videoclips.operations;
|
||||
|
||||
import org.example.videoclips.application.VideoClippingRepository;
|
||||
import org.example.videoclips.config.VideoClippingProperties;
|
||||
import org.example.videoclips.domain.Clip;
|
||||
import org.example.videoclips.domain.VideoAsset;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Component
|
||||
public class RetentionCleanupJob {
|
||||
|
||||
private final VideoClippingRepository repository;
|
||||
private final VideoClippingProperties properties;
|
||||
|
||||
public RetentionCleanupJob(VideoClippingRepository repository, VideoClippingProperties properties) {
|
||||
this.repository = repository;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${video-clipping.cleanup.retention-poll-interval-ms:3600000}")
|
||||
public void cleanupExpiredRecords() {
|
||||
if (!properties.getCleanup().isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Instant now = Instant.now();
|
||||
|
||||
for (Clip clip : repository.findExpiredClips(now)) {
|
||||
// Current retention behavior is metadata-only soft expiration for the scaffold.
|
||||
// Real object storage deletion should hang off the storage port once lifecycle policy is finalized.
|
||||
}
|
||||
|
||||
for (VideoAsset asset : repository.findExpiredAssets(now)) {
|
||||
repository.saveAsset(asset.withDeleted());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ public class JpaVideoClippingMapper {
|
|||
entity.setSourceDurationSeconds(asset.sourceDurationSeconds());
|
||||
entity.setCreatedAt(asset.createdAt());
|
||||
entity.setUploadedAt(asset.uploadedAt());
|
||||
entity.setExpiresAt(asset.expiresAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +48,8 @@ public class JpaVideoClippingMapper {
|
|||
entity.getUploadId(),
|
||||
entity.getSourceDurationSeconds(),
|
||||
entity.getCreatedAt(),
|
||||
entity.getUploadedAt()
|
||||
entity.getUploadedAt(),
|
||||
entity.getExpiresAt()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +123,7 @@ public class JpaVideoClippingMapper {
|
|||
entity.setContentType(clip.contentType());
|
||||
entity.setContentLengthBytes(clip.contentLengthBytes());
|
||||
entity.setCreatedAt(clip.createdAt());
|
||||
entity.setExpiresAt(clip.expiresAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +138,8 @@ public class JpaVideoClippingMapper {
|
|||
entity.getDurationSeconds(),
|
||||
entity.getContentType(),
|
||||
entity.getContentLengthBytes(),
|
||||
entity.getCreatedAt()
|
||||
entity.getCreatedAt(),
|
||||
entity.getExpiresAt()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,20 @@ public class JpaVideoClippingRepository implements VideoClippingRepository {
|
|||
return clipJpaRepository.findById(clipId).map(mapper::toDomain).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoAsset> findExpiredAssets(java.time.Instant cutoff) {
|
||||
return videoAssetJpaRepository.findByExpiresAtBeforeAndStatusNot(cutoff, "DELETED").stream()
|
||||
.map(mapper::toDomain)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Clip> findExpiredClips(java.time.Instant cutoff) {
|
||||
return clipJpaRepository.findByExpiresAtBefore(cutoff).stream()
|
||||
.map(mapper::toDomain)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveJobEvent(JobEvent event) {
|
||||
jobEventJpaRepository.save(mapper.toEntity(event));
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ public class ClipEntity {
|
|||
@Column(nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
private Instant expiresAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
@ -120,4 +122,12 @@ public class ClipEntity {
|
|||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Instant expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ public class VideoAssetEntity {
|
|||
|
||||
private Instant uploadedAt;
|
||||
|
||||
private Instant expiresAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
@ -140,4 +142,12 @@ public class VideoAssetEntity {
|
|||
public void setUploadedAt(Instant uploadedAt) {
|
||||
this.uploadedAt = uploadedAt;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Instant expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package org.example.videoclips.persistence.spring;
|
|||
import org.example.videoclips.persistence.entity.ClipEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public interface ClipJpaRepository extends JpaRepository<ClipEntity, String> {
|
||||
|
||||
List<ClipEntity> findByJobId(String jobId);
|
||||
|
||||
List<ClipEntity> findByExpiresAtBefore(Instant cutoff);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,10 @@ package org.example.videoclips.persistence.spring;
|
|||
import org.example.videoclips.persistence.entity.VideoAssetEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public interface VideoAssetJpaRepository extends JpaRepository<VideoAssetEntity, String> {
|
||||
|
||||
List<VideoAssetEntity> findByExpiresAtBeforeAndStatusNot(Instant cutoff, String status);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,8 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
|
|||
duration,
|
||||
"video/mp4",
|
||||
duration * bytesPerSecond,
|
||||
Instant.now()
|
||||
Instant.now(),
|
||||
Instant.now().plus(properties.getCleanup().getClipRetentionHours(), java.time.temporal.ChronoUnit.HOURS)
|
||||
), localPath));
|
||||
start += segmentDurationSeconds;
|
||||
index++;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.example.videoclips.processing;
|
|||
|
||||
import org.example.videoclips.domain.AccuracyMode;
|
||||
import org.example.videoclips.domain.Clip;
|
||||
import org.example.videoclips.config.VideoClippingProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
|
@ -17,6 +18,12 @@ import java.util.UUID;
|
|||
@ConditionalOnProperty(name = "video-clipping.processing", havingValue = "stub", matchIfMissing = true)
|
||||
public class StubVideoClipperAdapter implements VideoClipperPort {
|
||||
|
||||
private final VideoClippingProperties properties;
|
||||
|
||||
public StubVideoClipperAdapter(VideoClippingProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GeneratedClip> generateClips(
|
||||
String jobId,
|
||||
|
|
@ -51,7 +58,8 @@ public class StubVideoClipperAdapter implements VideoClipperPort {
|
|||
duration,
|
||||
"video/mp4",
|
||||
duration * bytesPerSecond,
|
||||
Instant.now()
|
||||
Instant.now(),
|
||||
Instant.now().plus(properties.getCleanup().getClipRetentionHours(), java.time.temporal.ChronoUnit.HOURS)
|
||||
), localPath));
|
||||
start += segmentDurationSeconds;
|
||||
index++;
|
||||
|
|
|
|||
|
|
@ -25,3 +25,6 @@ video-clipping.ffmpeg.cleanup-local-files=true
|
|||
video-clipping.cleanup.enabled=true
|
||||
video-clipping.cleanup.local-artifact-poll-interval-ms=300000
|
||||
video-clipping.cleanup.local-artifact-retention-hours=24
|
||||
video-clipping.cleanup.retention-poll-interval-ms=3600000
|
||||
video-clipping.cleanup.source-retention-hours=168
|
||||
video-clipping.cleanup.clip-retention-hours=168
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
stub-clip-content-clip_55da9cbe7120
|
||||
|
|
@ -0,0 +1 @@
|
|||
stub-clip-content-clip_4f379a7d5941
|
||||
|
|
@ -0,0 +1 @@
|
|||
stub-clip-content-clip_12f6257786bf
|
||||
Loading…
Reference in New Issue