From ae8595db0748a6802708b362fcb55c14cf44745e Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 7 Apr 2023 10:53:17 +0200 Subject: [PATCH] Add ServiceOnly annotation. --- .../config/ServiceAccessInterceptor.java | 39 ++++++++++++ .../gymboard_api/config/ServiceOnly.java | 16 +++++ .../gymboard_api/config/WebComponents.java | 4 +- .../api/service/cdn_client/CdnClient.java | 12 +++- .../controller/SubmissionController.java | 4 +- .../SampleSubmissionGenerator.java | 10 +--- .../application-development.properties | 2 + .../nl/andrewlalis/gymboardcdn/Config.java | 15 ++++- .../gymboardcdn/ServiceAccessInterceptor.java | 39 ++++++++++++ .../andrewlalis/gymboardcdn/ServiceOnly.java | 16 +++++ .../gymboardcdn/files/FileController.java | 4 +- .../uploads/api/UploadController.java | 3 +- .../VideoProcessingTaskStatusUpdate.java | 12 ++++ .../service/VideoProcessingService.java | 59 +++++++++++++------ .../application-development.properties | 6 +- 15 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceAccessInterceptor.java create mode 100644 gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceOnly.java create mode 100644 gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceAccessInterceptor.java create mode 100644 gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceOnly.java create mode 100644 gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskStatusUpdate.java diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceAccessInterceptor.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceAccessInterceptor.java new file mode 100644 index 0000000..d4dd351 --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceAccessInterceptor.java @@ -0,0 +1,39 @@ +package nl.andrewlalis.gymboard_api.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.lang.reflect.Method; + +/** + * An interceptor that checks that requests to endpoints annotated with + * {@link ServiceOnly} have a valid service secret header value. + */ +@Component +public class ServiceAccessInterceptor implements HandlerInterceptor { + public static final String HEADER_NAME = "X-Gymboard-Service-Secret"; + + @Value("${app.service-secret}") + private String serviceSecret; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + Method handlerMethod = ((HandlerMethod) handler).getMethod(); + Class handlerClass = handlerMethod.getDeclaringClass(); + + ServiceOnly methodAnnotation = handlerMethod.getAnnotation(ServiceOnly.class); + ServiceOnly classAnnotation = handlerClass.getAnnotation(ServiceOnly.class); + if (methodAnnotation != null || classAnnotation != null) { + String secret = request.getHeader(HEADER_NAME); + if (secret == null || !secret.trim().equals(serviceSecret)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + return true; + } +} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceOnly.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceOnly.java new file mode 100644 index 0000000..4ddd1ca --- /dev/null +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/ServiceOnly.java @@ -0,0 +1,16 @@ +package nl.andrewlalis.gymboard_api.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be applied to a controller or controller method to + * restrict access to only requests from another service that provide a + * legitimate service secret. + * @see ServiceAccessInterceptor + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ServiceOnly {} diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/WebComponents.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/WebComponents.java index 4ca698c..48562e7 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/WebComponents.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/WebComponents.java @@ -22,9 +22,11 @@ public class WebComponents { @Value("${app.cdn-origin}") private String cdnOrigin; + @Value("${app.cdn-secret}") + private String cdnSecret; @Bean public CdnClient cdnClient() { - return new CdnClient(cdnOrigin); + return new CdnClient(cdnOrigin, cdnSecret); } } 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 889a4d6..297f1a7 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 @@ -1,6 +1,7 @@ package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client; import com.fasterxml.jackson.databind.ObjectMapper; +import nl.andrewlalis.gymboard_api.config.ServiceAccessInterceptor; import java.io.IOException; import java.net.URI; @@ -13,17 +14,19 @@ import java.time.Duration; public class CdnClient { private final HttpClient httpClient; private final String baseUrl; + private final String cdnSecret; private final ObjectMapper objectMapper; public final UploadsClient uploads; public final FilesClient files; - public CdnClient(String baseUrl) { + public CdnClient(String baseUrl, String cdnSecret) { this.httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .followRedirects(HttpClient.Redirect.NORMAL) .build(); this.baseUrl = baseUrl; + this.cdnSecret = cdnSecret; this.objectMapper = new ObjectMapper(); this.uploads = new UploadsClient(this); this.files = new FilesClient(this); @@ -32,6 +35,7 @@ public class CdnClient { public T get(String urlPath, Class responseType) throws IOException, InterruptedException { HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath)) .GET() + .header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret) .build(); HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { @@ -48,6 +52,7 @@ public class CdnClient { .POST(HttpRequest.BodyPublishers.ofFile(filePath)) .header("Content-Type", contentType) .header("X-Gymboard-Filename", filePath.getFileName().toString()) + .header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret) .build(); HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); return objectMapper.readValue(response.body(), responseType); @@ -56,6 +61,7 @@ public class CdnClient { public void post(String urlPath) throws IOException, InterruptedException { HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath)) .POST(HttpRequest.BodyPublishers.noBody()) + .header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret) .build(); HttpResponse response = httpClient.send(req, HttpResponse.BodyHandlers.discarding()); if (response.statusCode() != 200) { @@ -65,7 +71,9 @@ public class CdnClient { public void delete(String urlPath) throws IOException, InterruptedException { HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath)) - .DELETE().build(); + .DELETE() + .header(ServiceAccessInterceptor.HEADER_NAME, cdnSecret) + .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/submission/controller/SubmissionController.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/submission/controller/SubmissionController.java index 09f3aaa..354d073 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/submission/controller/SubmissionController.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/domains/submission/controller/SubmissionController.java @@ -1,5 +1,6 @@ package nl.andrewlalis.gymboard_api.domains.submission.controller; +import nl.andrewlalis.gymboard_api.config.ServiceOnly; import nl.andrewlalis.gymboard_api.domains.submission.dto.SubmissionResponse; import nl.andrewlalis.gymboard_api.domains.api.dto.VideoProcessingCompletePayload; import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService; @@ -28,9 +29,8 @@ public class SubmissionController { return ResponseEntity.noContent().build(); } - @PostMapping(path = "/video-processing-complete") + @PostMapping(path = "/video-processing-complete") @ServiceOnly 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/util/sample_data/SampleSubmissionGenerator.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleSubmissionGenerator.java index dedd5e7..3d3de99 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleSubmissionGenerator.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/util/sample_data/SampleSubmissionGenerator.java @@ -15,7 +15,6 @@ import nl.andrewlalis.gymboard_api.domains.submission.model.SubmissionProperties import nl.andrewlalis.gymboard_api.util.ULID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; @@ -37,16 +36,15 @@ public class SampleSubmissionGenerator implements SampleDataGenerator { private final ExerciseRepository exerciseRepository; private final SubmissionRepository submissionRepository; private final ULID ulid; + private final CdnClient cdnClient; - @Value("${app.cdn-origin}") - private String cdnOrigin; - - public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, SubmissionRepository submissionRepository, ULID ulid) { + public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, SubmissionRepository submissionRepository, ULID ulid, CdnClient cdnClient) { this.gymRepository = gymRepository; this.userRepository = userRepository; this.exerciseRepository = exerciseRepository; this.submissionRepository = submissionRepository; this.ulid = ulid; + this.cdnClient = cdnClient; } @Override @@ -143,8 +141,6 @@ public class SampleSubmissionGenerator implements SampleDataGenerator { * @throws Exception If an error occurs. */ private Map> generateUploads() throws Exception { - final CdnClient cdnClient = new CdnClient(cdnOrigin); - List taskIds = new ArrayList<>(); taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_curl.mp4"), "video/mp4")); taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_ohp.mp4"), "video/mp4")); diff --git a/gymboard-api/src/main/resources/application-development.properties b/gymboard-api/src/main/resources/application-development.properties index 05850cf..43e16d6 100644 --- a/gymboard-api/src/main/resources/application-development.properties +++ b/gymboard-api/src/main/resources/application-development.properties @@ -15,7 +15,9 @@ spring.mail.protocol=smtp spring.mail.properties.mail.smtp.timeout=10000 app.auth.private-key-location=./private_key.der +app.service-secret=testing app.web-origin=http://localhost:9000 app.cdn-origin=http://localhost:8082 +app.cdn-secret=testing #logging.level.root=DEBUG 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 1258112..f646874 100644 --- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/Config.java +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/Config.java @@ -14,6 +14,8 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Arrays; import java.util.concurrent.Executor; @@ -21,12 +23,23 @@ import java.util.concurrent.Executors; @Configuration @EnableScheduling -public class Config { +public class Config implements WebMvcConfigurer { @Value("${app.web-origin}") private String webOrigin; @Value("${app.api-origin}") private String apiOrigin; + private final ServiceAccessInterceptor serviceAccessInterceptor; + + public Config(ServiceAccessInterceptor serviceAccessInterceptor) { + this.serviceAccessInterceptor = serviceAccessInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(serviceAccessInterceptor); + } + @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceAccessInterceptor.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceAccessInterceptor.java new file mode 100644 index 0000000..f82cfea --- /dev/null +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceAccessInterceptor.java @@ -0,0 +1,39 @@ +package nl.andrewlalis.gymboardcdn; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.lang.reflect.Method; + +/** + * An interceptor that checks that requests to endpoints annotated with + * {@link ServiceOnly} have a valid service secret header value. + */ +@Component +public class ServiceAccessInterceptor implements HandlerInterceptor { + private static final String HEADER_NAME = "X-Gymboard-Service-Secret"; + + @Value("${app.service-secret}") + private String serviceSecret; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + Method handlerMethod = ((HandlerMethod) handler).getMethod(); + Class handlerClass = handlerMethod.getDeclaringClass(); + + ServiceOnly methodAnnotation = handlerMethod.getAnnotation(ServiceOnly.class); + ServiceOnly classAnnotation = handlerClass.getAnnotation(ServiceOnly.class); + if (methodAnnotation != null || classAnnotation != null) { + String secret = request.getHeader(HEADER_NAME); + if (secret == null || !secret.trim().equals(serviceSecret)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + return true; + } +} diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceOnly.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceOnly.java new file mode 100644 index 0000000..769913a --- /dev/null +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/ServiceOnly.java @@ -0,0 +1,16 @@ +package nl.andrewlalis.gymboardcdn; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be applied to a controller or controller method to + * restrict access to only requests from another service that provide a + * legitimate service secret. + * @see ServiceAccessInterceptor + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ServiceOnly {} 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 69f0bb7..d329f1d 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 @@ -1,6 +1,7 @@ package nl.andrewlalis.gymboardcdn.files; import jakarta.servlet.http.HttpServletResponse; +import nl.andrewlalis.gymboardcdn.ServiceOnly; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -37,9 +38,8 @@ public class FileController { } } - @DeleteMapping(path = "/files/{id}") + @DeleteMapping(path = "/files/{id}") @ServiceOnly public void deleteFile(@PathVariable String id) { - // TODO: Secure this so only API can access it! try { fileStorageService.delete(id); } catch (IOException e) { diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java index c563277..d372f15 100644 --- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/api/UploadController.java @@ -1,6 +1,7 @@ package nl.andrewlalis.gymboardcdn.uploads.api; import jakarta.servlet.http.HttpServletRequest; +import nl.andrewlalis.gymboardcdn.ServiceOnly; import nl.andrewlalis.gymboardcdn.uploads.service.UploadService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -20,7 +21,7 @@ public class UploadController { return uploadService.processableVideoUpload(request); } - @PostMapping(path = "/uploads/video/{taskId}/start") + @PostMapping(path = "/uploads/video/{taskId}/start") @ServiceOnly public void startVideoProcessing(@PathVariable long taskId) { uploadService.startVideoProcessing(taskId); } diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskStatusUpdate.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskStatusUpdate.java new file mode 100644 index 0000000..9b52d01 --- /dev/null +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/model/VideoProcessingTaskStatusUpdate.java @@ -0,0 +1,12 @@ +package nl.andrewlalis.gymboardcdn.uploads.model; + +public record VideoProcessingTaskStatusUpdate( + long taskId, + String status, + String videoFileId, + String thumbnailFileId +) { + public VideoProcessingTaskStatusUpdate(VideoProcessingTask task) { + this(task.getId(), task.getStatus().name(), task.getVideoFileId(), task.getThumbnailFileId()); + } +} diff --git a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java index 4fe057d..18894e5 100644 --- a/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java +++ b/gymboard-cdn/src/main/java/nl/andrewlalis/gymboardcdn/uploads/service/VideoProcessingService.java @@ -2,11 +2,11 @@ package nl.andrewlalis.gymboardcdn.uploads.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; 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 nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskStatusUpdate; import nl.andrewlalis.gymboardcdn.uploads.service.process.ThumbnailGenerator; import nl.andrewlalis.gymboardcdn.uploads.service.process.VideoProcessor; import org.slf4j.Logger; @@ -42,6 +42,9 @@ public class VideoProcessingService { @Value("${app.api-origin}") private String apiOrigin; + @Value("${app.api-secret}") + private String apiSecret; + public VideoProcessingService(Executor videoProcessingExecutor, VideoProcessingTaskRepository taskRepo, FileStorageService fileStorageService, @@ -80,15 +83,18 @@ public class VideoProcessingService { for (var task : oldTasks) { if (task.getStatus() == VideoProcessingTask.Status.COMPLETED) { log.info("Deleting completed task {}.", task.getId()); + deleteAllTaskFiles(task); taskRepo.delete(task); } else if (task.getStatus() == VideoProcessingTask.Status.FAILED) { log.info("Deleting failed task {}.", task.getId()); taskRepo.delete(task); } else if (task.getStatus() == VideoProcessingTask.Status.IN_PROGRESS) { log.info("Task {} was in progress for too long; deleting.", task.getId()); + deleteAllTaskFiles(task); taskRepo.delete(task); } else if (task.getStatus() == VideoProcessingTask.Status.WAITING) { log.info("Task {} was waiting for too long; deleting.", task.getId()); + deleteAllTaskFiles(task); taskRepo.delete(task); } } @@ -156,32 +162,21 @@ public class VideoProcessingService { 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(rawUploadFile); - Files.deleteIfExists(videoFile); - Files.deleteIfExists(thumbnailFile); - } catch (IOException e) { - log.error("Couldn't delete temporary output files for uploaded video {}", uploadFile); - e.printStackTrace(); - } + deleteAllTaskFiles(task); } } /** * Sends an update message to the Gymboard API when a task finishes its - * processing. + * processing. Note that Gymboard API will also eventually poll the CDN's + * own API to get task status if we fail to send it, so there's some + * redundancy built-in. * @param task The task to send. */ private void sendTaskCompleteToApi(VideoProcessingTask task) { - ObjectNode obj = objectMapper.createObjectNode(); - obj.put("taskId", task.getId()); - obj.put("status", task.getStatus().name()); - obj.put("videoFileId", task.getVideoFileId()); - obj.put("thumbnailFileId", task.getThumbnailFileId()); String json; try { - json = objectMapper.writeValueAsString(obj); + json = objectMapper.writeValueAsString(new VideoProcessingTaskStatusUpdate(task)); } catch (JsonProcessingException e) { log.error("JSON error while sending task data to API for task " + task.getId(), e); return; @@ -189,6 +184,7 @@ public class VideoProcessingService { HttpClient httpClient = HttpClient.newBuilder().build(); HttpRequest request = HttpRequest.newBuilder(URI.create(apiOrigin + "/submissions/video-processing-complete")) .header("Content-Type", "application/json") + .header("X-Gymboard-Service-Secret", apiSecret) .timeout(Duration.ofSeconds(3)) .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); @@ -201,4 +197,33 @@ public class VideoProcessingService { log.error("Failed to send HTTP request to API.", e); } } + + /** + * Helper function to delete all temporary files related to a task's + * processing operations. If the task is FAILED, then files are kept for + * debugging purposes. + * @param task The task to delete files for. + */ + private void deleteAllTaskFiles(VideoProcessingTask task) { + if (task.getStatus() == VideoProcessingTask.Status.FAILED) { + log.warn("Retaining files for failed task {}, upload id {}.", task.getId(), task.getUploadFileId()); + return; + } + Path dir = fileStorageService.getStoragePathForFile(task.getUploadFileId()).getParent(); + try (var s = Files.list(dir)) { + var files = s.toList(); + for (var file : files) { + String filename = file.getFileName().toString().strip(); + if (Files.isRegularFile(file) && filename.startsWith(task.getUploadFileId())) { + try { + Files.delete(file); + } catch (IOException e) { + log.error("Failed to delete file " + file + " related to task " + task.getId(), e); + } + } + } + } catch (IOException e) { + log.error("Failed to list files in " + dir + " when deleting files for task " + task.getId(), e); + } + } } diff --git a/gymboard-cdn/src/main/resources/application-development.properties b/gymboard-cdn/src/main/resources/application-development.properties index 543e510..adac387 100644 --- a/gymboard-cdn/src/main/resources/application-development.properties +++ b/gymboard-cdn/src/main/resources/application-development.properties @@ -6,7 +6,9 @@ spring.jpa.hibernate.ddl-auto=update server.port=8082 +# A secret header token that other services must provide to use service-only endpoints. +app.service-secret=testing + app.web-origin=http://localhost:9000 app.api-origin=http://localhost:8080 -app.files.storage-dir=./cdn-files/ -app.files.temp-dir=./cdn-files/tmp/ +app.api-secret=testing