refactor(directories): improve error handling

This commit is contained in:
2026-06-20 12:38:40 +01:00
parent 94269459b9
commit 1062f72661
8 changed files with 94 additions and 65 deletions

View File

@@ -1,12 +1,7 @@
package com.ddf.vodsystem.controllers; package com.ddf.vodsystem.controllers;
import com.ddf.vodsystem.dto.APIResponse; import com.ddf.vodsystem.dto.APIResponse;
import com.ddf.vodsystem.exceptions.AlreadyStreaming; import com.ddf.vodsystem.exceptions.*;
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 org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpMediaTypeNotSupportedException;
@@ -92,4 +87,11 @@ public class GlobalExceptionHandler {
APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null); APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
return ResponseEntity.status(HttpStatus.CONFLICT).body(response); return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
} }
@ExceptionHandler(StorageException.class)
public ResponseEntity<APIResponse<Void>> handleStorage(StorageException e) {
logger.error("Storage operation failed", e);
APIResponse<Void> response = new APIResponse<>(ERROR, "Could not process file", null);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
} }

View File

@@ -7,6 +7,7 @@ public class JobStatus {
private ProgressTracker process = new ProgressTracker(); private ProgressTracker process = new ProgressTracker();
private ProgressTracker conversion = new ProgressTracker(); private ProgressTracker conversion = new ProgressTracker();
private boolean failed = false; private boolean failed = false;
private String failedReason;
} }

View File

@@ -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); }
}

View File

@@ -13,6 +13,7 @@ import java.util.concurrent.ExecutionException;
import com.ddf.vodsystem.exceptions.FFMPEGException; import com.ddf.vodsystem.exceptions.FFMPEGException;
import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.exceptions.NotAuthenticated;
import com.ddf.vodsystem.exceptions.StorageException;
import com.ddf.vodsystem.repositories.ClipRepository; import com.ddf.vodsystem.repositories.ClipRepository;
import com.ddf.vodsystem.services.media.MetadataService; import com.ddf.vodsystem.services.media.MetadataService;
import com.ddf.vodsystem.services.media.ThumbnailService; import com.ddf.vodsystem.services.media.ThumbnailService;
@@ -152,10 +153,17 @@ public class ClipService {
User user, User user,
File tempFile, File tempFile,
String fileName) { String fileName) {
// Move clip from temp to output directory File clipFile;
File clipFile = directoryService.getUserClipsFile(user.getId(), fileName); File thumbnailFile;
File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png");
directoryService.cutFile(tempFile, clipFile); // 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; ClipOptions clipMetadata;
try { try {
@@ -165,11 +173,14 @@ public class ClipService {
throw new FFMPEGException("Error retrieving video metadata for clip: " + e.getMessage()); throw new FFMPEGException("Error retrieving video metadata for clip: " + e.getMessage());
} }
// Thumbnail generation can fail with error propagation
try { try {
thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f); thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f);
} catch (IOException | InterruptedException e) { } catch (InterruptedException e) {
logger.error("Error generating thumbnail for user: {}, {}", user.getId(), e.getMessage()); logger.error("Thumbnail generation interrupted for user: {}", user.getId(), e);
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} catch (IOException e) {
logger.error("Error generating thumbnail for user: {}", user.getId(), e);
} }
clipMetadata.setTitle(clipOptions.getTitle()); clipMetadata.setTitle(clipOptions.getTitle());

View File

