3 Commits

Author SHA1 Message Date
40c7a6bae5 fix(api): remove detailed exception message from API response
All checks were successful
CI / Maven unit tests (push) Successful in 14s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped
2026-07-16 22:19:19 +01:00
76b30214ae fix(dirs): add defensive ownership check 2026-07-16 22:19:19 +01:00
a87987d9ce perf(media): add range request for media endpoints 2026-07-16 22:18:57 +01:00
5 changed files with 71 additions and 4 deletions

View File

@@ -6,12 +6,16 @@ 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;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
@RestController
@@ -78,19 +82,39 @@ public class ClipController {
}
@GetMapping("/{id}/media")
public ResponseEntity<Resource> downloadClip(@PathVariable Long id) {
public ResponseEntity<?> downloadClip(@PathVariable Long id, @RequestHeader HttpHeaders headers) throws IOException {
Resource resource = clipService.downloadClip(id);
if (resource == null || !resource.exists()) {
return ResponseEntity.notFound().build();
}
List<HttpRange> 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))
.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<Resource> downloadThumbnail(@PathVariable Long id) {
Resource resource = clipService.downloadThumbnail(id);

View File

@@ -30,7 +30,7 @@ public class MediaController {
public ResponseEntity<APIResponse<UUIDResponse>> compress(
@RequestParam("file")MultipartFile file,
@Valid @ModelAttribute ClipOptions options
) throws IOException, InterruptedException {
) throws IOException {
Job job = mediaService.compress(file, options);
return ResponseEntity.ok(new APIResponse<>(
SUCCESS,

View File

@@ -6,12 +6,16 @@ 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;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
@RestController
@@ -55,19 +59,39 @@ public class VodController {
}
@GetMapping("/{id}/media")
public ResponseEntity<Resource> downloadVod(@PathVariable Long id) {
public ResponseEntity<?> downloadVod(@PathVariable Long id, @RequestHeader HttpHeaders headers) throws IOException {
Resource resource = vodService.downloadVod(id);
if (resource == null || !resource.exists()) {
return ResponseEntity.notFound().build();
}
List<HttpRange> 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))
.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<Resource> downloadThumbnail(@PathVariable Long id) {
Resource resource = vodService.downloadThumbnail(id);

View File

@@ -10,6 +10,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import java.nio.file.StandardCopyOption;
@@ -129,11 +130,29 @@ public class DirectoryService {
}
}
private void checkDirPermissions() {
List<Path> dirs = List.of(
Path.of(tempInputsDir),
Path.of(tempOutputsDir),
Path.of(usersDir)
);
List<Path> bad = dirs.stream()
.filter(p -> !Files.isDirectory(p) || !Files.isReadable(p) || !Files.isWritable(p))
.toList();
if (!bad.isEmpty()) {
throw new IllegalStateException("Unusable directories: " + bad);
}
}
@PostConstruct
public void createDirectoriesIfNotExist() throws IOException {
Files.createDirectories(Path.of(tempInputsDir));
Files.createDirectories(Path.of(tempOutputsDir));
Files.createDirectories(Path.of(usersDir));
checkDirPermissions();
}
@Scheduled(fixedRate = TEMP_DIR_CLEANUP_RATE)

View File

@@ -93,7 +93,7 @@ public class MediaService {
);
}).exceptionally(ex -> {
job.setState(JobState.FAILED);
job.setErrorOutput(ex.getMessage());
job.setErrorOutput("Compression job failed");
logger.error("Compression job with UUID {} failed due to: {}", job.getUuid(), ex.getMessage());
return null;
});