diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/controller/SubmissionController.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/controller/SubmissionController.java index 87cb30e..99ccca6 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/controller/SubmissionController.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/controller/SubmissionController.java @@ -1,6 +1,8 @@ package nl.andrewlalis.gymboard_api.domains.api.controller; import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse; +import nl.andrewlalis.gymboard_api.domains.api.dto.VideoProcessingCompletePayload; +import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.UploadsClient; import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService; import nl.andrewlalis.gymboard_api.domains.auth.model.User; import org.springframework.http.ResponseEntity; @@ -26,4 +28,11 @@ public class SubmissionController { submissionService.deleteSubmission(submissionId, user); return ResponseEntity.noContent().build(); } + + @PostMapping(path = "/video-processing-complete") + public ResponseEntity handleVideoProcessingComplete(@RequestBody VideoProcessingCompletePayload taskStatus) { + // TODO: Validate that the request came ONLY from the CDN service. + submissionService.handleVideoProcessingComplete(taskStatus); + return ResponseEntity.noContent().build(); + } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dao/submission/SubmissionRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dao/submission/SubmissionRepository.java index d21792b..af58280 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dao/submission/SubmissionRepository.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dao/submission/SubmissionRepository.java @@ -7,8 +7,12 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface SubmissionRepository extends JpaRepository, JpaSpecificationExecutor { @Modifying void deleteAllByUser(User user); + + List findAllByVideoProcessingTaskId(long taskId); } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/VideoProcessingCompletePayload.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/VideoProcessingCompletePayload.java new file mode 100644 index 0000000..7e991cf --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/dto/VideoProcessingCompletePayload.java @@ -0,0 +1,8 @@ +package nl.andrewlalis.gymboard_api.domains.api.dto; + +public record VideoProcessingCompletePayload( + long taskId, + String status, + String videoFileId, + String thumbnailFileId +) {} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java index 4208730..889a4d6 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/CdnClient.java @@ -16,6 +16,7 @@ public class CdnClient { private final ObjectMapper objectMapper; public final UploadsClient uploads; + public final FilesClient files; public CdnClient(String baseUrl) { this.httpClient = HttpClient.newBuilder() @@ -25,6 +26,7 @@ public class CdnClient { this.baseUrl = baseUrl; this.objectMapper = new ObjectMapper(); this.uploads = new UploadsClient(this); + this.files = new FilesClient(this); } public T get(String urlPath, Class responseType) throws IOException, InterruptedException { @@ -60,4 +62,13 @@ public class CdnClient { throw new IOException("Request failed with code " + response.statusCode()); } } + + public void delete(String urlPath) throws IOException, InterruptedException { + HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath)) + .DELETE().build(); + HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() >= 400) { + throw new IOException("Request failed with code " + response.statusCode()); + } + } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/FilesClient.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/FilesClient.java new file mode 100644 index 0000000..b0d0abc --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/FilesClient.java @@ -0,0 +1,18 @@ +package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client; + +public record FilesClient(CdnClient client) { + public record FileMetadataResponse( + String filename, + String mimeType, + long size, + String createdAt + ) {} + + public FileMetadataResponse getFileMetadata(String id) throws Exception { + return client.get("/files/" + id + "/metadata", FileMetadataResponse.class); + } + + public void deleteFile(String id) throws Exception { + client.delete("/files/" + id); + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java index 903d1fc..2b914c8 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/cdn_client/UploadsClient.java @@ -10,14 +10,6 @@ public record UploadsClient(CdnClient client) { String thumbnailFileId ) {} - public record FileMetadataResponse( - String filename, - String mimeType, - long size, - String uploadedAt, - boolean availableForDownload - ) {} - public long uploadVideo(Path filePath, String contentType) throws Exception { return client.postFile("/uploads/video", filePath, contentType, FileUploadResponse.class).taskId(); } @@ -26,10 +18,6 @@ public record UploadsClient(CdnClient client) { return client.get("/uploads/video/" + id + "/status", VideoProcessingTaskStatusResponse.class); } - public FileMetadataResponse getFileMetadata(String id) throws Exception { - return client.get("/files/" + id + "/metadata", FileMetadataResponse.class); - } - public void startTask(long taskId) throws Exception { client.post("/uploads/video/" + taskId + "/start"); } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java index 1df518e..302b9c0 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/api/service/submission/ExerciseSubmissionService.java @@ -141,7 +141,32 @@ public class ExerciseSubmissionService { if (!submission.getUser().getId().equals(user.getId())) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete other user's submission."); } - // TODO: Find a secure way to delete the associated video. + try { + + if (submission.getVideoFileId() != null) { + cdnClient.files.deleteFile(submission.getVideoFileId()); + } + if (submission.getThumbnailFileId() != null) { + cdnClient.files.deleteFile(submission.getThumbnailFileId()); + } + } catch (Exception e) { + log.error("Couldn't delete CDN content for submission " + submissionId, e); + } submissionRepository.delete(submission); } + + @Transactional + public void handleVideoProcessingComplete(VideoProcessingCompletePayload payload) { + for (var submission : submissionRepository.findAllByVideoProcessingTaskId(payload.taskId())) { + if (payload.status().equalsIgnoreCase("COMPLETE")) { + submission.setVideoFileId(payload.videoFileId()); + submission.setThumbnailFileId(payload.thumbnailFileId()); + submissionRepository.save(submission); + // TODO: Send notification of successful processing to the user! + } else if (payload.status().equalsIgnoreCase("FAILED")) { + submissionRepository.delete(submission); + // TODO: Send notification of failed video processing to the user! + } + } + } } diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java index 4eb9341..69f0bb7 100644 --- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/files/FileController.java @@ -2,6 +2,7 @@ package nl.andrewlalis.gymboardcdn.files; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -35,4 +36,14 @@ public class FileController { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Couldn't read file metadata.", e); } } + + @DeleteMapping(path = "/files/{id}") + public void deleteFile(@PathVariable String id) { + // TODO: Secure this so only API can access it! + try { + fileStorageService.delete(id); + } catch (IOException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete file.", e); + } + } }