From 1062f72661ee7852e49ace1c34017039c9d0ad69 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Sat, 20 Jun 2026 12:38:40 +0100 Subject: [PATCH] refactor(directories): improve error handling --- .../controllers/GlobalExceptionHandler.java | 14 +-- .../java/com/ddf/vodsystem/dto/JobStatus.java | 1 + .../exceptions/StorageException.java | 8 ++ .../ddf/vodsystem/services/ClipService.java | 23 +++-- .../vodsystem/services/DirectoryService.java | 86 +++++++++---------- .../ddf/vodsystem/services/JobService.java | 13 ++- .../ddf/vodsystem/services/StreamService.java | 2 +- .../ddf/vodsystem/services/UploadService.java | 12 ++- 8 files changed, 94 insertions(+), 65 deletions(-) create mode 100644 src/main/java/com/ddf/vodsystem/exceptions/StorageException.java diff --git a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java index 659f944..df990a7 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java +++ b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java @@ -1,12 +1,7 @@ package com.ddf.vodsystem.controllers; import com.ddf.vodsystem.dto.APIResponse; -import com.ddf.vodsystem.exceptions.AlreadyStreaming; -import com.ddf.vodsystem.exceptions.FFMPEGException; -import com.ddf.vodsystem.exceptions.JobNotFinished; -import com.ddf.vodsystem.exceptions.JobNotFound; -import com.ddf.vodsystem.exceptions.KeyNotFound; -import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.exceptions.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -92,4 +87,11 @@ public class GlobalExceptionHandler { APIResponse response = new APIResponse<>(ERROR, ex.getMessage(), null); return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } + + @ExceptionHandler(StorageException.class) + public ResponseEntity> handleStorage(StorageException e) { + logger.error("Storage operation failed", e); + APIResponse response = new APIResponse<>(ERROR, "Could not process file", null); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } } \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/dto/JobStatus.java b/src/main/java/com/ddf/vodsystem/dto/JobStatus.java index 4930b4b..397587b 100644 --- a/src/main/java/com/ddf/vodsystem/dto/JobStatus.java +++ b/src/main/java/com/ddf/vodsystem/dto/JobStatus.java @@ -7,6 +7,7 @@ public class JobStatus { private ProgressTracker process = new ProgressTracker(); private ProgressTracker conversion = new ProgressTracker(); private boolean failed = false; + private String failedReason; } diff --git a/src/main/java/com/ddf/vodsystem/exceptions/StorageException.java b/src/main/java/com/ddf/vodsystem/exceptions/StorageException.java new file mode 100644 index 0000000..67a9e66 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/exceptions/StorageException.java @@ -0,0 +1,8 @@ +package com.ddf.vodsystem.exceptions; + +public class StorageException extends RuntimeException { + public StorageException(String message) { + super(message); + } + public StorageException(String message, Throwable cause) { super(message, cause); } +} diff --git a/src/main/java/com/ddf/vodsystem/services/ClipService.java b/src/main/java/com/ddf/vodsystem/services/ClipService.java index 9931556..3838dfe 100644 --- a/src/main/java/com/ddf/vodsystem/services/ClipService.java +++ b/src/main/java/com/ddf/vodsystem/services/ClipService.java @@ -13,6 +13,7 @@ import java.util.concurrent.ExecutionException; import com.ddf.vodsystem.exceptions.FFMPEGException; import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.exceptions.StorageException; import com.ddf.vodsystem.repositories.ClipRepository; import com.ddf.vodsystem.services.media.MetadataService; import com.ddf.vodsystem.services.media.ThumbnailService; @@ -152,10 +153,17 @@ public class ClipService { User user, File tempFile, String fileName) { - // Move clip from temp to output directory - File clipFile = directoryService.getUserClipsFile(user.getId(), fileName); - File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png"); - directoryService.cutFile(tempFile, clipFile); + File clipFile; + File thumbnailFile; + + // Move temp file from temp dir to output dir + try { + clipFile = directoryService.getUserClipsFile(user.getId(), fileName); + thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png"); + directoryService.cutFile(tempFile, clipFile); + } catch (IOException e) { + throw new StorageException("Failed to move clip from temporary directory to output directory", e); + } ClipOptions clipMetadata; try { @@ -165,11 +173,14 @@ public class ClipService { throw new FFMPEGException("Error retrieving video metadata for clip: " + e.getMessage()); } + // Thumbnail generation can fail with error propagation try { thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f); - } catch (IOException | InterruptedException e) { - logger.error("Error generating thumbnail for user: {}, {}", user.getId(), e.getMessage()); + } catch (InterruptedException e) { + logger.error("Thumbnail generation interrupted for user: {}", user.getId(), e); Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.error("Error generating thumbnail for user: {}", user.getId(), e); } clipMetadata.setTitle(clipOptions.getTitle()); diff --git a/src/main/java/com/ddf/vodsystem/services/DirectoryService.java b/src/main/java/com/ddf/vodsystem/services/DirectoryService.java index 74d8351..64949a4 100644 --- a/src/main/java/com/ddf/vodsystem/services/DirectoryService.java +++ b/src/main/java/com/ddf/vodsystem/services/DirectoryService.java @@ -42,80 +42,72 @@ public class DirectoryService { return new File(dir); } - public File getUserClipsFile(Long userId, String fileName) { - if (userId == null || fileName == null || fileName.isEmpty()) { - throw new IllegalArgumentException("User ID and file name cannot be null or empty"); + public File getUserFolder(Long userId) throws IOException { + if (userId == null) { + throw new IllegalArgumentException("User ID cannot be null or empty"); } - String dir = outputDir + File.separator + userId + File.separator + "clips" + File.separator + fileName; - File file = new File(dir); + String dir = outputDir + File.separator + userId; + createDirectory(dir); + return new File(dir); + } - try { - createDirectory(file.getParent()); - } catch (IOException e) { - logger.error("Error creating clips directory: {}", e.getMessage()); + public File getUserClipsFile(Long userId, String filename) throws IOException { + if (filename == null || filename.isEmpty()) { + throw new IllegalArgumentException("File name cannot be null or empty"); } + File userDir = getUserFolder(userId); + String fullPath = userDir.getAbsolutePath() + File.separator + "clips" + File.separator + filename; + File file = new File(fullPath); + + createDirectory(file.getParent()); return file; } - public File getVodFile(String filename) throws IOException { - String dir = outputDir + File.separator + "vods" + File.separator + filename; - File file = new File(dir); + public File getVodFile(Long userId, String filename) throws IOException { + if (filename == null || filename.isEmpty()) { + throw new IllegalArgumentException("File name cannot be null or empty"); + } + + String userDir = getUserFolder(userId).getAbsolutePath(); + File file = new File(userDir + File.separator + "vods" + File.separator + filename); createDirectory(file.getParent()); return file; } - public File getUserThumbnailsFile(Long userId, String fileName) { - if (userId == null || fileName == null || fileName.isEmpty()) { - throw new IllegalArgumentException("User ID and file name cannot be null or empty"); + public File getUserThumbnailsFile(Long userId, String filename) throws IOException { + if (filename == null || filename.isEmpty()) { + throw new IllegalArgumentException("File name cannot be null or empty"); } - String dir = outputDir + File.separator + userId + File.separator + "thumbnails" + File.separator + fileName; - File file = new File(dir); - - try { - createDirectory(file.getParent()); - } catch (IOException e) { - logger.error("Error creating thumbnails directory: {}", e.getMessage()); - } + String userDir = getUserFolder(userId).getAbsolutePath(); + File file = new File(userDir + File.separator + "thumbnails" + File.separator + filename); + createDirectory(file.getParent()); return file; } - public void saveAtDir(File file, MultipartFile multipartFile) { - try { - createDirectory(file.getAbsolutePath()); - Path filePath = Paths.get(file.getAbsolutePath()); - Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - logger.error(e.getMessage()); - } + public void saveMultipartFile(File file, MultipartFile multipartFile) throws IOException { + createDirectory(file.getParent()); + Path filePath = Paths.get(file.getAbsolutePath()); + Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); } - public void copyFile(File source, File target) { + public void copyFile(File source, File target) throws IOException { Path sourcePath = Paths.get(source.getAbsolutePath()); Path destPath = Paths.get(target.getAbsolutePath()); - try { - Files.createDirectories(destPath.getParent()); - Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING); - logger.info("Copied file from {} to {}", sourcePath, destPath); - } catch (IOException e) { - logger.error(e.getMessage()); - } + Files.createDirectories(destPath.getParent()); + Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING); + logger.info("Copied file from {} to {}", sourcePath, destPath); } - public void cutFile(File source, File target) { + public void cutFile(File source, File target) throws IOException { copyFile(source, target); - - try { - Files.deleteIfExists(source.toPath()); - logger.info("Deleted source file: {}", source.getAbsolutePath()); - } catch (IOException e) { - logger.error("Error deleting source file: {}", e.getMessage()); - } + Files.deleteIfExists(source.toPath()); + logger.info("Deleted source file: {}", source.getAbsolutePath()); } public String getFileExtension(String filePath) { diff --git a/src/main/java/com/ddf/vodsystem/services/JobService.java b/src/main/java/com/ddf/vodsystem/services/JobService.java index 0849862..311649c 100644 --- a/src/main/java/com/ddf/vodsystem/services/JobService.java +++ b/src/main/java/com/ddf/vodsystem/services/JobService.java @@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentHashMap; import com.ddf.vodsystem.dto.Job; import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.StorageException; import com.ddf.vodsystem.services.media.CompressionService; import com.ddf.vodsystem.services.media.RemuxService; @@ -76,7 +77,12 @@ public class JobService { public void convertJob(Job job) { logger.info("Converting job: {}", job.getUuid()); File tempFile = new File(job.getInputFile().getAbsolutePath() + ".temp"); - directoryService.copyFile(job.getInputFile(), tempFile); + + try { + directoryService.copyFile(job.getInputFile(), tempFile); + } catch (IOException e) { + throw new StorageException("Could not create temporary file: " + tempFile, e); + } job.getStatus().getConversion().reset(); @@ -92,7 +98,7 @@ public class JobService { try { Files.deleteIfExists(Paths.get(tempFile.getAbsolutePath())); } catch (IOException e) { - throw new RuntimeException(e); + throw new StorageException("Could not delete path at: " + tempFile.getAbsolutePath(), e); } }).whenComplete((ignored, throwable) -> { if (throwable != null) { @@ -129,11 +135,14 @@ public class JobService { )).exceptionally( ex -> { job.getStatus().setFailed(true); + job.getStatus().setFailedReason(ex.getMessage()); + logger.error("Error processing job {}: {}", job.getUuid(), ex.getMessage()); return null; } ); } catch (IOException | InterruptedException e) { job.getStatus().setFailed(true); + job.getStatus().setFailedReason(e.getMessage()); logger.error("Error processing job {}: {}", job.getUuid(), e.getMessage()); Thread.currentThread().interrupt(); } diff --git a/src/main/java/com/ddf/vodsystem/services/StreamService.java b/src/main/java/com/ddf/vodsystem/services/StreamService.java index 97e62f1..1195019 100644 --- a/src/main/java/com/ddf/vodsystem/services/StreamService.java +++ b/src/main/java/com/ddf/vodsystem/services/StreamService.java @@ -141,7 +141,7 @@ public class StreamService { float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f); float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 1000f; - File outputFile = directoryService.getVodFile(UUID.randomUUID() + ".mp4"); + File outputFile = directoryService.getVodFile(user.getId(), UUID.randomUUID() + ".mp4"); streamActionsService.saveSection(fileSegments, trimOffset, duration, outputFile); } diff --git a/src/main/java/com/ddf/vodsystem/services/UploadService.java b/src/main/java/com/ddf/vodsystem/services/UploadService.java index 46e909b..77c3044 100644 --- a/src/main/java/com/ddf/vodsystem/services/UploadService.java +++ b/src/main/java/com/ddf/vodsystem/services/UploadService.java @@ -3,11 +3,13 @@ package com.ddf.vodsystem.services; import com.ddf.vodsystem.dto.Job; import com.ddf.vodsystem.dto.ClipOptions; import com.ddf.vodsystem.exceptions.FFMPEGException; +import com.ddf.vodsystem.exceptions.StorageException; import com.ddf.vodsystem.services.media.MetadataService; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.Base64; import java.util.UUID; @@ -41,10 +43,14 @@ public class UploadService { File inputFile = directoryService.getTempInputFile(uuid + "." + extension); File outputFile = directoryService.getTempOutputFile(uuid + ".mp4"); - directoryService.saveAtDir(inputFile, file); - // add job - logger.info("Uploaded file and creating job with UUID: {}", uuid); + try { + directoryService.saveMultipartFile(inputFile, file); + } catch (IOException e) { + throw new StorageException("Could not save Multipart file at: " + inputFile, e); + } + + logger.info("Multi-part file uploaded at {}", inputFile); ClipOptions clipOptions = getMetadataWithTimeout(inputFile); Job job = new Job(uuid, inputFile, outputFile, clipOptions);