diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/Config.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/Config.java
index 109afc1..05c3ced 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/Config.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/Config.java
@@ -1,7 +1,7 @@
package nl.andrewlalis.gymboardcdn;
-import nl.andrewlalis.gymboardcdn.service.FileStorageService;
-import nl.andrewlalis.gymboardcdn.util.ULID;
+import nl.andrewlalis.gymboardcdn.files.FileStorageService;
+import nl.andrewlalis.gymboardcdn.files.util.ULID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/StatusController.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/StatusController.java
similarity index 91%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/StatusController.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/StatusController.java
index 1925b6c..422ff91 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/StatusController.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/StatusController.java
@@ -1,4 +1,4 @@
-package nl.andrewlalis.gymboardcdn.api;
+package nl.andrewlalis.gymboardcdn;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileUploadResponse.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileUploadResponse.java
deleted file mode 100644
index 29ffb6b..0000000
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileUploadResponse.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package nl.andrewlalis.gymboardcdn.api;
-
-public record FileUploadResponse(
- String id
-) {}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileController.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java
similarity index 88%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileController.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java
index 2fc2d8d..4eb9341 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileController.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java
@@ -1,8 +1,6 @@
-package nl.andrewlalis.gymboardcdn.api;
+package nl.andrewlalis.gymboardcdn.files;
import jakarta.servlet.http.HttpServletResponse;
-import nl.andrewlalis.gymboardcdn.model.FullFileMetadata;
-import nl.andrewlalis.gymboardcdn.service.FileStorageService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileMetadata.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileMetadata.java
new file mode 100644
index 0000000..442e09b
--- /dev/null
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileMetadata.java
@@ -0,0 +1,7 @@
+package nl.andrewlalis.gymboardcdn.files;
+
+public record FileMetadata (
+ String filename,
+ String mimeType,
+ boolean accessible
+) {}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileMetadataResponse.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileMetadataResponse.java
similarity index 77%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileMetadataResponse.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileMetadataResponse.java
index 6846cfa..bff629c 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/FileMetadataResponse.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileMetadataResponse.java
@@ -1,4 +1,4 @@
-package nl.andrewlalis.gymboardcdn.api;
+package nl.andrewlalis.gymboardcdn.files;
public record FileMetadataResponse(
String filename,
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/FileStorageService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java
similarity index 95%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/FileStorageService.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java
index e2df559..1fac261 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/FileStorageService.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileStorageService.java
@@ -1,10 +1,8 @@
-package nl.andrewlalis.gymboardcdn.service;
+package nl.andrewlalis.gymboardcdn.files;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
-import nl.andrewlalis.gymboardcdn.model.FileMetadata;
-import nl.andrewlalis.gymboardcdn.model.FullFileMetadata;
-import nl.andrewlalis.gymboardcdn.util.ULID;
+import nl.andrewlalis.gymboardcdn.files.util.ULID;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
@@ -46,6 +44,10 @@ public class FileStorageService {
this.baseStorageDir = baseStorageDir;
}
+ public String generateFileId() {
+ return ulid.nextULID();
+ }
+
/**
* Saves a new file to the storage.
* @param in The input stream to the file contents.
@@ -106,8 +108,8 @@ public class FileStorageService {
FileMetadata metadata = readMetadata(in);
LocalDateTime date = dateFromULID(ULID.parseULID(rawId));
return new FullFileMetadata(
- metadata.filename,
- metadata.mimeType,
+ metadata.filename(),
+ metadata.mimeType(),
Files.size(filePath) - HEADER_SIZE,
date.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
);
@@ -131,7 +133,7 @@ public class FileStorageService {
try (var in = Files.newInputStream(filePath)) {
FileMetadata metadata = readMetadata(in);
- response.setContentType(metadata.mimeType);
+ response.setContentType(metadata.mimeType());
response.setContentLengthLong(Files.size(filePath) - HEADER_SIZE);
response.addHeader("Cache-Control", "max-age=604800, immutable");
var out = response.getOutputStream();
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/FullFileMetadata.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FullFileMetadata.java
similarity index 72%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/FullFileMetadata.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FullFileMetadata.java
index acb024f..5610ed9 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/FullFileMetadata.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FullFileMetadata.java
@@ -1,4 +1,4 @@
-package nl.andrewlalis.gymboardcdn.model;
+package nl.andrewlalis.gymboardcdn.files;
public record FullFileMetadata(
String filename,
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/util/ULID.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/util/ULID.java
similarity index 99%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/util/ULID.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/util/ULID.java
index 1819795..1adb435 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/util/ULID.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/util/ULID.java
@@ -1,4 +1,4 @@
-package nl.andrewlalis.gymboardcdn.util;
+package nl.andrewlalis.gymboardcdn.files.util;
/*
* sulky-modules - several general-purpose modules.
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/FileMetadata.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/FileMetadata.java
deleted file mode 100644
index fdef3ff..0000000
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/FileMetadata.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package nl.andrewlalis.gymboardcdn.model;
-
-public class FileMetadata {
- public String filename;
- public String mimeType;
-}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/VideoProcessingTask.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/VideoProcessingTask.java
deleted file mode 100644
index d6a7e79..0000000
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/VideoProcessingTask.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package nl.andrewlalis.gymboardcdn.model;
-
-import jakarta.persistence.*;
-import org.hibernate.annotations.CreationTimestamp;
-
-import java.time.LocalDateTime;
-
-/**
- * An entity to keep track of a task for processing a raw video into a better
- * format for Gymboard to serve.
- */
-@Entity
-@Table(name = "task_video_processing")
-public class VideoProcessingTask {
- public enum Status {
- WAITING,
- IN_PROGRESS,
- COMPLETED,
- FAILED
- }
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @CreationTimestamp
- private LocalDateTime createdAt;
-
- @Enumerated(EnumType.STRING)
- @Column(nullable = false)
- private Status status;
-
- @Column(nullable = false, updatable = false, length = 26)
- private String rawUploadFileId;
-
- public VideoProcessingTask() {}
-
- public VideoProcessingTask(Status status, String rawUploadFileId) {
- this.status = status;
- this.rawUploadFileId = rawUploadFileId;
- }
-
- public Long getId() {
- return id;
- }
-
- public LocalDateTime getCreatedAt() {
- return createdAt;
- }
-
- public Status getStatus() {
- return status;
- }
-
- public void setStatus(Status status) {
- this.status = status;
- }
-
- public String getRawUploadFileId() {
- return rawUploadFileId;
- }
-}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/UploadController.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java
similarity index 57%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/UploadController.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java
index eb5ade9..c563277 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/UploadController.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java
@@ -1,7 +1,7 @@
-package nl.andrewlalis.gymboardcdn.api;
+package nl.andrewlalis.gymboardcdn.uploads.api;
import jakarta.servlet.http.HttpServletRequest;
-import nl.andrewlalis.gymboardcdn.service.UploadService;
+import nl.andrewlalis.gymboardcdn.uploads.service.UploadService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -16,12 +16,17 @@ public class UploadController {
}
@PostMapping(path = "/uploads/video", consumes = {"video/mp4"})
- public FileUploadResponse uploadVideo(HttpServletRequest request) {
+ public VideoUploadResponse uploadVideo(HttpServletRequest request) {
return uploadService.processableVideoUpload(request);
}
- @GetMapping(path = "/uploads/video/{id}/status")
- public VideoProcessingTaskStatusResponse getVideoProcessingStatus(@PathVariable String id) {
- return uploadService.getVideoProcessingStatus(id);
+ @PostMapping(path = "/uploads/video/{taskId}/start")
+ public void startVideoProcessing(@PathVariable long taskId) {
+ uploadService.startVideoProcessing(taskId);
+ }
+
+ @GetMapping(path = "/uploads/video/{taskId}/status")
+ public VideoProcessingTaskStatusResponse getVideoProcessingStatus(@PathVariable long taskId) {
+ return uploadService.getVideoProcessingStatus(taskId);
}
}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/VideoProcessingTaskStatusResponse.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java
similarity index 59%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/VideoProcessingTaskStatusResponse.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java
index 49670d4..fdbfa74 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/api/VideoProcessingTaskStatusResponse.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoProcessingTaskStatusResponse.java
@@ -1,4 +1,4 @@
-package nl.andrewlalis.gymboardcdn.api;
+package nl.andrewlalis.gymboardcdn.uploads.api;
public record VideoProcessingTaskStatusResponse(
String status
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoUploadResponse.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoUploadResponse.java
new file mode 100644
index 0000000..9c0729f
--- /dev/null
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/VideoUploadResponse.java
@@ -0,0 +1,5 @@
+package nl.andrewlalis.gymboardcdn.uploads.api;
+
+public record VideoUploadResponse(
+ long taskId
+) {}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java
new file mode 100644
index 0000000..e4dba71
--- /dev/null
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTask.java
@@ -0,0 +1,87 @@
+package nl.andrewlalis.gymboardcdn.uploads.model;
+
+import jakarta.persistence.*;
+import org.hibernate.annotations.CreationTimestamp;
+
+import java.time.LocalDateTime;
+
+/**
+ * An entity to keep track of a task for processing a raw video into a better
+ * format for Gymboard to serve. Generally, tasks are processed like so:
+ *
+ * - A video is uploaded, and a new task is created with the NOT_STARTED status.
+ * - Once the Gymboard API verifies the associated submission, it'll
+ * request to start the task, bringing it to the WAITING status.
+ * - When a task executor picks up the waiting task, its status changes to IN_PROGRESS.
+ * - If the video is processed successfully, then the task is COMPLETED, otherwise FAILED.
+ *
+ */
+@Entity
+@Table(name = "task_video_processing")
+public class VideoProcessingTask {
+ public enum Status {
+ NOT_STARTED,
+ WAITING,
+ IN_PROGRESS,
+ COMPLETED,
+ FAILED
+ }
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @CreationTimestamp
+ private LocalDateTime createdAt;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Status status;
+
+ /**
+ * The file id for the original, raw user-uploaded video file that needs to
+ * be processed.
+ */
+ @Column(nullable = false, updatable = false, length = 26)
+ private String uploadFileId;
+
+ /**
+ * The file id for the final processed video file. This doesn't exist yet,
+ * but we generate the video id right away, just in case there's a need to
+ * preemptively link to it.
+ */
+ @Column(nullable = false, updatable = false, length = 26)
+ private String videoFileId;
+
+ public VideoProcessingTask() {}
+
+ public VideoProcessingTask(Status status, String uploadFileId, String videoFileId) {
+ this.status = status;
+ this.uploadFileId = uploadFileId;
+ this.videoFileId = videoFileId;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public String getUploadFileId() {
+ return uploadFileId;
+ }
+
+ public String getVideoFileId() {
+ return videoFileId;
+ }
+}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/VideoProcessingTaskRepository.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskRepository.java
similarity index 76%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/VideoProcessingTaskRepository.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskRepository.java
index 6c997d6..fc21541 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/model/VideoProcessingTaskRepository.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskRepository.java
@@ -1,16 +1,13 @@
-package nl.andrewlalis.gymboardcdn.model;
+package nl.andrewlalis.gymboardcdn.uploads.model;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
-import java.util.Optional;
@Repository
public interface VideoProcessingTaskRepository extends JpaRepository {
- Optional findByVideoIdentifier(String identifier);
-
List findAllByStatusOrderByCreatedAtDesc(VideoProcessingTask.Status status);
List findAllByCreatedAtBefore(LocalDateTime cutoff);
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/CommandFailedException.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/CommandFailedException.java
similarity index 94%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/CommandFailedException.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/CommandFailedException.java
index 9ef064b..bca62f2 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/CommandFailedException.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/CommandFailedException.java
@@ -1,4 +1,4 @@
-package nl.andrewlalis.gymboardcdn.service;
+package nl.andrewlalis.gymboardcdn.uploads.service;
import java.io.IOException;
import java.nio.file.Path;
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/UploadService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java
similarity index 51%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/UploadService.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java
index 387dbf4..312ba41 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/UploadService.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/UploadService.java
@@ -1,11 +1,12 @@
-package nl.andrewlalis.gymboardcdn.service;
+package nl.andrewlalis.gymboardcdn.uploads.service;
import jakarta.servlet.http.HttpServletRequest;
-import nl.andrewlalis.gymboardcdn.api.FileUploadResponse;
-import nl.andrewlalis.gymboardcdn.api.VideoProcessingTaskStatusResponse;
-import nl.andrewlalis.gymboardcdn.model.FileMetadata;
-import nl.andrewlalis.gymboardcdn.model.VideoProcessingTask;
-import nl.andrewlalis.gymboardcdn.model.VideoProcessingTaskRepository;
+import nl.andrewlalis.gymboardcdn.uploads.api.VideoUploadResponse;
+import nl.andrewlalis.gymboardcdn.uploads.api.VideoProcessingTaskStatusResponse;
+import nl.andrewlalis.gymboardcdn.files.FileMetadata;
+import nl.andrewlalis.gymboardcdn.files.FileStorageService;
+import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask;
+import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@@ -33,11 +34,12 @@ public class UploadService {
* Handles uploading of a processable video file that will be processed
* before being stored in the system.
* @param request The request from which we can read the file.
- * @return A response that contains an identifier that can be used to check
- * the status of the video processing, and eventually fetch the video.
+ * @return A response containing the id of the video processing task, to be
+ * given to the Gymboard API so that it can further manage processing after
+ * a submission is completed.
*/
@Transactional
- public FileUploadResponse processableVideoUpload(HttpServletRequest request) {
+ public VideoUploadResponse processableVideoUpload(HttpServletRequest request) {
String contentLengthStr = request.getHeader("Content-Length");
if (contentLengthStr == null || !contentLengthStr.matches("\\d+")) {
throw new ResponseStatusException(HttpStatus.LENGTH_REQUIRED);
@@ -46,34 +48,51 @@ public class UploadService {
if (contentLength > MAX_UPLOAD_SIZE_BYTES) {
throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE);
}
- FileMetadata metadata = new FileMetadata();
- metadata.mimeType = request.getContentType();
- metadata.filename = request.getHeader("X-Gymboard-Filename");
- if (metadata.filename == null) metadata.filename = "unnamed.mp4";
- String fileId;
+ String filename = request.getHeader("X-Gymboard-Filename");
+ if (filename == null) filename = "unnamed.mp4";
+ FileMetadata metadata = new FileMetadata(
+ filename,
+ request.getContentType(),
+ false
+ );
+ String uploadFileId;
try {
- fileId = fileStorageService.save(request.getInputStream(), metadata, contentLength);
+ uploadFileId = fileStorageService.save(request.getInputStream(), metadata, contentLength);
} catch (IOException e) {
log.error("Failed to save video upload to temp file.", e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
- videoTaskRepository.save(new VideoProcessingTask(
- VideoProcessingTask.Status.WAITING,
- fileId,
- "bleh"
+ var task = videoTaskRepository.save(new VideoProcessingTask(
+ VideoProcessingTask.Status.NOT_STARTED,
+ uploadFileId,
+ fileStorageService.generateFileId()
));
- return new FileUploadResponse("bleh");
+ return new VideoUploadResponse(task.getId());
}
/**
* Gets the status of a video processing task.
- * @param id The video identifier.
+ * @param id The task id.
* @return The status of the video processing task.
*/
@Transactional(readOnly = true)
- public VideoProcessingTaskStatusResponse getVideoProcessingStatus(String id) {
- VideoProcessingTask task = videoTaskRepository.findByVideoIdentifier(id)
+ public VideoProcessingTaskStatusResponse getVideoProcessingStatus(long id) {
+ VideoProcessingTask task = videoTaskRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return new VideoProcessingTaskStatusResponse(task.getStatus().name());
}
+
+ /**
+ * Marks this task as waiting to be picked up for processing. The Gymboard
+ * API should send a message itself to start processing of an uploaded video
+ * once it validates a submission.
+ * @param taskId The task id.
+ */
+ @Transactional
+ public void startVideoProcessing(long taskId) {
+ VideoProcessingTask task = videoTaskRepository.findById(taskId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ task.setStatus(VideoProcessingTask.Status.WAITING);
+ videoTaskRepository.save(task);
+ }
}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/VideoProcessingService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java
similarity index 73%
rename from gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/VideoProcessingService.java
rename to gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java
index 5e64960..f3587bb 100644
--- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/service/VideoProcessingService.java
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java
@@ -1,7 +1,10 @@
-package nl.andrewlalis.gymboardcdn.service;
+package nl.andrewlalis.gymboardcdn.uploads.service;
-import nl.andrewlalis.gymboardcdn.model.VideoProcessingTask;
-import nl.andrewlalis.gymboardcdn.model.VideoProcessingTaskRepository;
+import nl.andrewlalis.gymboardcdn.files.FileMetadata;
+import nl.andrewlalis.gymboardcdn.files.FileStorageService;
+import nl.andrewlalis.gymboardcdn.files.util.ULID;
+import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask;
+import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
@@ -65,7 +68,7 @@ public class VideoProcessingService {
private void processVideo(VideoProcessingTask task) {
log.info("Started processing task {}.", task.getId());
- Path tempFilePath = fileStorageService.getStoragePathForFile(task.getRawUploadFileId());
+ Path tempFilePath = fileStorageService.getStoragePathForFile(task.getUploadFileId());
if (Files.notExists(tempFilePath) || !Files.isReadable(tempFilePath)) {
log.error("Temp file {} doesn't exist or isn't readable.", tempFilePath);
updateTask(task, VideoProcessingTask.Status.FAILED);
@@ -74,19 +77,20 @@ public class VideoProcessingService {
// Then begin running the actual FFMPEG processing.
Path tempDir = tempFilePath.getParent();
- Files.createTempFile()
- Path ffmpegOutputFile = tempDir.resolve(task.getVideoIdentifier());
+ Path ffmpegOutputFile = tempDir.resolve(task.getUploadFileId() + "-video-out");
+ Path ffmpegThumbnailOutputFile = tempDir.resolve(task.getUploadFileId() + "-thumbnail-out");
try {
- processVideoWithFFMPEG(tempDir, tempFile, ffmpegOutputFile);
+ generateThumbnailWithFFMPEG(tempDir, tempFilePath, ffmpegThumbnailOutputFile);
+ processVideoWithFFMPEG(tempDir, tempFilePath, ffmpegOutputFile);
} catch (Exception e) {
e.printStackTrace();
log.error("""
- Video processing failed for video {}:
+ Video processing failed for task {}:
Input file: {}
Output file: {}
Exception message: {}""",
- task.getVideoIdentifier(),
- tempFile,
+ task.getId(),
+ tempFilePath,
ffmpegOutputFile,
e.getMessage()
);
@@ -95,24 +99,41 @@ public class VideoProcessingService {
}
// And finally, copy the output to the final location.
- try {
- StoredFile storedFile = new StoredFile(
- task.getVideoIdentifier(),
- task.getFilename(),
- "video/mp4",
- Files.size(ffmpegOutputFile),
- task.getCreatedAt()
+ try (
+ var videoIn = Files.newInputStream(ffmpegOutputFile);
+ var thumbnailIn = Files.newInputStream(ffmpegThumbnailOutputFile)
+ ) {
+ // Save the video to a final file location.
+ var originalMetadata = fileStorageService.getMetadata(task.getUploadFileId());
+ FileMetadata metadata = new FileMetadata(
+ originalMetadata.filename(),
+ originalMetadata.mimeType(),
+ true
);
- Path finalFilePath = fileService.getStoragePathForFile(storedFile);
- Files.move(ffmpegOutputFile, finalFilePath);
- Files.deleteIfExists(tempFile);
- Files.deleteIfExists(ffmpegOutputFile);
- storedFileRepository.saveAndFlush(storedFile);
+ fileStorageService.save(ULID.parseULID(task.getVideoFileId()), videoIn, metadata, Files.size(ffmpegOutputFile));
+ // Save the thumbnail too.
+ FileMetadata thumbnailMetadata = new FileMetadata(
+ "thumbnail.jpeg",
+ "image/jpeg",
+ true
+ );
+ fileStorageService.save(thumbnailIn, thumbnailMetadata, Files.size(ffmpegThumbnailOutputFile));
updateTask(task, VideoProcessingTask.Status.COMPLETED);
- log.info("Finished processing video {}.", task.getVideoIdentifier());
+ log.info("Finished processing task {}.", task.getId());
+
+ // TODO: Send HTTP POST to API, with video id and thumbnail id.
} catch (IOException e) {
log.error("Failed to copy processed video to final storage location.", e);
updateTask(task, VideoProcessingTask.Status.FAILED);
+ } finally {
+ try {
+ fileStorageService.delete(task.getUploadFileId());
+ Files.deleteIfExists(ffmpegOutputFile);
+ Files.deleteIfExists(ffmpegThumbnailOutputFile);
+ } catch (IOException e) {
+ log.error("Couldn't delete temporary FFMPEG output file: {}", ffmpegOutputFile);
+ e.printStackTrace();
+ }
}
}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/FfmpegVideoProcessor.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/FfmpegVideoProcessor.java
new file mode 100644
index 0000000..e8d1337
--- /dev/null
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/FfmpegVideoProcessor.java
@@ -0,0 +1,47 @@
+package nl.andrewlalis.gymboardcdn.uploads.service.process;
+
+import nl.andrewlalis.gymboardcdn.uploads.service.CommandFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+
+public class FfmpegVideoProcessor implements VideoProcessor {
+ private static final Logger log = LoggerFactory.getLogger(FfmpegVideoProcessor.class);
+
+ @Override
+ public void processVideo(Path inputFilePath, Path outputFilePath) throws IOException {
+ String inputFilename = inputFilePath.getFileName().toString().strip();
+ Path stdoutFile = inputFilePath.resolveSibling(inputFilename + "-ffmpeg-video-stdout.log");
+ Path stderrFile = inputFilePath.resolveSibling(inputFilename + "-ffmpeg-video-stderr.log");
+ final String[] command = {
+ "ffmpeg",
+ "-i", inputFilePath.toAbsolutePath().toString(),
+ "-vf", "scale=640x480:flags=lanczos",
+ "-vcodec", "libx264",
+ "-crf", "28",
+ "-f", "mp4",
+ outputFilePath.toAbsolutePath().toString()
+ };
+ long startFileSize = Files.size(inputFilePath);
+ Instant startTime = Instant.now();
+ Process process = new ProcessBuilder(command)
+ .redirectOutput(stdoutFile.toAbsolutePath().toFile())
+ .redirectError(stderrFile.toAbsolutePath().toFile())
+ .start();
+ int result = process.waitFor();
+ if (result != 0) {
+ throw new CommandFailedException(command, result, stdoutFile, stderrFile);
+ }
+ long endFileSize = Files.size(outputFilePath);
+ Duration duration = Duration.between(startTime, Instant.now());
+ double reductionFactor = startFileSize / (double) endFileSize;
+ String reductionFactorStr = String.format("%.3f%%", reductionFactor * 100);
+ log.info("Processed video from {} bytes to {} bytes in {} seconds, {} reduction.", startSize, endSize, dur.getSeconds(), reductionFactorStr);
+
+ }
+}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/ThumbnailGenerator.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/ThumbnailGenerator.java
new file mode 100644
index 0000000..4059336
--- /dev/null
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/ThumbnailGenerator.java
@@ -0,0 +1,7 @@
+package nl.andrewlalis.gymboardcdn.uploads.service.process;
+
+import java.nio.file.Path;
+
+public interface ThumbnailGenerator {
+ void generateThumbnailImage(Path videoInputFile, Path outputFilePath);
+}
diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/VideoProcessor.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/VideoProcessor.java
new file mode 100644
index 0000000..a8af275
--- /dev/null
+++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/process/VideoProcessor.java
@@ -0,0 +1,8 @@
+package nl.andrewlalis.gymboardcdn.uploads.service.process;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public interface VideoProcessor {
+ void processVideo(Path inputFilePath, Path outputFilePath) throws IOException;
+}
diff --git a/gymboard-cdn/src/test/java/nl/andrewlalis/gymboardcdn/service/UploadServiceTest.java b/gymboard-cdn/src/test/java/nl/andrewlalis/gymboardcdn/service/UploadServiceTest.java
index ff4c03b..6fe37c2 100644
--- a/gymboard-cdn/src/test/java/nl/andrewlalis/gymboardcdn/service/UploadServiceTest.java
+++ b/gymboard-cdn/src/test/java/nl/andrewlalis/gymboardcdn/service/UploadServiceTest.java
@@ -1,21 +1,11 @@
package nl.andrewlalis.gymboardcdn.service;
-import jakarta.servlet.ServletInputStream;
-import jakarta.servlet.http.HttpServletRequest;
-import nl.andrewlalis.gymboardcdn.api.FileUploadResponse;
-import nl.andrewlalis.gymboardcdn.model.VideoProcessingTask;
-import nl.andrewlalis.gymboardcdn.model.VideoProcessingTaskRepository;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
public class UploadServiceTest {
/**