{"id":29687041,"url":"https://github.com/teoadal/storage","last_synced_at":"2025-07-23T04:08:28.341Z","repository":{"id":116116483,"uuid":"610076851","full_name":"teoadal/Storage","owner":"teoadal","description":"Simple client for S3-storage","archived":false,"fork":false,"pushed_at":"2025-04-29T07:41:15.000Z","size":159,"stargazers_count":65,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-01T15:54:10.494Z","etag":null,"topics":["minio","multipart-uploads","performance","s3","s3-buckets","s3-object-storage","s3-storage","yandex-object-storage"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/teoadal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-03-06T03:14:06.000Z","updated_at":"2025-05-05T16:04:47.000Z","dependencies_parsed_at":"2025-04-29T06:32:30.967Z","dependency_job_id":"552c27de-0f7d-452f-8335-c10383222a81","html_url":"https://github.com/teoadal/Storage","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/teoadal/Storage","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teoadal%2FStorage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teoadal%2FStorage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teoadal%2FStorage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teoadal%2FStorage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/teoadal","download_url":"https://codeload.github.com/teoadal/Storage/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teoadal%2FStorage/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266614700,"owners_count":23956424,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["minio","multipart-uploads","performance","s3","s3-buckets","s3-object-storage","s3-storage","yandex-object-storage"],"created_at":"2025-07-23T04:08:27.528Z","updated_at":"2025-07-23T04:08:28.320Z","avatar_url":"https://github.com/teoadal.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![.NET Core](https://github.com/teoadal/Storage/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/teoadal/Storage/actions/workflows/dotnet.yml)\n[![NuGet](https://img.shields.io/nuget/v/Storages3.svg)](https://www.nuget.org/packages/Storages3)\n[![NuGet](https://img.shields.io/nuget/dt/Storages3.svg)](https://www.nuget.org/packages/Storages3)\n[![codecov](https://codecov.io/gh/teoadal/Storage/branch/master/graph/badge.svg?token=8L4HN9FAIV)](https://codecov.io/gh/teoadal/Storage)\n[![CodeFactor](https://www.codefactor.io/repository/github/teoadal/storage/badge)](https://www.codefactor.io/repository/github/teoadal/storage)\n\n# Клиент для S3\n\nПривет! Это обертка над HttpClient для работы с S3 хранилищами. Мотивация создания была простейшей - я не понимал,\nпочему клиенты [AWS](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/welcome.html) (4.0.0)\nи [Minio](https://github.com/minio/minio-dotnet) (6.0.4) потребляют так много памяти. Результат экспериментов: скорость\nпочти как у AWS, а потребление памяти почти в 150 раз меньше, чем клиент для Minio (и в 17 для AWS).\n\n```ini\nBenchmarkDotNet v0.14.0, Debian GNU/Linux 12 (bookworm) (container)\nIntel Xeon CPU E5-2697 v3 2.60GHz, 1 CPU, 56 logical and 28 physical cores\n.NET SDK 8.0.408\n[Host]   : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2\n.NET 8.0 : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2\n\nJob=.NET 8.0  Runtime=.NET 8.0\n```\n| Method  | Mean    | Error    | StdDev   | Ratio | RatioSD |    Gen0 | Allocated | Alloc Ratio |\n|-------- |--------:|---------:|---------:|------:|--------:|--------:|----------:|------------:|\n| Aws     | 5.788 s | 0.0297 s | 0.0264 s |  1.05 |    0.02 |    1000 |  25.78 MB |       17.14 |\n| Minio   | 7.016 s | 0.0824 s | 0.0730 s |  1.27 |    0.03 |       - | 274.03 MB |      182.13 |\n| Storage | 5.510 s | 0.1063 s | 0.0994 s |  1.00 |    0.02 |       - |    1.5 MB |        1.00 |\n\n\n## Создание клиента\n\nДля работы с хранилищем необходимо создать клиент.\n\n```csharp\nvar storageClient = new S3Client(new S3Settings\n{\n    AccessKey = \"ROOTUSER\",\n    Bucket = \"mybucket\",\n    EndPoint = \"localhost\",     // для Yandex.Objects это \"storage.yandexcloud.net\"\n    Port = 9000,                // стандартный порт Minio - 9000, для Yandex.Objects указывать не нужно\n    SecretKey = \"ChangeMe123\",\n    UseHttps = false,           // для Yandex.Objects укажите true\n    UseHttp2 = false            // Yandex.Objects позволяет работать по HTTP2, можете указать true\n})\n```\nТакже, в конструктор клиента можно передать имплементацию интерфейса `IArrayPool`, которая позволяет тонко настроить переиспользование массивов клиента.\n\nMinio предоставляет [playground](https://play.min.io:9443) для тестирования (порт для запросов всё тот же - 9000). Ключи\nможно найти [в документации](https://min.io/docs/minio/linux/developers/python/minio-py.html#file-uploader-py). Доступ к\nAmazon S3 не тестировался.\n\n## Операции с S3 bucket\n\n### Создание bucket'a\n\nМы передаём название bucket'a в настройках, поэтому дополнительно его вводить не надо.\n\n```csharp\nbool bucketCreateResult = await storageClient.CreateBucket(cancellationToken);\nConsole.WriteLine(bucketCreateResult\n    ? \"Bucket создан\"\n    : \"Bucket не был создан\");\n```\n\n### Проверка существования bucket'a\n\nКак и в прошлый раз, мы знаем название bucket'a, так как мы передаём его в настройках клиента.\n\n```csharp\nbool bucketCheckResult = await storageClient.IsBucketExists(cancellationToken);\nif (bucketCheckResult) Console.WriteLine(\"Bucket существует\");\n```\n\n### Удаление bucket'a\n\n```csharp\nbool bucketDeleteResult = await storageClient.DeleteBucket(cancellationToken);\nif (bucketDeleteResult) Console.WriteLine(\"Bucket удалён\");\n```\n\n## Операции с S3 object\n\nНапомню, что объект в смысле S3 это и есть файл.\n\n### Создание файла\n\nСоздание, то есть загрузка файла в S3 хранилище, возможна двумя путями: можно разбить исходные данных на кусочки (\nmultipart), а можно не разбивать. Самый простой способ загрузки файла - воспользоваться следующим методом (если файл\nбудет больше 5 МБ, то применяется multipart):\n\n```csharp\nbool fileUploadResult = await storageClient.UploadFile(fileName, fileContentType, fileStream, cancellationToken);\nif (fileUploadResult) Console.WriteLine(\"Файл загружен\");\n```\n\n\n#### Управление Multipart-загрузкой\n\nДля самостоятельного управления multipart-загрузкой, можно воспользоваться методом `UploadFile` без указания данных. Получится примеоно такой код:\n\n```csharp\n\nusing S3Upload upload = await storageClient.UploadFile(fileName, fileType, cancellationToken);\n\nawait upload.AddParts(stream, cancellationToken); // загружаем части документа\nif (!await upload.AddParts(byteArray, cancellationToken)) { // загружаем другую часть документа\n    await upload.Abort(cancellationToken); // отменяем загрузку\n}\nelse {\n    await upload.Complete(cancellationToken); // завершаем загрузку\n}\n\n```\n\nВ коде клиента именно эту логику использует метод PutFileMultipart. Конкретную реализацию можно подсмотреть в нём.\n\n### Получение файла\n\n```csharp\nStorageFile fileGetResult = await storageClient.GetFile(fileName, cancellationToken);\nif (fileGetResult) {\n    Console.WriteLine($\"Размер файла {fileGetResult.Length}, контент {fileGetResult.ContetType}\");\n    return await fileGetResult.GetStream(cancellationToken);\n}\nelse {\n    Console.WriteLine($\"Файл не может быть загружен, так как {fileGetResult}\");\n}\n```\n\n### Получение файла как Stream\n\n```csharp\nvar fileStream = await storageClient.GetFileStream(fileName, cancellationToken);\n```\n\nВ случае, если файл не существует, возвратится `Stream.Null`.\n\n### Проверка существования файла\n\n```csharp\nbool fileExistsResult = await storageClient.IsFileExists(fileName, cancellationToken);\nif (fileExistsResult) {\n\tConsole.WriteLine(\"Файл существует\");\n}\n```\n\n### Создание подписанной ссылки на файл\n\nМетод проверяет наличие файла в хранилище S3 и формирует GET запрос файла. Параметр `expiration` должен содержать время\nвалидности ссылки начиная с даты формирования ссылки.\n\n```csharp\nstring? preSignedFileUrl = storageClient.GetFileUrl(fileName, expiration);\nif (preSignedFileUrl != null) {\n\tConsole.WriteLine($\"URL получен: {preSignedFileUrl}\");\n}\n```\n\nСуществует не безопасный способ создать ссылку, без проверки наличия файла в S3.\n\n```csharp\nstring preSignedFileUrl = await storageClient.BuildFileUrl(fileName, expiration, cancellationToken);\n```\n\n### Удаление\n\nУдаление объекта из S3 происходит почти мгновенно. На самом деле в S3 хранилище просто ставится задача на удаление и\nклиенту возвращается результат. Кстати, если удалить файл, который не существует, то ответ будет такой же, как если бы\nфайл существовал. Поэтому этот метод ничего не возвращает.\n\n```csharp\nawait storageClient.DeleteFile(fileName, cancellationToken);\nConsole.WriteLine(\"Файл удалён, если он, конечно, существовал\");\n```\n\n## Измерение производительности и тестирование\n\nЛокальное измерение производительности и тестирование осуществляется с помощью Minio в Docker'e по http. Понимаю, что\nэто не самый хороший способ, но зато он самый доступный и простой.\n\n1. Файл `docker-compose` для локального тестирования можно найти в репозитории.\n2. Запускаем `docker-compose up -d`. Если всё хорошо, то бенчмарк заработает в Docker'e.\n3. Если нужно запустить бенчмарк локально, то обращаем внимание на файл `appsettings.json`. В нём содержатся основные\n   настройки для подключения к Minio.\n4. Свойство `BigFilePath` файла `appsettings.json` сейчас не заполнено. Его можно использовать для загрузки реального\n   файла (больше 100МБ). Если свойство не заполнено, то тест сгенерирует случайную последовательность байт размером\n   123МБ в памяти.\n\n## Вопросы\n\nУ меня есть канал в TG: [@csharp_gepard](https://t.me/csharp_gepard/91). К нему привязан чат - вопросы можно задавать в чате, либо в любом из последних постов.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteoadal%2Fstorage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fteoadal%2Fstorage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteoadal%2Fstorage/lists"}