From 09cc841dfd6dec97f2a2eebc194677912d7b62bc Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Sat, 25 Jul 2026 10:42:00 +0100 Subject: [PATCH] feat(api): fully support range requests on backend --- .../vodsystem/controllers/ClipController.java | 34 ++++--------------- .../controllers/GlobalExceptionHandler.java | 14 ++++++++ .../vodsystem/controllers/VodController.java | 34 ++++--------------- .../exceptions/ClipMediaUnavailable.java | 18 ++++++++++ .../exceptions/VodMediaUnavailable.java | 18 ++++++++++ .../ddf/vodsystem/services/ClipService.java | 4 +++ .../ddf/vodsystem/services/VodService.java | 4 +++ 7 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java create mode 100644 src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java diff --git a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java index 68a3c70..88de8cb 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java @@ -6,10 +6,7 @@ import com.ddf.vodsystem.controllers.dto.ClipUpdateRequest; import com.ddf.vodsystem.entities.Clip; import com.ddf.vodsystem.services.ClipService; import org.springframework.core.io.Resource; -import org.springframework.core.io.support.ResourceRegion; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRange; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; import org.springframework.http.ResponseEntity; @@ -81,40 +78,23 @@ public class ClipController { ); } - @GetMapping("/{id}/media") - public ResponseEntity downloadClip(@PathVariable Long id, @RequestHeader HttpHeaders headers) throws IOException { + @GetMapping(value = "/{id}/media", produces = MediaType.ALL_VALUE) + public ResponseEntity downloadClip(@PathVariable Long id) throws IOException { Resource resource = clipService.downloadClip(id); if (resource == null || !resource.exists()) { return ResponseEntity.notFound().build(); } - List ranges = headers.getRange(); - - if (!ranges.isEmpty()) { - ResourceRegion region = createResourceRegion(resource, ranges.getFirst()); - - return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT) - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename())) - .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) - .body(region); - } - return ResponseEntity.ok() - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename())) - .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) + .header(HttpHeaders.CONTENT_DISPOSITION, + String.format(FILENAME_HEADER, resource.getFilename())) + .contentType(MediaTypeFactory.getMediaType(resource) + .orElse(MediaType.APPLICATION_OCTET_STREAM)) + .contentLength(resource.contentLength()) .body(resource); } - private ResourceRegion createResourceRegion(Resource resource, HttpRange range) throws IOException { - long contentLength = resource.contentLength(); - long start = range.getRangeStart(contentLength); - long end = range.getRangeEnd(contentLength); - return new ResourceRegion(resource, start, end - start + 1); - } - @GetMapping("/{id}/thumbnail") public ResponseEntity downloadThumbnail(@PathVariable Long id) { Resource resource = clipService.downloadThumbnail(id); diff --git a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java index a42e2e2..281e194 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java +++ b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java @@ -102,6 +102,13 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + @ExceptionHandler(ClipMediaUnavailable.class) + public ResponseEntity> handleClipMediaUnavailable(ClipMediaUnavailable ex) { + logger.error("ClipMediaUnavailable (clip {}): {}", ex.getClipId(), ex.getMessage()); + APIResponse response = new APIResponse<>(ERROR, "Clip media is currently unavailable", null); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response); + } + @ExceptionHandler(VodNotFound.class) public ResponseEntity> handleVodNotFound(VodNotFound ex) { logger.error("VodNotFound: {}", ex.getMessage()); @@ -109,6 +116,13 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + @ExceptionHandler(VodMediaUnavailable.class) + public ResponseEntity> handleVodMediaUnavailable(VodMediaUnavailable ex) { + logger.error("VodMediaUnavailable (vod {}): {}", ex.getVodId(), ex.getMessage()); + APIResponse response = new APIResponse<>(ERROR, "Vod media is currently unavailable", null); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response); + } + @ExceptionHandler(NotStreaming.class) public ResponseEntity> handleNotStreaming(NotStreaming ex) { logger.error("NotStreaming: {}", ex.getMessage()); diff --git a/src/main/java/com/ddf/vodsystem/controllers/VodController.java b/src/main/java/com/ddf/vodsystem/controllers/VodController.java index 460ffbd..60981fa 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/VodController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/VodController.java @@ -6,10 +6,7 @@ import com.ddf.vodsystem.dto.APIResponse; import com.ddf.vodsystem.entities.Vod; import com.ddf.vodsystem.services.VodService; import org.springframework.core.io.Resource; -import org.springframework.core.io.support.ResourceRegion; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRange; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; import org.springframework.http.ResponseEntity; @@ -58,40 +55,23 @@ public class VodController { "Vod with ID " + id + " has been deleted")); } - @GetMapping("/{id}/media") - public ResponseEntity downloadVod(@PathVariable Long id, @RequestHeader HttpHeaders headers) throws IOException { + @GetMapping(value = "/{id}/media", produces = MediaType.ALL_VALUE) + public ResponseEntity downloadVod(@PathVariable Long id) throws IOException { Resource resource = vodService.downloadVod(id); if (resource == null || !resource.exists()) { return ResponseEntity.notFound().build(); } - List ranges = headers.getRange(); - - if (!ranges.isEmpty()) { - ResourceRegion region = createResourceRegion(resource, ranges.getFirst()); - - return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT) - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename())) - .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) - .body(region); - } - return ResponseEntity.ok() - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename())) - .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) + .header(HttpHeaders.CONTENT_DISPOSITION, + String.format(FILENAME_HEADER, resource.getFilename())) + .contentType(MediaTypeFactory.getMediaType(resource) + .orElse(MediaType.APPLICATION_OCTET_STREAM)) + .contentLength(resource.contentLength()) .body(resource); } - private ResourceRegion createResourceRegion(Resource resource, HttpRange range) throws IOException { - long contentLength = resource.contentLength(); - long start = range.getRangeStart(contentLength); - long end = range.getRangeEnd(contentLength); - return new ResourceRegion(resource, start, end - start + 1); - } - @GetMapping("/{id}/thumbnail") public ResponseEntity downloadThumbnail(@PathVariable Long id) { Resource resource = vodService.downloadThumbnail(id); diff --git a/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java b/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java new file mode 100644 index 0000000..b850d2c --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java @@ -0,0 +1,18 @@ +package com.ddf.vodsystem.exceptions; + +public class ClipMediaUnavailable extends RuntimeException { + private final Long clipId; + + public ClipMediaUnavailable(Long clipId, String message) { + this(clipId, message, null); + } + + public ClipMediaUnavailable(Long clipId, String message, Throwable cause) { + super(message, cause); + this.clipId = clipId; + } + + public Long getClipId() { + return clipId; + } +} diff --git a/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java b/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java new file mode 100644 index 0000000..fb200fd --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java @@ -0,0 +1,18 @@ +package com.ddf.vodsystem.exceptions; + +public class VodMediaUnavailable extends RuntimeException { + private final Long vodId; + + public VodMediaUnavailable(Long vodId, String message) { + this(vodId, message, null); + } + + public VodMediaUnavailable(Long vodId, String message, Throwable cause) { + super(message, cause); + this.vodId = vodId; + } + + public Long getVodId() { + return vodId; + } +} diff --git a/src/main/java/com/ddf/vodsystem/services/ClipService.java b/src/main/java/com/ddf/vodsystem/services/ClipService.java index 763e382..9192e30 100644 --- a/src/main/java/com/ddf/vodsystem/services/ClipService.java +++ b/src/main/java/com/ddf/vodsystem/services/ClipService.java @@ -156,6 +156,10 @@ public class ClipService { throw new ClipNotFound("Clip file not found"); } + if (!Files.isReadable(file)) { + throw new ClipMediaUnavailable(id, "Clip file is not readable"); + } + return new FileSystemResource(file); } diff --git a/src/main/java/com/ddf/vodsystem/services/VodService.java b/src/main/java/com/ddf/vodsystem/services/VodService.java index 9e19947..a6dbb2c 100644 --- a/src/main/java/com/ddf/vodsystem/services/VodService.java +++ b/src/main/java/com/ddf/vodsystem/services/VodService.java @@ -139,6 +139,10 @@ public class VodService { throw new VodNotFound("Vod file not found for ID: " + id); } + if (!Files.isReadable(file)) { + throw new VodMediaUnavailable(id, "Vod file is not readable"); + } + return new FileSystemResource(file); }