@@ -42,80 +42,72 @@ public class DirectoryService {
return new File(dir); return new File(dir);
} }
public File getUserClipsFile(Long userId, String fileName) { public File getUserFolder(Long userId) throws IOException {
if (userId == null || fileName == null || fileName.isEmpty()) { if (userId == null) {
throw new IllegalArgumentException("User ID and file name cannot be null or empty"); throw new IllegalArgumentException("User ID cannot be null or empty");
} }
String dir = outputDir + File.separator + userId + File.separator + "clips" + File.separator + fileName; String dir = outputDir + File.separator + userId;
File file = new File(dir); createDirectory(dir);
return new File(dir);
}
try { public File getUserClipsFile(Long userId, String filename) throws IOException {
createDirectory(file.getParent()); if (filename == null || filename.isEmpty()) {
} catch (IOException e) { throw new IllegalArgumentException("File name cannot be null or empty");
logger.error("Error creating clips directory: {}", e.getMessage());
} }
File userDir = getUserFolder(userId);
String fullPath = userDir.getAbsolutePath() + File.separator + "clips" + File.separator + filename;
File file = new File(fullPath);
createDirectory(file.getParent());
return file; return file;
} }
public File getVodFile(String filename) throws IOException { public File getVodFile(Long userId, String filename) throws IOException {
String dir = outputDir + File.separator + "vods" + File.separator + filename; if (filename == null || filename.isEmpty()) {
File file = new File(dir); 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()); createDirectory(file.getParent());
return file; return file;
} }
public File getUserThumbnailsFile(Long userId, String fileName) { public File getUserThumbnailsFile(Long userId, String filename) throws IOException {
if (userId == null || fileName == null || fileName.isEmpty()) { if (filename == null || filename.isEmpty()) {
throw new IllegalArgumentException("User ID and file name cannot be null or empty"); throw new IllegalArgumentException("File name cannot be null or empty");
} }
String dir = outputDir + File.separator + userId + File.separator + "thumbnails" + File.separator + fileName; String userDir = getUserFolder(userId).getAbsolutePath();
File file = new File(dir); File file = new File(userDir + File.separator + "thumbnails" + File.separator + filename);
createDirectory(file.getParent());
try {
createDirectory(file.getParent());
} catch (IOException e) {
logger.error("Error creating thumbnails directory: {}", e.getMessage());
}
return file; return file;
} }
public void saveAtDir(File file, MultipartFile multipartFile) { public void saveMultipartFile(File file, MultipartFile multipartFile) throws IOException {
try { createDirectory(file.getParent());
createDirectory(file.getAbsolutePath()); Path filePath = Paths.get(file.getAbsolutePath());
Path filePath = Paths.get(file.getAbsolutePath()); Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
logger.error(e.getMessage());
}
} }
public void copyFile(File source, File target) { public void copyFile(File source, File target) throws IOException {
Path sourcePath = Paths.get(source.getAbsolutePath()); Path sourcePath = Paths.get(source.getAbsolutePath());
Path destPath = Paths.get(target.getAbsolutePath()); Path destPath = Paths.get(target.getAbsolutePath());
try { Files.createDirectories(destPath.getParent());
Files.createDirectories(destPath.getParent()); Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING);
Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING); logger.info("Copied file from {} to {}", sourcePath, destPath);
logger.info("Copied file from {} to {}", sourcePath, destPath);
} catch (IOException e) {
logger.error(e.getMessage());
}
} }
public void cutFile(File source, File target) { public void cutFile(File source, File target) throws IOException {
copyFile(source, target); copyFile(source, target);
Files.deleteIfExists(source.toPath());
try { logger.info("Deleted source file: {}", source.getAbsolutePath());
Files.deleteIfExists(source.toPath());
logger.info("Deleted source file: {}", source.getAbsolutePath());
} catch (IOException e) {
logger.error("Error deleting source file: {}", e.getMessage());
}
} }
public String getFileExtension(String filePath) { public String getFileExtension(String filePath) {

View File

@@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentHashMap;
import com.ddf.vodsystem.dto.Job; import com.ddf.vodsystem.dto.Job;
import com.ddf.vodsystem.entities.User; 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.CompressionService;
import com.ddf.vodsystem.services.media.RemuxService; import com.ddf.vodsystem.services.media.RemuxService;
@@ -76,7 +77,12 @@ public class JobService {
public void convertJob(Job job) { public void convertJob(Job job) {
logger.info("Converting job: {}", job.getUuid()); logger.info("Converting job: {}", job.getUuid());
File tempFile = new File(job.getInputFile().getAbsolutePath() + ".temp"); 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(); job.getStatus().getConversion().reset();
@@ -92,7 +98,7 @@ public class JobService {
try { try {
Files.deleteIfExists(Paths.get(tempFile.getAbsolutePath())); Files.deleteIfExists(Paths.get(tempFile.getAbsolutePath()));
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new StorageException("Could not delete path at: " + tempFile.getAbsolutePath(), e);
} }
}).whenComplete((ignored, throwable) -> { }).whenComplete((ignored, throwable) -> {
if (throwable != null) { if (throwable != null) {
@@ -129,11 +135,14 @@ public class JobService {
)).exceptionally( )).exceptionally(
ex -> { ex -> {
job.getStatus().setFailed(true); job.getStatus().setFailed(true);
job.getStatus().setFailedReason(ex.getMessage());
logger.error("Error processing job {}: {}", job.getUuid(), ex.getMessage());
return null; return null;
} }
); );
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
job.getStatus().setFailed(true); job.getStatus().setFailed(true);
job.getStatus().setFailedReason(e.getMessage());
logger.error("Error processing job {}: {}", job.getUuid(), e.getMessage()); logger.error("Error processing job {}: {}", job.getUuid(), e.getMessage());
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }

View File

@@ -141,7 +141,7 @@ public class StreamService {
float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f); float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f);
float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 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); streamActionsService.saveSection(fileSegments, trimOffset, duration, outputFile);
} }

View File

@@ -3,11 +3,13 @@ package com.ddf.vodsystem.services;
import com.ddf.vodsystem.dto.Job; import com.ddf.vodsystem.dto.Job;
import com.ddf.vodsystem.dto.ClipOptions; import com.ddf.vodsystem.dto.ClipOptions;
import com.ddf.vodsystem.exceptions.FFMPEGException; import com.ddf.vodsystem.exceptions.FFMPEGException;
import com.ddf.vodsystem.exceptions.StorageException;
import com.ddf.vodsystem.services.media.MetadataService; import com.ddf.vodsystem.services.media.MetadataService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Base64; import java.util.Base64;
import java.util.UUID; import java.util.UUID;
@@ -41,10 +43,14 @@ public class UploadService {
File inputFile = directoryService.getTempInputFile(uuid + "." + extension); File inputFile = directoryService.getTempInputFile(uuid + "." + extension);
File outputFile = directoryService.getTempOutputFile(uuid + ".mp4"); File outputFile = directoryService.getTempOutputFile(uuid + ".mp4");
directoryService.saveAtDir(inputFile, file);
// add job try {
logger.info("Uploaded file and creating job with UUID: {}", uuid); 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); ClipOptions clipOptions = getMetadataWithTimeout(inputFile);
Job job = new Job(uuid, inputFile, outputFile, clipOptions); Job job = new Job(uuid, inputFile, outputFile, clipOptions);