using Microsoft.Extensions.Options; namespace GB5Shared.Storage { /// /// backed by the OS filesystem (local disk or UNC network share). /// Configure via StorageConfiguration.NetworkBasePath. /// public class NetworkStorageProvider : IStorageProvider { private readonly string _basePath; public StorageProviderType ProviderType => StorageProviderType.Network; public NetworkStorageProvider(IOptions config) { _basePath = config.Value.NetworkBasePath; if (string.IsNullOrWhiteSpace(_basePath)) throw new InvalidOperationException( "StorageConfiguration.NetworkBasePath is not configured. " + "Set it via HashiCorp Vault or configuration."); } public async Task SaveAsync(Stream content, string storagePath, string mimeType, CancellationToken ct = default) { var fullPath = GetFullPath(storagePath); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); await using var fs = new FileStream( fullPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 81_920, useAsync: true); await content.CopyToAsync(fs, ct).ConfigureAwait(false); return storagePath; } public Task GetStreamAsync(string storagePath, CancellationToken ct = default) { var fullPath = GetFullPath(storagePath); if (!File.Exists(fullPath)) throw new FileNotFoundException($"File not found at storage path: {storagePath}", fullPath); Stream stream = new FileStream( fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 81_920, useAsync: true); return Task.FromResult(stream); } public Task DeleteAsync(string storagePath, CancellationToken ct = default) { var fullPath = GetFullPath(storagePath); if (!File.Exists(fullPath)) return Task.FromResult(false); File.Delete(fullPath); // Remove the immediate parent directory if it is now empty (keeps storage tidy) var dir = Path.GetDirectoryName(fullPath); if (dir is not null && Directory.Exists(dir) && Directory.GetFileSystemEntries(dir).Length == 0) { Directory.Delete(dir); } return Task.FromResult(true); } public Task ExistsAsync(string storagePath, CancellationToken ct = default) => Task.FromResult(File.Exists(GetFullPath(storagePath))); public Task MoveAsync(string oldStoragePath, string newStoragePath, CancellationToken ct = default) { var oldFull = GetFullPath(oldStoragePath); if (!File.Exists(oldFull)) return Task.FromResult(false); var newFull = GetFullPath(newStoragePath); Directory.CreateDirectory(Path.GetDirectoryName(newFull)!); File.Move(oldFull, newFull, overwrite: true); var oldDir = Path.GetDirectoryName(oldFull); if (oldDir is not null && Directory.Exists(oldDir) && Directory.GetFileSystemEntries(oldDir).Length == 0) { Directory.Delete(oldDir); } return Task.FromResult(true); } // storagePath is usually relative (joined onto _basePath below), but some rows store a // fully-qualified path instead (e.g. MSIGNATORY.SIGNATUREIMAGESTORAGEPATH saved as // "/root/GB5BuildFile/Images/x.png" or "C:\..."). Detect both Unix- and Windows-style // absolute paths explicitly (rather than relying on Path.IsPathRooted, which only // recognizes the current OS's own convention) so an absolute path saved on one OS is // still honored as-is when the app later runs on the other OS. private string GetFullPath(string storagePath) { var normalized = storagePath.Replace('\\', '/'); bool isWindowsAbsolute = normalized.Length >= 2 && normalized[1] == ':'; bool isUnixAbsolute = normalized.StartsWith('/'); if (isWindowsAbsolute || isUnixAbsolute) return normalized.Replace('/', Path.DirectorySeparatorChar); return Path.Combine(_basePath, normalized.Replace('/', Path.DirectorySeparatorChar)); } } }