The storage port now has a real staging operation:

- materializeSourceObject(String sourceObjectKey, Path targetPath) in src/main/java/org/example/videoclips/storage/ObjectStoragePort.java

  Both storage adapters implement it:

  - src/main/java/org/example/videoclips/storage/InMemoryObjectStorageAdapter.java creates a placeholder local file for the default stub flow
  - src/main/java/org/example/videoclips/storage/S3ObjectStorageAdapter.java downloads the object from S3-compatible storage to the worker filesystem

  The processor now uses that seam before clip generation starts. In src/main/java/org/example/videoclips/processing/ClipProcessor.java it resolves the local input path under the configured FFmpeg
  input directory, materializes the source object there, and records a SOURCE_MATERIALIZED job event before invoking the clipper.

  That means the FFmpeg path is no longer relying on a file appearing magically in the input directory. The next concrete step is to finish the output side the same way: persist generated clip
  object keys and push FFmpeg outputs back through the storage port instead of only creating clip metadata in memory.
This commit is contained in:
JSLMPR 2026-07-09 00:02:05 +02:00
parent 9df97362ba
commit bd620fc773
5 changed files with 52 additions and 1 deletions

View File

@ -1,14 +1,17 @@
package org.example.videoclips.processing;
import org.example.videoclips.config.VideoClippingProperties;
import org.example.videoclips.application.VideoClippingRepository;
import org.example.videoclips.domain.Clip;
import org.example.videoclips.domain.ClipJob;
import org.example.videoclips.domain.ClipJobStatus;
import org.example.videoclips.domain.VideoAsset;
import org.example.videoclips.domain.JobEvent;
import org.example.videoclips.storage.ObjectStoragePort;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
@ -17,10 +20,19 @@ public class ClipProcessor {
private final VideoClippingRepository repository;
private final VideoClipperPort videoClipperPort;
private final ObjectStoragePort objectStoragePort;
private final VideoClippingProperties properties;
public ClipProcessor(VideoClippingRepository repository, VideoClipperPort videoClipperPort) {
public ClipProcessor(
VideoClippingRepository repository,
VideoClipperPort videoClipperPort,
ObjectStoragePort objectStoragePort,
VideoClippingProperties properties
) {
this.repository = repository;
this.videoClipperPort = videoClipperPort;
this.objectStoragePort = objectStoragePort;
this.properties = properties;
}
@Async
@ -43,6 +55,9 @@ public class ClipProcessor {
recordEvent(jobId, "CANCELLED", "Job was cancelled before clip generation");
return;
}
Path localInputPath = Path.of(properties.getFfmpeg().getInputDirectory()).resolve(asset.sourceObjectKey());
objectStoragePort.materializeSourceObject(asset.sourceObjectKey(), localInputPath);
recordEvent(jobId, "SOURCE_MATERIALIZED", "Source object staged to " + localInputPath);
List<Clip> clips = videoClipperPort.generateClips(
jobId,
asset.id(),

View File

@ -3,6 +3,9 @@ package org.example.videoclips.storage;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
@ -26,6 +29,18 @@ public class InMemoryObjectStorageAdapter implements ObjectStoragePort {
// No-op for the in-memory adapter.
}
@Override
public void materializeSourceObject(String sourceObjectKey, Path targetPath) {
try {
Files.createDirectories(targetPath.getParent());
if (!Files.exists(targetPath)) {
Files.writeString(targetPath, "stub-video-content");
}
} catch (IOException ex) {
throw new IllegalStateException("Unable to materialize in-memory source object", ex);
}
}
@Override
public String createClipDownloadUrl(String clipId, Instant expiresAt) {
return "https://storage.example/clips/" + clipId + "?expiresAt=" + expiresAt.toString() + "&signature=demo";

View File

@ -1,5 +1,6 @@
package org.example.videoclips.storage;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
@ -9,6 +10,8 @@ public interface ObjectStoragePort {
void completeMultipartUpload(String providerUploadId, String uploadId, List<CompletedUploadPart> parts);
void materializeSourceObject(String sourceObjectKey, Path targetPath);
String createClipDownloadUrl(String clipId, Instant expiresAt);
record UploadPartDescriptor(int partNumber, String url) {

View File

@ -4,6 +4,7 @@ import org.example.videoclips.config.VideoClippingProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
@ -18,6 +19,8 @@ import software.amazon.awssdk.services.s3.presigner.model.PresignedUploadPartReq
import software.amazon.awssdk.services.s3.presigner.model.UploadPartPresignRequest;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@ -87,6 +90,20 @@ public class S3ObjectStorageAdapter implements ObjectStoragePort {
.build());
}
@Override
public void materializeSourceObject(String sourceObjectKey, Path targetPath) {
try {
Files.createDirectories(targetPath.getParent());
} catch (java.io.IOException ex) {
throw new IllegalStateException("Unable to create local source-object directory", ex);
}
s3Client.getObject(GetObjectRequest.builder()
.bucket(properties.getS3().getBucket())
.key(sourceObjectKey)
.build(),
ResponseTransformer.toFile(targetPath));
}
@Override
public String createClipDownloadUrl(String clipId, Instant expiresAt) {
String objectKey = "clips/" + clipId + ".mp4";

View File

@ -0,0 +1 @@
stub-video-content