using System.Collections.Concurrent; namespace TuneRipper.Api.Services; public class SessionService : ISessionService { private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; private readonly ConcurrentDictionary _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 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) ); } /// /// Creates a new session with a unique ID. /// Note: The session directory is NOT created at this point - it will be created /// lazily when the first MP3 file is saved using EnsureSessionDirectoryExists(). /// /// The unique session ID public string CreateSession() { var sessionId = Guid.NewGuid().ToString("N")[..16]; // Use first 16 characters for shorter IDs _sessions[sessionId] = DateTime.UtcNow; // Don't create directory here - it will be created when first MP3 is saved _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 if it exists 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); } } else { _logger.LogDebug("No directory to clean up for expired session: {SessionId} (no MP3 files were downloaded)", 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); } /// /// Ensures that the session directory exists, creating it if necessary. /// This method should be called just before saving the first MP3 file in a session. /// /// The session ID /// Thrown when sessionId is null or empty /// Thrown when session is invalid or expired public void EnsureSessionDirectoryExists(string sessionId) { if (string.IsNullOrWhiteSpace(sessionId)) { throw new ArgumentException("Session ID cannot be null or empty", nameof(sessionId)); } if (!IsValidSession(sessionId)) { throw new InvalidOperationException($"Invalid session ID: {sessionId}"); } var sessionPath = GetSessionDownloadsPath(sessionId); if (!Directory.Exists(sessionPath)) { Directory.CreateDirectory(sessionPath); _logger.LogInformation("Created session directory for session: {SessionId} at {Path}", sessionId, sessionPath); } } public void Dispose() { _cleanupTimer?.Dispose(); } }