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.
This commit is contained in:
JSLMPR 2026-07-08 23:57:09 +02:00
parent 7e8a214c3e
commit 9df97362ba
11 changed files with 47 additions and 4 deletions

View File

@ -60,6 +60,7 @@ public class VideoAssetService {
String assetId = nextId("vid"); String assetId = nextId("vid");
String uploadId = nextId("upl"); String uploadId = nextId("upl");
String sourceObjectKey = "uploads/" + uploadId + "/source";
Instant now = Instant.now(); Instant now = Instant.now();
VideoAsset asset = new VideoAsset( VideoAsset asset = new VideoAsset(
@ -69,6 +70,7 @@ public class VideoAssetService {
request.contentLengthBytes(), request.contentLengthBytes(),
request.checksumSha256(), request.checksumSha256(),
request.clipProfile() == null || request.clipProfile().isBlank() ? "mp4-h264-aac" : request.clipProfile(), request.clipProfile() == null || request.clipProfile().isBlank() ? "mp4-h264-aac" : request.clipProfile(),
sourceObjectKey,
VideoAsset.Status.PENDING_UPLOAD, VideoAsset.Status.PENDING_UPLOAD,
uploadId, uploadId,
null, null,
@ -402,6 +404,7 @@ public class VideoAssetService {
response.put("contentType", asset.contentType()); response.put("contentType", asset.contentType());
response.put("contentLengthBytes", asset.contentLengthBytes()); response.put("contentLengthBytes", asset.contentLengthBytes());
response.put("clipProfile", asset.clipProfile()); response.put("clipProfile", asset.clipProfile());
response.put("sourceObjectKey", asset.sourceObjectKey());
response.put("status", asset.status().name()); response.put("status", asset.status().name());
response.put("uploadId", asset.uploadId()); response.put("uploadId", asset.uploadId());
response.put("sourceDurationSeconds", asset.sourceDurationSeconds()); response.put("sourceDurationSeconds", asset.sourceDurationSeconds());

View File

@ -9,6 +9,7 @@ public record VideoAsset(
long contentLengthBytes, long contentLengthBytes,
String checksumSha256, String checksumSha256,
String clipProfile, String clipProfile,
String sourceObjectKey,
Status status, Status status,
String uploadId, String uploadId,
Long sourceDurationSeconds, Long sourceDurationSeconds,
@ -24,12 +25,12 @@ public record VideoAsset(
} }
public VideoAsset withUploadCompleted(long durationSeconds, Instant uploadedAt) { 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); Status.UPLOADED, uploadId, durationSeconds, createdAt, uploadedAt);
} }
public VideoAsset withStatus(Status status) { 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); status, uploadId, sourceDurationSeconds, createdAt, uploadedAt);
} }

View File

@ -25,6 +25,7 @@ public class JpaVideoClippingMapper {
entity.setContentLengthBytes(asset.contentLengthBytes()); entity.setContentLengthBytes(asset.contentLengthBytes());
entity.setChecksumSha256(asset.checksumSha256()); entity.setChecksumSha256(asset.checksumSha256());
entity.setClipProfile(asset.clipProfile()); entity.setClipProfile(asset.clipProfile());
entity.setSourceObjectKey(asset.sourceObjectKey());
entity.setStatus(asset.status().name()); entity.setStatus(asset.status().name());
entity.setUploadId(asset.uploadId()); entity.setUploadId(asset.uploadId());
entity.setSourceDurationSeconds(asset.sourceDurationSeconds()); entity.setSourceDurationSeconds(asset.sourceDurationSeconds());
@ -41,6 +42,7 @@ public class JpaVideoClippingMapper {
entity.getContentLengthBytes(), entity.getContentLengthBytes(),
entity.getChecksumSha256(), entity.getChecksumSha256(),
entity.getClipProfile(), entity.getClipProfile(),
entity.getSourceObjectKey(),
VideoAsset.Status.valueOf(entity.getStatus()), VideoAsset.Status.valueOf(entity.getStatus()),
entity.getUploadId(), entity.getUploadId(),
entity.getSourceDurationSeconds(), entity.getSourceDurationSeconds(),

View File

@ -29,6 +29,9 @@ public class VideoAssetEntity {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String clipProfile; private String clipProfile;
@Column(nullable = false, length = 255)
private String sourceObjectKey;
@Column(nullable = false, length = 32) @Column(nullable = false, length = 32)
private String status; private String status;
@ -90,6 +93,14 @@ public class VideoAssetEntity {
this.clipProfile = clipProfile; this.clipProfile = clipProfile;
} }
public String getSourceObjectKey() {
return sourceObjectKey;
}
public void setSourceObjectKey(String sourceObjectKey) {
this.sourceObjectKey = sourceObjectKey;
}
public String getStatus() { public String getStatus() {
return status; return status;
} }

View File

@ -46,6 +46,7 @@ public class ClipProcessor {
List<Clip> clips = videoClipperPort.generateClips( List<Clip> clips = videoClipperPort.generateClips(
jobId, jobId,
asset.id(), asset.id(),
asset.sourceObjectKey(),
totalDuration, totalDuration,
segmentDuration, segmentDuration,
job.accuracyMode() job.accuracyMode()

View File

@ -28,11 +28,12 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
public List<Clip> generateClips( public List<Clip> generateClips(
String jobId, String jobId,
String assetId, String assetId,
String sourceObjectKey,
long sourceDurationSeconds, long sourceDurationSeconds,
int segmentDurationSeconds, int segmentDurationSeconds,
AccuracyMode accuracyMode AccuracyMode accuracyMode
) { ) {
Path inputFile = Path.of(properties.getFfmpeg().getInputDirectory(), assetId + ".mp4"); Path inputFile = resolveInputFile(sourceObjectKey);
Path outputDir = Path.of(properties.getFfmpeg().getOutputDirectory(), jobId); Path outputDir = Path.of(properties.getFfmpeg().getOutputDirectory(), jobId);
if (!Files.exists(inputFile)) { if (!Files.exists(inputFile)) {
throw new IllegalStateException("FFmpeg input file not found: " + 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); 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<String> buildCommand(Path inputFile, Path outputDir, int segmentDurationSeconds, AccuracyMode accuracyMode) { private List<String> buildCommand(Path inputFile, Path outputDir, int segmentDurationSeconds, AccuracyMode accuracyMode) {
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add(properties.getFfmpeg().getFfmpegBinary()); command.add(properties.getFfmpeg().getFfmpegBinary());

View File

@ -18,6 +18,7 @@ public class StubVideoClipperAdapter implements VideoClipperPort {
public List<Clip> generateClips( public List<Clip> generateClips(
String jobId, String jobId,
String assetId, String assetId,
String sourceObjectKey,
long sourceDurationSeconds, long sourceDurationSeconds,
int segmentDurationSeconds, int segmentDurationSeconds,
AccuracyMode accuracyMode AccuracyMode accuracyMode

View File

@ -7,5 +7,12 @@ import java.util.List;
public interface VideoClipperPort { public interface VideoClipperPort {
List<Clip> generateClips(String jobId, String assetId, long sourceDurationSeconds, int segmentDurationSeconds, AccuracyMode accuracyMode); List<Clip> generateClips(
String jobId,
String assetId,
String sourceObjectKey,
long sourceDurationSeconds,
int segmentDurationSeconds,
AccuracyMode accuracyMode
);
} }

View File

@ -5,6 +5,7 @@ create table if not exists video_assets (
content_length_bytes bigint not null, content_length_bytes bigint not null,
checksum_sha256 varchar(64), checksum_sha256 varchar(64),
clip_profile varchar(100) not null, clip_profile varchar(100) not null,
source_object_key varchar(255) not null,
status varchar(32) not null, status varchar(32) not null,
upload_id varchar(64) not null, upload_id varchar(64) not null,
source_duration_seconds bigint, source_duration_seconds bigint,

View File

@ -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;

View File

@ -65,6 +65,7 @@ class VideoAssetControllerTest {
mockMvc.perform(get("/v1/video-assets/{assetId}", assetId)) mockMvc.perform(get("/v1/video-assets/{assetId}", assetId))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.assetId").value(assetId)) .andExpect(jsonPath("$.assetId").value(assetId))
.andExpect(jsonPath("$.sourceObjectKey").value("uploads/" + uploadId + "/source"))
.andExpect(jsonPath("$.status").value("UPLOADED")); .andExpect(jsonPath("$.status").value("UPLOADED"));
mockMvc.perform(get("/v1/video-assets/{assetId}/upload-session", assetId)) mockMvc.perform(get("/v1/video-assets/{assetId}/upload-session", assetId))