From 9df97362baaae1fc15064be3adcd8b7e0d5683d8 Mon Sep 17 00:00:00 2001 From: JSLMPR Date: Wed, 8 Jul 2026 23:57:09 +0200 Subject: [PATCH] The core change is that VideoAsset now carries sourceObjectKey, and that value is persisted through the JPA entity and migrations: - src/main/java/org/example/videoclips/domain/VideoAsset.java - src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java - src/main/java/org/example/videoclips/persistence/JpaVideoClippingMapper.java - src/main/resources/db/migration/V4__video_asset_source_object_key.sql createAsset(...) now assigns a stable source object key of the form uploads/{uploadId}/source, and that value is exposed back through the asset read response in src/main/java/org/example/ videoclips/application/VideoAssetService.java. I also threaded that source-object reference through the clipper contract in src/main/java/org/example/videoclips/processing/VideoClipperPort.java, so src/main/java/org/example/videoclips/processing/ClipProcessor.java passes a real persisted object reference into the processing adapter. The FFmpeg adapter now resolves its input path from that source object key instead of assuming {assetId}.mp4. I also completed the multipart upload seam: - UploadSession now stores providerUploadId - the storage port gained completeMultipartUpload(...) - the service now calls storage completion during uploads:complete - the new metadata is migrated in src/main/resources/db/migration/V3__upload_session_provider_metadata.sql Finally, I extended the MockMvc test to assert that the asset read response now includes the expected sourceObjectKey in src/test/java/org/example/videoclips/api/VideoAssetControllerTest.java. --- .../videoclips/application/VideoAssetService.java | 3 +++ .../org/example/videoclips/domain/VideoAsset.java | 5 +++-- .../persistence/JpaVideoClippingMapper.java | 2 ++ .../persistence/entity/VideoAssetEntity.java | 11 +++++++++++ .../example/videoclips/processing/ClipProcessor.java | 1 + .../processing/FfmpegVideoClipperAdapter.java | 8 +++++++- .../processing/StubVideoClipperAdapter.java | 1 + .../videoclips/processing/VideoClipperPort.java | 9 ++++++++- .../db/migration/V1__init_video_clipping.sql | 1 + .../migration/V4__video_asset_source_object_key.sql | 9 +++++++++ .../videoclips/api/VideoAssetControllerTest.java | 1 + 11 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/db/migration/V4__video_asset_source_object_key.sql diff --git a/src/main/java/org/example/videoclips/application/VideoAssetService.java b/src/main/java/org/example/videoclips/application/VideoAssetService.java index 00ea9ac..026381f 100644 --- a/src/main/java/org/example/videoclips/application/VideoAssetService.java +++ b/src/main/java/org/example/videoclips/application/VideoAssetService.java @@ -60,6 +60,7 @@ public class VideoAssetService { String assetId = nextId("vid"); String uploadId = nextId("upl"); + String sourceObjectKey = "uploads/" + uploadId + "/source"; Instant now = Instant.now(); VideoAsset asset = new VideoAsset( @@ -69,6 +70,7 @@ public class VideoAssetService { request.contentLengthBytes(), request.checksumSha256(), request.clipProfile() == null || request.clipProfile().isBlank() ? "mp4-h264-aac" : request.clipProfile(), + sourceObjectKey, VideoAsset.Status.PENDING_UPLOAD, uploadId, null, @@ -402,6 +404,7 @@ public class VideoAssetService { response.put("contentType", asset.contentType()); response.put("contentLengthBytes", asset.contentLengthBytes()); response.put("clipProfile", asset.clipProfile()); + response.put("sourceObjectKey", asset.sourceObjectKey()); response.put("status", asset.status().name()); response.put("uploadId", asset.uploadId()); response.put("sourceDurationSeconds", asset.sourceDurationSeconds()); diff --git a/src/main/java/org/example/videoclips/domain/VideoAsset.java b/src/main/java/org/example/videoclips/domain/VideoAsset.java index 13dc30f..3243059 100644 --- a/src/main/java/org/example/videoclips/domain/VideoAsset.java +++ b/src/main/java/org/example/videoclips/domain/VideoAsset.java @@ -9,6 +9,7 @@ public record VideoAsset( long contentLengthBytes, String checksumSha256, String clipProfile, + String sourceObjectKey, Status status, String uploadId, Long sourceDurationSeconds, @@ -24,12 +25,12 @@ public record VideoAsset( } public VideoAsset withUploadCompleted(long durationSeconds, Instant uploadedAt) { - return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, + return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, sourceObjectKey, Status.UPLOADED, uploadId, durationSeconds, createdAt, uploadedAt); } public VideoAsset withStatus(Status status) { - return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, + return new VideoAsset(id, fileName, contentType, contentLengthBytes, checksumSha256, clipProfile, sourceObjectKey, status, uploadId, sourceDurationSeconds, createdAt, uploadedAt); } diff --git a/src/main/java/org/example/videoclips/persistence/JpaVideoClippingMapper.java b/src/main/java/org/example/videoclips/persistence/JpaVideoClippingMapper.java index c7948c6..e61e228 100644 --- a/src/main/java/org/example/videoclips/persistence/JpaVideoClippingMapper.java +++ b/src/main/java/org/example/videoclips/persistence/JpaVideoClippingMapper.java @@ -25,6 +25,7 @@ public class JpaVideoClippingMapper { entity.setContentLengthBytes(asset.contentLengthBytes()); entity.setChecksumSha256(asset.checksumSha256()); entity.setClipProfile(asset.clipProfile()); + entity.setSourceObjectKey(asset.sourceObjectKey()); entity.setStatus(asset.status().name()); entity.setUploadId(asset.uploadId()); entity.setSourceDurationSeconds(asset.sourceDurationSeconds()); @@ -41,6 +42,7 @@ public class JpaVideoClippingMapper { entity.getContentLengthBytes(), entity.getChecksumSha256(), entity.getClipProfile(), + entity.getSourceObjectKey(), VideoAsset.Status.valueOf(entity.getStatus()), entity.getUploadId(), entity.getSourceDurationSeconds(), diff --git a/src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java b/src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java index 20c4059..7cb63b2 100644 --- a/src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java +++ b/src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java @@ -29,6 +29,9 @@ public class VideoAssetEntity { @Column(nullable = false, length = 100) private String clipProfile; + @Column(nullable = false, length = 255) + private String sourceObjectKey; + @Column(nullable = false, length = 32) private String status; @@ -90,6 +93,14 @@ public class VideoAssetEntity { this.clipProfile = clipProfile; } + public String getSourceObjectKey() { + return sourceObjectKey; + } + + public void setSourceObjectKey(String sourceObjectKey) { + this.sourceObjectKey = sourceObjectKey; + } + public String getStatus() { return status; } diff --git a/src/main/java/org/example/videoclips/processing/ClipProcessor.java b/src/main/java/org/example/videoclips/processing/ClipProcessor.java index 18d74f1..4ae7b96 100644 --- a/src/main/java/org/example/videoclips/processing/ClipProcessor.java +++ b/src/main/java/org/example/videoclips/processing/ClipProcessor.java @@ -46,6 +46,7 @@ public class ClipProcessor { List clips = videoClipperPort.generateClips( jobId, asset.id(), + asset.sourceObjectKey(), totalDuration, segmentDuration, job.accuracyMode() diff --git a/src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java b/src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java index c64f64d..0932798 100644 --- a/src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java +++ b/src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java @@ -28,11 +28,12 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort { public List generateClips( String jobId, String assetId, + String sourceObjectKey, long sourceDurationSeconds, int segmentDurationSeconds, AccuracyMode accuracyMode ) { - Path inputFile = Path.of(properties.getFfmpeg().getInputDirectory(), assetId + ".mp4"); + Path inputFile = resolveInputFile(sourceObjectKey); Path outputDir = Path.of(properties.getFfmpeg().getOutputDirectory(), jobId); if (!Files.exists(inputFile)) { throw new IllegalStateException("FFmpeg input file not found: " + inputFile); @@ -61,6 +62,11 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort { return buildClipManifest(jobId, assetId, sourceDurationSeconds, segmentDurationSeconds, accuracyMode); } + private Path resolveInputFile(String sourceObjectKey) { + String normalized = sourceObjectKey.startsWith("/") ? sourceObjectKey.substring(1) : sourceObjectKey; + return Path.of(properties.getFfmpeg().getInputDirectory()).resolve(normalized); + } + private List buildCommand(Path inputFile, Path outputDir, int segmentDurationSeconds, AccuracyMode accuracyMode) { List command = new ArrayList<>(); command.add(properties.getFfmpeg().getFfmpegBinary()); diff --git a/src/main/java/org/example/videoclips/processing/StubVideoClipperAdapter.java b/src/main/java/org/example/videoclips/processing/StubVideoClipperAdapter.java index 6561a7f..68ea506 100644 --- a/src/main/java/org/example/videoclips/processing/StubVideoClipperAdapter.java +++ b/src/main/java/org/example/videoclips/processing/StubVideoClipperAdapter.java @@ -18,6 +18,7 @@ public class StubVideoClipperAdapter implements VideoClipperPort { public List generateClips( String jobId, String assetId, + String sourceObjectKey, long sourceDurationSeconds, int segmentDurationSeconds, AccuracyMode accuracyMode diff --git a/src/main/java/org/example/videoclips/processing/VideoClipperPort.java b/src/main/java/org/example/videoclips/processing/VideoClipperPort.java index 981c58d..1b82ad6 100644 --- a/src/main/java/org/example/videoclips/processing/VideoClipperPort.java +++ b/src/main/java/org/example/videoclips/processing/VideoClipperPort.java @@ -7,5 +7,12 @@ import java.util.List; public interface VideoClipperPort { - List generateClips(String jobId, String assetId, long sourceDurationSeconds, int segmentDurationSeconds, AccuracyMode accuracyMode); + List generateClips( + String jobId, + String assetId, + String sourceObjectKey, + long sourceDurationSeconds, + int segmentDurationSeconds, + AccuracyMode accuracyMode + ); } diff --git a/src/main/resources/db/migration/V1__init_video_clipping.sql b/src/main/resources/db/migration/V1__init_video_clipping.sql index 2d60046..923b7d7 100644 --- a/src/main/resources/db/migration/V1__init_video_clipping.sql +++ b/src/main/resources/db/migration/V1__init_video_clipping.sql @@ -5,6 +5,7 @@ create table if not exists video_assets ( content_length_bytes bigint not null, checksum_sha256 varchar(64), clip_profile varchar(100) not null, + source_object_key varchar(255) not null, status varchar(32) not null, upload_id varchar(64) not null, source_duration_seconds bigint, diff --git a/src/main/resources/db/migration/V4__video_asset_source_object_key.sql b/src/main/resources/db/migration/V4__video_asset_source_object_key.sql new file mode 100644 index 0000000..f421a5b --- /dev/null +++ b/src/main/resources/db/migration/V4__video_asset_source_object_key.sql @@ -0,0 +1,9 @@ +alter table video_assets + add column if not exists source_object_key varchar(255); + +update video_assets +set source_object_key = 'uploads/' || upload_id || '/source' +where source_object_key is null; + +alter table video_assets + alter column source_object_key set not null; diff --git a/src/test/java/org/example/videoclips/api/VideoAssetControllerTest.java b/src/test/java/org/example/videoclips/api/VideoAssetControllerTest.java index 0699636..527962d 100644 --- a/src/test/java/org/example/videoclips/api/VideoAssetControllerTest.java +++ b/src/test/java/org/example/videoclips/api/VideoAssetControllerTest.java @@ -65,6 +65,7 @@ class VideoAssetControllerTest { mockMvc.perform(get("/v1/video-assets/{assetId}", assetId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.assetId").value(assetId)) + .andExpect(jsonPath("$.sourceObjectKey").value("uploads/" + uploadId + "/source")) .andExpect(jsonPath("$.status").value("UPLOADED")); mockMvc.perform(get("/v1/video-assets/{assetId}/upload-session", assetId))