Add final controller endpoints.

This commit is contained in:
Andrew Lalis 2023-04-05 15:02:03 +02:00
parent ffe1d9bd40
commit 55eb95e08a
8 changed files with 87 additions and 13 deletions

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package nl.andrewlalis.gymboard_api.domains.api.dto;
public record VideoProcessingCompletePayload(
long taskId,
String status,
String videoFileId,
String thumbnailFileId
) {}

View File

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

View File

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

View File

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

View File

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

View File

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