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 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());

View File

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

View File

@ -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(),

View File

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

View File

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

View File

@ -28,11 +28,12 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
public List<Clip> 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<String> buildCommand(Path inputFile, Path outputDir, int segmentDurationSeconds, AccuracyMode accuracyMode) {
List<String> command = new ArrayList<>();
command.add(properties.getFfmpeg().getFfmpegBinary());

View File

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

View File

@ -7,5 +7,12 @@ import java.util.List;
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,
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,

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))
.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))