3 Commits

Author SHA1 Message Date
d189578eda fix(api): remove detailed exception message from API response
All checks were successful
CI / Maven unit tests (push) Successful in 54s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped
2026-07-16 22:17:36 +01:00
43f62fa6f1 fix(dirs): add defensive ownership check 2026-07-16 21:30:58 +01:00
1a3d341ace wip 2026-07-16 21:17:07 +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.entities.Clip;
import com.ddf.vodsystem.services.ClipService; import com.ddf.vodsystem.services.ClipService;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory; import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List; import java.util.List;
@RestController @RestController
@@ -78,19 +82,39 @@ public class ClipController {
} }
@GetMapping("/{id}/media") @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); Resource resource = clipService.downloadClip(id);
if (resource == null || !resource.exists()) { if (resource == null || !resource.exists()) {
return ResponseEntity.notFound().build(); 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() return ResponseEntity.ok()
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename())) .header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.body(resource); .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") @GetMapping("/{id}/thumbnail")
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) { public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
Resource resource = clipService.downloadThumbnail(id); Resource resource = clipService.downloadThumbnail(id);

View File

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

View File

@@ -6,12 +6,16 @@ import com.ddf.vodsystem.dto.APIResponse;
import com.ddf.vodsystem.entities.Vod; import com.ddf.vodsystem.entities.Vod;
import com.ddf.vodsystem.services.VodService; import com.ddf.vodsystem.services.VodService;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory; import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List; import java.util.List;
@RestController @RestController
@@ -55,19 +59,39 @@ public class VodController {
} }
@GetMapping("/{id}/media") @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); Resource resource = vodService.downloadVod(id);
if (resource == null || !resource.exists()) { if (resource == null || !resource.exists()) {
return ResponseEntity.notFound().build(); 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() return ResponseEntity.ok()
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename())) .header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.body(resource); .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") @GetMapping("/{id}/thumbnail")
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) { public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
Resource resource = vodService.downloadThumbnail(id); Resource resource = vodService.downloadThumbnail(id);

View File

@@ -10,6 +10,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.nio.file.StandardCopyOption; 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 @PostConstruct
public void createDirectoriesIfNotExist() throws IOException { public void createDirectoriesIfNotExist() throws IOException {
Files.createDirectories(Path.of(tempInputsDir)); Files.createDirectories(Path.of(tempInputsDir));
Files.createDirectories(Path.of(tempOutputsDir)); Files.createDirectories(Path.of(tempOutputsDir));
Files.createDirectories(Path.of(usersDir)); Files.createDirectories(Path.of(usersDir));
checkDirPermissions();
} }
@Scheduled(fixedRate = TEMP_DIR_CLEANUP_RATE) @Scheduled(fixedRate = TEMP_DIR_CLEANUP_RATE)

View File

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