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", }; }