using Microsoft.Extensions.Configuration; namespace ROLAC.API.Services.Storage; public class LocalDiskFileStorage : IFileStorage { private readonly string _root; public LocalDiskFileStorage(IConfiguration config) { var configured = config["Storage:LocalRoot"] ?? "App_Data/storage"; _root = Path.IsPathRooted(configured) ? configured : Path.Combine(Directory.GetCurrentDirectory(), configured); Directory.CreateDirectory(_root); } private string Resolve(string relativePath) { if (string.IsNullOrWhiteSpace(relativePath)) throw new ArgumentException("relativePath is required.", nameof(relativePath)); var normalized = relativePath.Replace('\\', '/').TrimStart('/'); var full = Path.GetFullPath(Path.Combine(_root, normalized)); var rootFull = Path.GetFullPath(_root) + Path.DirectorySeparatorChar; if (!full.StartsWith(rootFull, StringComparison.Ordinal)) throw new ArgumentException("Path escapes storage root.", nameof(relativePath)); return full; } public async Task SaveAsync(Stream content, string relativePath, CancellationToken ct = default) { var full = Resolve(relativePath); Directory.CreateDirectory(Path.GetDirectoryName(full)!); await using var file = File.Create(full); await content.CopyToAsync(file, ct); return relativePath.Replace('\\', '/').TrimStart('/'); } public Task OpenReadAsync(string relativePath, CancellationToken ct = default) { var full = Resolve(relativePath); Stream? result = File.Exists(full) ? File.OpenRead(full) : null; return Task.FromResult(result); } public Task DeleteAsync(string relativePath, CancellationToken ct = default) { var full = Resolve(relativePath); if (File.Exists(full)) File.Delete(full); return Task.CompletedTask; } }