214 lines
7.1 KiB
C#
214 lines
7.1 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Options;
|
|
using TuneRipper.Api.Configuration;
|
|
|
|
namespace TuneRipper.Api.Services;
|
|
|
|
public class SessionService : ISessionService, IDisposable
|
|
{
|
|
private readonly ILogger<SessionService> _logger;
|
|
private readonly string _downloadsRoot;
|
|
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,
|
|
IOptions<StorageOptions> options,
|
|
ILogger<SessionService> logger)
|
|
{
|
|
_logger = logger;
|
|
_downloadsRoot = Path.GetFullPath(
|
|
string.IsNullOrWhiteSpace(options.Value.DownloadsPath)
|
|
? Path.Combine(environment.WebRootPath, "downloads")
|
|
: options.Value.DownloadsPath);
|
|
|
|
Directory.CreateDirectory(_downloadsRoot);
|
|
RestoreSessionsFromDisk();
|
|
|
|
// Clean up expired sessions every hour
|
|
_cleanupTimer = new Timer(
|
|
callback: _ => CleanupExpiredSessions(),
|
|
state: null,
|
|
dueTime: TimeSpan.FromHours(1),
|
|
period: TimeSpan.FromHours(1)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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().
|
|
/// </summary>
|
|
/// <returns>The unique session ID</returns>
|
|
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))
|
|
{
|
|
TouchSession(sessionId);
|
|
return sessionId;
|
|
}
|
|
|
|
return CreateSession();
|
|
}
|
|
|
|
public bool IsValidSession(string sessionId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sessionId))
|
|
return false;
|
|
|
|
if (!_sessions.TryGetValue(sessionId, out var lastActivity))
|
|
{
|
|
var sessionPath = GetSessionDownloadsPath(sessionId);
|
|
if (Directory.Exists(sessionPath))
|
|
{
|
|
lastActivity = Directory.GetLastWriteTimeUtc(sessionPath);
|
|
_sessions.TryAdd(sessionId, lastActivity);
|
|
}
|
|
}
|
|
|
|
if (_sessions.TryGetValue(sessionId, out 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 GetDownloadsRootPath() => _downloadsRoot;
|
|
|
|
public string GetSessionDownloadsPath(string sessionId)
|
|
{
|
|
return Path.Combine(_downloadsRoot, sessionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID</param>
|
|
/// <exception cref="ArgumentException">Thrown when sessionId is null or empty</exception>
|
|
/// <exception cref="InvalidOperationException">Thrown when session is invalid or expired</exception>
|
|
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);
|
|
}
|
|
|
|
TouchSession(sessionId);
|
|
}
|
|
|
|
private void RestoreSessionsFromDisk()
|
|
{
|
|
foreach (var sessionPath in Directory.EnumerateDirectories(_downloadsRoot))
|
|
{
|
|
var sessionId = Path.GetFileName(sessionPath);
|
|
var lastActivity = Directory.GetLastWriteTimeUtc(sessionPath);
|
|
|
|
if (DateTime.UtcNow - lastActivity <= _sessionTimeout)
|
|
{
|
|
_sessions[sessionId] = lastActivity;
|
|
}
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"Restored {Count} active sessions from {DownloadsRoot}",
|
|
_sessions.Count,
|
|
_downloadsRoot);
|
|
}
|
|
|
|
private void TouchSession(string sessionId)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
_sessions[sessionId] = now;
|
|
|
|
var sessionPath = GetSessionDownloadsPath(sessionId);
|
|
if (Directory.Exists(sessionPath))
|
|
{
|
|
Directory.SetLastWriteTimeUtc(sessionPath, now);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_cleanupTimer?.Dispose();
|
|
}
|
|
}
|