using Microsoft.Extensions.Options;
namespace foodmarket.Api.Storage;
/// Файловое хранилище в {ContentRoot}/uploads/. По умолчанию.
/// Содержимое раздаётся nginx'ом как статика (см. deploy/nginx.conf, location
/// /uploads/). Используется когда MinIO не настроен или fallback после
/// ошибки MinIO connect на старте.
public sealed class LocalObjectStorage : IObjectStorage
{
private readonly IWebHostEnvironment _env;
private readonly StorageOptions _opts;
public LocalObjectStorage(IWebHostEnvironment env, IOptions opts)
{
_env = env;
_opts = opts.Value;
}
public string Kind => "local";
private string Root => Path.Combine(_env.ContentRootPath, _opts.LocalRoot.TrimStart('/'));
private string PathOf(string key) => Path.Combine(Root, key.Replace('/', Path.DirectorySeparatorChar));
public async Task SaveAsync(string key, Stream content, string contentType, CancellationToken ct = default)
{
var path = PathOf(key);
var dir = Path.GetDirectoryName(path);
if (dir is not null) Directory.CreateDirectory(dir);
await using var fs = File.Create(path);
await content.CopyToAsync(fs, ct);
return key;
}
public Task<(Stream Stream, string ContentType)?> OpenAsync(string key, CancellationToken ct = default)
{
var path = PathOf(key);
if (!File.Exists(path)) return Task.FromResult<(Stream, string)?>(null);
Stream s = File.OpenRead(path);
return Task.FromResult<(Stream, string)?>((s, GuessContentType(path)));
}
public Task DeleteAsync(string key, CancellationToken ct = default)
{
var path = PathOf(key);
try { if (File.Exists(path)) File.Delete(path); }
catch { /* idempotent */ }
return Task.CompletedTask;
}
private static string GuessContentType(string path) => Path.GetExtension(path).ToLowerInvariant() switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".webp" => "image/webp",
".csv" => "text/csv",
_ => "application/octet-stream",
};
}