Подключение MinIO (S3-совместимое хранилище) в ASP.NET Core

Иван Гурин Технологии интеграции информационных систем 6 мин

S3 (Simple Storage Service) - это объектное хранилище для хранения и отдачи файлов по HTTP API. В отличие от хранения файлов в файловой системе сервера, S3-подход:

  • не привязан к конкретному инстансу приложения;
  • масштабируется горизонтально;
  • корректно работает в Docker и Kubernetes;
  • отделяет хранение файлов от бизнес-логики.

MinIO - это self-hosted S3-совместимое хранилище. Оно реализует API, совместимое с Amazon S3, но может быть запущено локально или в приватном облаке.

Где используется S3

Типовые сценарии:

  • пользовательские загрузки (файлы, документы, аватары);
  • хранение больших бинарных объектов;
  • раздача файлов без проксирования через backend;
  • резервное копирование;
  • временные и технические файлы.

Архитектура примера

Поток данных:

[HTTP Client]
      |
      v
[ASP.NET Core API]
      |
      v
[MinIO (S3 API)]
      |
      v
[Bucket / Objects]
  • API принимает файл через multipart/form-data
  • сервис сохраняет файл в MinIO
  • по запросу файл возвращается клиенту

Практический пример

Установка пакетов

Добавьте NuGet-пакет (через консоль диспетчера пакетов):

Install-Package Minio

Сервис работы с файлами FileStorageService

using Minio;
using Minio.DataModel.Args;

public class FileStorageService
{
    private readonly IMinioClient _minio;
    
    public FileStorageService(IMinioClient minio)
    {
        _minio = minio;
    }

    public async Task UploadAsync(string objectName, string bucket, Stream content, string contentType)
    {
        var exists = await _minio.BucketExistsAsync(
            new BucketExistsArgs().WithBucket(bucket));

        if (!exists)
        {
            await _minio.MakeBucketAsync(
                new MakeBucketArgs().WithBucket(bucket));
        }

        await _minio.PutObjectAsync(
            new PutObjectArgs()
                .WithBucket(bucket)
                .WithObject(objectName)
                .WithStreamData(content)
                .WithObjectSize(content.Length)
                .WithContentType(contentType));
    }

    public async Task<Stream> DownloadAsync(string objectName, string bucket)
    {
        var memory = new MemoryStream();

        await _minio.GetObjectAsync(
            new GetObjectArgs()
                .WithBucket(bucket)
                .WithObject(objectName)
                .WithCallbackStream(stream =>
                {
                    stream.CopyTo(memory);
                }));

        memory.Position = 0;
        return memory;
    }
}

Регистрация в DI

builder.Services.AddSingleton(_ =>
{
    return new MinioClient()
        .WithEndpoint("localhost:9000")
        .WithCredentials("minio_access_key", "minio_secret_key")
        .WithSSL(false) // только для локальной разработки
        .Build();
});

builder.Services.AddScoped<FileStorageService>();

Контроллер FilesController

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/files")]
public class FilesController : ControllerBase
{
    private readonly FileStorageService _storage;

    public FilesController(FileStorageService storage)
    {
        _storage = storage;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("File is empty");

        await using var stream = file.OpenReadStream();

        await _storage.UploadAsync(
            file.FileName,
            "files",
            stream,
            file.ContentType);

        return Ok(new { file = file.FileName });
    }

    [HttpGet("download/{name}")]
    public async Task<IActionResult> Download(string name)
    {
        var stream = await _storage.DownloadAsync(name, "files");

        return File(
            stream,
            "application/octet-stream",
            name);
    }
}

docker-compose.yml для MinIO

services:
  minio-s3:
    image: minio/minio
    container_name: minio-s3
    hostname: minio-s3
    restart: always
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - ./storage:/data
    environment:
      MINIO_ACCESS_KEY: minio_access_key
      MINIO_SECRET_KEY: minio_secret_key
    command: server /data --console-address ":9001"

Проверка работы

  1. Запустить MinIO:

    docker-compose up -d
    
  2. Запустить ASP.NET Core приложение

  3. Загрузить файл:

    curl --location 'http://localhost/api/files/upload' --form 'file=@"file.png"'
    
  4. Скачать файл:

    curl --location 'http://localhost/api/files/download/file.png'
    

Методические ремарки

Упрощения

  • bucket создаётся при первом запросе;
  • файл читается в память при скачивании;
  • отсутствуют таймауты.

Для production

  • использовать streaming вместо MemoryStream;
  • ограничить размер файлов;
  • использовать presigned URLs;
  • вынести секреты в переменные окружения;
  • добавить логирование и политику повторов.

Заключение

MinIO позволяет использовать S3-подход без привязки к облачному провайдеру. Пример демонстрирует минимальную, но рабочую интеграцию с ASP.NET Core и может служить базой для production-решения.