Refactor downloads to use job-specific subdirectories

Organize downloaded files into per-job subdirectories under "downloads" instead of a flat structure. Update DownloadController and VideoService to read, write, and delete files within these job-specific directories. Add helper methods for directory management and improve logging. Update download/delete endpoints and result URLs to reflect the new structure. Clean up empty job directories after file deletion.
This commit is contained in:
Michal Vrba
2025-12-14 11:04:46 +01:00
parent 7c99721174
commit 2d2e855202
14 changed files with 1073 additions and 66 deletions
+292 -29
View File
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using TuneRipper.Api.Services;
namespace TuneRipper.Api.Controllers;
@@ -8,40 +9,48 @@ public class DownloadController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<DownloadController> _logger;
private readonly ISessionService _sessionService;
public DownloadController(IWebHostEnvironment environment, ILogger<DownloadController> logger)
public DownloadController(IWebHostEnvironment environment, ILogger<DownloadController> logger, ISessionService sessionService)
{
_environment = environment;
_logger = logger;
_sessionService = sessionService;
}
[HttpGet("{jobId}")]
public IActionResult DownloadFile(string jobId)
// New session-aware download endpoint
[HttpGet("{sessionId}/{jobId}")]
public IActionResult DownloadFileBySession(string sessionId, string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
return BadRequest("Session ID and Job ID are required");
}
if (!_sessionService.IsValidSession(sessionId))
{
return NotFound("Invalid or expired session");
}
try
{
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId);
if (!Directory.Exists(downloadsDir))
if (!Directory.Exists(sessionDownloadsDir))
{
_logger.LogWarning("Downloads directory not found: {Path}", downloadsDir);
return NotFound("Downloads directory not found");
_logger.LogWarning("Session downloads directory not found: {Path}", sessionDownloadsDir);
return NotFound("Session downloads directory not found");
}
// Find files with the specific job ID pattern: jobId_title.ext
var jobFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
// Find files with the specific job ID pattern in the session directory
var jobFiles = Directory.GetFiles(sessionDownloadsDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (jobFiles.Length == 0)
{
_logger.LogWarning("No files found for job ID: {JobId}", jobId);
_logger.LogWarning("No files found for job ID: {JobId} in session: {SessionId}", jobId, sessionId);
return NotFound("File not found for the specified job ID");
}
@@ -54,7 +63,7 @@ public class DownloadController : ControllerBase
return NotFound("File not found");
}
_logger.LogInformation("Serving file: {FileName} for job: {JobId}", fileName, jobId);
_logger.LogInformation("Serving file: {FileName} for job: {JobId} in session: {SessionId}", fileName, jobId, sessionId);
// Determine content type based on file extension
var contentType = Path.GetExtension(fileName).ToLowerInvariant() switch
@@ -74,12 +83,99 @@ public class DownloadController : ControllerBase
return File(fileStream, contentType, downloadFileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading file for job ID: {JobId} in session: {SessionId}", jobId, sessionId);
return StatusCode(500, $"Error downloading file: {ex.Message}");
}
}
// Legacy endpoint for backward compatibility
[HttpGet("{jobId}")]
public IActionResult DownloadFile(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
try
{
// Check in the legacy downloads directory first
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
if (Directory.Exists(downloadsDir))
{
var legacyFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (legacyFiles.Length > 0)
{
return ServeFile(legacyFiles[0], jobId, null);
}
}
// If not found in legacy location, search all session directories
var sessionDirs = Directory.GetDirectories(downloadsDir, "*", SearchOption.TopDirectoryOnly);
foreach (var sessionDir in sessionDirs)
{
var sessionId = Path.GetFileName(sessionDir);
if (!_sessionService.IsValidSession(sessionId)) continue;
var jobFiles = Directory.GetFiles(sessionDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (jobFiles.Length > 0)
{
return ServeFile(jobFiles[0], jobId, sessionId);
}
}
_logger.LogWarning("No files found for job ID: {JobId}", jobId);
return NotFound("File not found for the specified job ID");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading file for job ID: {JobId}", jobId);
return StatusCode(500, $"Error downloading file: {ex.Message}");
}
}
private IActionResult ServeFile(string filePath, string jobId, string? sessionId)
{
var fileName = Path.GetFileName(filePath);
if (!System.IO.File.Exists(filePath))
{
_logger.LogWarning("File not found: {FilePath}", filePath);
return NotFound("File not found");
}
var sessionInfo = sessionId != null ? $" in session: {sessionId}" : "";
_logger.LogInformation("Serving file: {FileName} for job: {JobId}{SessionInfo}", fileName, jobId, sessionInfo);
// Determine content type based on file extension
var contentType = Path.GetExtension(fileName).ToLowerInvariant() switch
{
".mp3" => "audio/mpeg",
".m4a" => "audio/mp4",
".webm" => "audio/webm",
".wav" => "audio/wav",
".ogg" => "audio/ogg",
_ => "application/octet-stream"
};
// Create a user-friendly download filename from our pattern
var downloadFileName = CreateUserFriendlyFileName(fileName);
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
return File(fileStream, contentType, downloadFileName);
}
private string CreateUserFriendlyFileName(string originalFileName)
{
try
@@ -118,25 +214,30 @@ public class DownloadController : ControllerBase
}
}
[HttpGet("info/{jobId}")]
public IActionResult GetDownloadInfo(string jobId)
[HttpGet("info/{sessionId}/{jobId}")]
public IActionResult GetDownloadInfoBySession(string sessionId, string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
return BadRequest("Session ID and Job ID are required");
}
if (!_sessionService.IsValidSession(sessionId))
{
return NotFound("Invalid or expired session");
}
try
{
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId);
if (!Directory.Exists(downloadsDir))
if (!Directory.Exists(sessionDownloadsDir))
{
return NotFound("Downloads directory not found");
return NotFound("Session downloads directory not found");
}
// Find files with the specific job ID pattern
var jobFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
// Find files with the specific job ID pattern in the session directory
var jobFiles = Directory.GetFiles(sessionDownloadsDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
@@ -155,6 +256,7 @@ public class DownloadController : ControllerBase
var info = new
{
jobId,
sessionId,
fileName,
friendlyFileName,
fileSize = FormatFileSize(fileInfo.Length),
@@ -162,20 +264,21 @@ public class DownloadController : ControllerBase
format = Path.GetExtension(fileName).TrimStart('.').ToUpperInvariant(),
createdAt = fileInfo.CreationTime,
lastModified = fileInfo.LastWriteTime,
downloadUrl = $"/api/download/{jobId}"
downloadUrl = $"/api/download/{sessionId}/{jobId}"
};
return Ok(info);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting download info for job ID: {JobId}", jobId);
_logger.LogError(ex, "Error getting download info for job ID: {JobId} in session: {SessionId}", jobId, sessionId);
return StatusCode(500, $"Error getting download info: {ex.Message}");
}
}
[HttpDelete("{jobId}")]
public IActionResult DeleteFile(string jobId)
// Legacy info endpoint for backward compatibility
[HttpGet("info/{jobId}")]
public IActionResult GetDownloadInfo(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
@@ -191,8 +294,96 @@ public class DownloadController : ControllerBase
return NotFound("Downloads directory not found");
}
// Find and delete files with the specific job ID pattern
var jobFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
// Check legacy location first
var legacyFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (legacyFiles.Length > 0)
{
return GetFileInfo(legacyFiles[0], jobId, null);
}
// Search session directories
var sessionDirs = Directory.GetDirectories(downloadsDir, "*", SearchOption.TopDirectoryOnly);
foreach (var sessionDir in sessionDirs)
{
var sessionId = Path.GetFileName(sessionDir);
if (!_sessionService.IsValidSession(sessionId)) continue;
var jobFiles = Directory.GetFiles(sessionDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (jobFiles.Length > 0)
{
return GetFileInfo(jobFiles[0], jobId, sessionId);
}
}
return NotFound("File not found for the specified job ID");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting download info for job ID: {JobId}", jobId);
return StatusCode(500, $"Error getting download info: {ex.Message}");
}
}
private IActionResult GetFileInfo(string filePath, string jobId, string? sessionId)
{
var fileName = Path.GetFileName(filePath);
var fileInfo = new FileInfo(filePath);
var friendlyFileName = CreateUserFriendlyFileName(fileName);
var downloadUrl = sessionId != null
? $"/api/download/{sessionId}/{jobId}"
: $"/api/download/{jobId}";
var info = new
{
jobId,
sessionId,
fileName,
friendlyFileName,
fileSize = FormatFileSize(fileInfo.Length),
fileSizeBytes = fileInfo.Length,
format = Path.GetExtension(fileName).TrimStart('.').ToUpperInvariant(),
createdAt = fileInfo.CreationTime,
lastModified = fileInfo.LastWriteTime,
downloadUrl
};
return Ok(info);
}
[HttpDelete("{sessionId}/{jobId}")]
public IActionResult DeleteFileBySession(string sessionId, string jobId)
{
if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Session ID and Job ID are required");
}
if (!_sessionService.IsValidSession(sessionId))
{
return NotFound("Invalid or expired session");
}
try
{
var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId);
if (!Directory.Exists(sessionDownloadsDir))
{
return NotFound("Session downloads directory not found");
}
// Find and delete files with the specific job ID pattern in the session directory
var jobFiles = Directory.GetFiles(sessionDownloadsDir, $"{jobId}_*.*")
.ToArray();
var deletedCount = 0;
@@ -203,7 +394,7 @@ public class DownloadController : ControllerBase
{
System.IO.File.Delete(filePath);
deletedCount++;
_logger.LogInformation("Deleted file: {FilePath} for job: {JobId}", filePath, jobId);
_logger.LogInformation("Deleted file: {FilePath} for job: {JobId} in session: {SessionId}", filePath, jobId, sessionId);
}
catch (Exception ex)
{
@@ -216,6 +407,78 @@ public class DownloadController : ControllerBase
return NotFound("No files found to delete for the specified job ID");
}
return Ok(new { message = $"Deleted {deletedCount} file(s) for job {jobId} in session {sessionId}", deletedCount, jobId, sessionId });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting files for job ID: {JobId} in session: {SessionId}", jobId, sessionId);
return StatusCode(500, $"Error deleting files: {ex.Message}");
}
}
// Legacy delete endpoint for backward compatibility
[HttpDelete("{jobId}")]
public IActionResult DeleteFile(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
try
{
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
var deletedCount = 0;
if (!Directory.Exists(downloadsDir))
{
return NotFound("Downloads directory not found");
}
// Delete from legacy location
var legacyFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*");
foreach (var filePath in legacyFiles)
{
try
{
System.IO.File.Delete(filePath);
deletedCount++;
_logger.LogInformation("Deleted legacy file: {FilePath} for job: {JobId}", filePath, jobId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete legacy file: {FilePath}", filePath);
}
}
// Delete from session directories
var sessionDirs = Directory.GetDirectories(downloadsDir, "*", SearchOption.TopDirectoryOnly);
foreach (var sessionDir in sessionDirs)
{
var sessionId = Path.GetFileName(sessionDir);
var jobFiles = Directory.GetFiles(sessionDir, $"{jobId}_*.*");
foreach (var filePath in jobFiles)
{
try
{
System.IO.File.Delete(filePath);
deletedCount++;
_logger.LogInformation("Deleted file: {FilePath} for job: {JobId} in session: {SessionId}", filePath, jobId, sessionId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete file: {FilePath}", filePath);
}
}
}
if (deletedCount == 0)
{
return NotFound("No files found to delete for the specified job ID");
}
return Ok(new { message = $"Deleted {deletedCount} file(s) for job {jobId}", deletedCount, jobId });
}
catch (Exception ex)