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
+124
View File
@@ -0,0 +1,124 @@
using System.Collections.Concurrent;
namespace TuneRipper.Api.Services;
public class SessionService : ISessionService
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<SessionService> _logger;
private readonly ConcurrentDictionary<string, DateTime> _sessions = new();
private readonly Timer _cleanupTimer;
// Session expires after 24 hours of inactivity
private readonly TimeSpan _sessionTimeout = TimeSpan.FromHours(24);
public SessionService(IWebHostEnvironment environment, ILogger<SessionService> logger)
{
_environment = environment;
_logger = logger;
// Clean up expired sessions every hour
_cleanupTimer = new Timer(
callback: _ => CleanupExpiredSessions(),
state: null,
dueTime: TimeSpan.FromHours(1),
period: TimeSpan.FromHours(1)
);
}
public string CreateSession()
{
var sessionId = Guid.NewGuid().ToString("N")[..16]; // Use first 16 characters for shorter IDs
_sessions[sessionId] = DateTime.UtcNow;
// Ensure session directory exists
var sessionPath = GetSessionDownloadsPath(sessionId);
Directory.CreateDirectory(sessionPath);
_logger.LogInformation("Created new session: {SessionId}", sessionId);
return sessionId;
}
public string GetOrCreateSession(string? sessionId)
{
if (!string.IsNullOrWhiteSpace(sessionId) && IsValidSession(sessionId))
{
// Update last activity time
_sessions[sessionId] = DateTime.UtcNow;
return sessionId;
}
return CreateSession();
}
public bool IsValidSession(string sessionId)
{
if (string.IsNullOrWhiteSpace(sessionId))
return false;
if (_sessions.TryGetValue(sessionId, out var lastActivity))
{
// Check if session has expired
if (DateTime.UtcNow - lastActivity > _sessionTimeout)
{
_sessions.TryRemove(sessionId, out _);
return false;
}
return true;
}
return false;
}
public void CleanupExpiredSessions()
{
try
{
var expiredSessions = _sessions
.Where(kvp => DateTime.UtcNow - kvp.Value > _sessionTimeout)
.Select(kvp => kvp.Key)
.ToList();
foreach (var sessionId in expiredSessions)
{
if (_sessions.TryRemove(sessionId, out _))
{
// Clean up session directory
var sessionPath = GetSessionDownloadsPath(sessionId);
if (Directory.Exists(sessionPath))
{
try
{
Directory.Delete(sessionPath, recursive: true);
_logger.LogInformation("Cleaned up expired session directory: {SessionId}", sessionId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete expired session directory: {SessionId}", sessionId);
}
}
}
}
if (expiredSessions.Count > 0)
{
_logger.LogInformation("Cleaned up {Count} expired sessions", expiredSessions.Count);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during session cleanup");
}
}
public string GetSessionDownloadsPath(string sessionId)
{
var downloadsRoot = Path.Combine(_environment.WebRootPath, "downloads");
return Path.Combine(downloadsRoot, sessionId);
}
public void Dispose()
{
_cleanupTimer?.Dispose();
}
}