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