Some checks are pending
Backend:
- src/food-market.api/Storage/IObjectStorage.cs — абстракция
(SaveAsync, OpenAsync, DeleteAsync, PublicUrl). Имя реализации (Kind)
для логов.
- LocalObjectStorage — ContentRoot/uploads/{key}. По умолчанию.
- MinioObjectStorage — S3-совместимый bucket, ключ-объекта совпадает с
тем что хранится в БД (products/{id:N}/{guid}.png). PutObject с явным
size (для NetworkStream копируем в MemoryStream).
- StorageOptions: Type=Local|Minio, Endpoint, AccessKey, SecretKey,
UseSsl, Bucket=food-market-uploads.
- StorageBootstrap.AddObjectStorage — DI-регистрация с runtime fallback
на Local если MinIO-config пустой; MinioBootstrap (IHostedService)
создаёт bucket на старте, ловит ошибку и не валит API.
- UploadsController: GET /uploads/{**path} → стримит из IObjectStorage
(cache-control 7 дней). Нужен когда MinIO активен — для Local nginx
раздаёт быстрее, но фолбэк работает.
- ProductImagesController отрефакторен на IObjectStorage; URL'ы в БД
остаются /uploads/products/{id}/{guid}.ext.
Тесты:
- StorageAbstractionTests (3/3 ✓): Local default, round-trip bytes,
PublicUrl pattern.
Stage-готовность:
- deploy/docker-compose.yml на стейдже обновлён (через scp): добавлен
minio container, depends_on в api, env переменные Storage__*.
Bucket автосоздаётся.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
33 lines
1.9 KiB
C#
33 lines
1.9 KiB
C#
namespace foodmarket.Api.Storage;
|
||
|
||
/// <summary>Абстракция для хранилища бинарных файлов (картинки товаров,
|
||
/// CSV-импорт и т.д.). Реализации: <see cref="LocalObjectStorage"/>
|
||
/// (файлы в <c>{ContentRoot}/uploads/</c>, доступны nginx'ом) и
|
||
/// <see cref="MinioObjectStorage"/> (S3-совместимый bucket).
|
||
///
|
||
/// Принцип идентификации: <c>key</c> — относительный путь типа
|
||
/// <c>products/{productId:N}/{guid}.png</c>. URL для публичной отдачи
|
||
/// возвращает <see cref="PublicUrl"/> — для Local это
|
||
/// <c>/uploads/{key}</c>, для Minio — тот же путь, но nginx уже его
|
||
/// проксирует на API (см. GET /uploads/{**path}).</summary>
|
||
public interface IObjectStorage
|
||
{
|
||
/// <summary>Сохранить поток. Возвращает <c>key</c> (то что
|
||
/// каллер передал).</summary>
|
||
Task<string> SaveAsync(string key, Stream content, string contentType, CancellationToken ct = default);
|
||
|
||
/// <summary>Открыть на чтение. null если объекта нет.</summary>
|
||
Task<(Stream Stream, string ContentType)?> OpenAsync(string key, CancellationToken ct = default);
|
||
|
||
/// <summary>Удалить. Не бросает если объекта уже нет.</summary>
|
||
Task DeleteAsync(string key, CancellationToken ct = default);
|
||
|
||
/// <summary>Публичный URL для отдачи через API/nginx. Не presigned —
|
||
/// в обоих реализациях возвращает <c>/uploads/{key}</c>. Nginx/api
|
||
/// прокси решает, откуда взять (диск или MinIO).</summary>
|
||
string PublicUrl(string key) => $"/uploads/{key.TrimStart('/')}";
|
||
|
||
/// <summary>Имя реализации — для логов и диагностики.</summary>
|
||
string Kind { get; }
|
||
}
|