Files
TuneRipper/TuneRipper.Api/Services/SessionService.cs
T

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