Add final controller endpoints.
This commit is contained in:
parent
ffe1d9bd40
commit
55eb95e08a
|
@ -1,6 +1,8 @@
|
||||||
package nl.andrewlalis.gymboard_api.domains.api.controller;
|
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.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.api.service.submission.ExerciseSubmissionService;
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
@ -26,4 +28,11 @@ public class SubmissionController {
|
||||||
submissionService.deleteSubmission(submissionId, user);
|
submissionService.deleteSubmission(submissionId, user);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/video-processing-complete")
|
||||||
|
public ResponseEntity<Void> handleVideoProcessingComplete(@RequestBody VideoProcessingCompletePayload taskStatus) {
|
||||||
|
// TODO: Validate that the request came ONLY from the CDN service.
|
||||||
|
submissionService.handleVideoProcessingComplete(taskStatus);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,12 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface SubmissionRepository extends JpaRepository<Submission, String>, JpaSpecificationExecutor<Submission> {
|
public interface SubmissionRepository extends JpaRepository<Submission, String>, JpaSpecificationExecutor<Submission> {
|
||||||
@Modifying
|
@Modifying
|
||||||
void deleteAllByUser(User user);
|
void deleteAllByUser(User user);
|
||||||
|
|
||||||
|
List<Submission> findAllByVideoProcessingTaskId(long taskId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||||
|
|
||||||
|
public record VideoProcessingCompletePayload(
|
||||||
|
long taskId,
|
||||||
|
String status,
|
||||||
|
String videoFileId,
|
||||||
|
String thumbnailFileId
|
||||||
|
) {}
|
|
@ -16,6 +16,7 @@ public class CdnClient {
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public final UploadsClient uploads;
|
public final UploadsClient uploads;
|
||||||
|
public final FilesClient files;
|
||||||
|
|
||||||
public CdnClient(String baseUrl) {
|
public CdnClient(String baseUrl) {
|
||||||
this.httpClient = HttpClient.newBuilder()
|
this.httpClient = HttpClient.newBuilder()
|
||||||
|
@ -25,6 +26,7 @@ public class CdnClient {
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
this.uploads = new UploadsClient(this);
|
this.uploads = new UploadsClient(this);
|
||||||
|
this.files = new FilesClient(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T get(String urlPath, Class<T> responseType) throws IOException, InterruptedException {
|
public <T> T get(String urlPath, Class<T> responseType) throws IOException, InterruptedException {
|
||||||
|
@ -60,4 +62,13 @@ public class CdnClient {
|
||||||
throw new IOException("Request failed with code " + response.statusCode());
|
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<Void> response = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
|
||||||
|
if (response.statusCode() >= 400) {
|
||||||
|
throw new IOException("Request failed with code " + response.statusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,14 +10,6 @@ public record UploadsClient(CdnClient client) {
|
||||||
String thumbnailFileId
|
String thumbnailFileId
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public record FileMetadataResponse(
|
|
||||||
String filename,
|
|
||||||
String mimeType,
|
|
||||||
long size,
|
|
||||||
String uploadedAt,
|
|
||||||
boolean availableForDownload
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public long uploadVideo(Path filePath, String contentType) throws Exception {
|
public long uploadVideo(Path filePath, String contentType) throws Exception {
|
||||||
return client.postFile("/uploads/video", filePath, contentType, FileUploadResponse.class).taskId();
|
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);
|
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 {
|
public void startTask(long taskId) throws Exception {
|
||||||
client.post("/uploads/video/" + taskId + "/start");
|
client.post("/uploads/video/" + taskId + "/start");
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,32 @@ public class ExerciseSubmissionService {
|
||||||
if (!submission.getUser().getId().equals(user.getId())) {
|
if (!submission.getUser().getId().equals(user.getId())) {
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete other user's submission.");
|
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);
|
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!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package nl.andrewlalis.gymboardcdn.files;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.http.HttpStatus;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue