using Femto.Modules.Media.Data; namespace Femto.Modules.Media.Infrastructure; internal class FilesystemStorageProvider : IStorageProvider { private readonly string _storageRoot; public FilesystemStorageProvider(string storageRoot) { if (string.IsNullOrWhiteSpace(storageRoot)) throw new ArgumentException( "Storage root cannot be null or empty.", nameof(storageRoot) ); this._storageRoot = Path.GetFullPath(storageRoot); if (!Directory.Exists(this._storageRoot)) { Directory.CreateDirectory(this._storageRoot); } } public async Task SaveBlob(string id, Stream data) { string filePath = this.GetSafeFilePath(id); // Ensure the directory exists Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write); await data.CopyToAsync(fileStream); } public async Task LoadBlob(string id) { string filePath = this.GetSafeFilePath(id); if (!File.Exists(filePath)) throw new FileNotFoundException("The blob with the specified ID was not found.", id); Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); return fileStream; } private string GetSafeFilePath(string id) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Blob ID cannot be null or empty.", nameof(id)); // Sanitize and validate the path string combinedPath = Path.Combine(this._storageRoot, id); string fullPath = Path.GetFullPath(combinedPath); if (!fullPath.StartsWith(this._storageRoot, StringComparison.Ordinal)) throw new UnauthorizedAccessException("Access to the path is denied."); return fullPath; } }