Files
ROLAC/API/ROLAC.API/Services/Storage/LocalDiskFileStorage.cs
T
Chris Chen e7bf07c2ad feat(storage): add IFileStorage + local-disk implementation
Adds IFileStorage abstraction and LocalDiskFileStorage for receipt file storage with path-traversal protection, and registers it in DI. Includes 3 TDD-verified xUnit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:28 -07:00

54 lines
1.9 KiB
C#

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<string> 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<Stream?> 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;
}
}