Gargar.Common.Storage 2.0.1

Gargar.Common.Storage

A .NET storage library with support for SeaweedFS and AWS S3, featuring bulk operations, metadata management, progress tracking, automatic thumbnail generation, and file validation.

2.0 — breaking change. Was Gargar.Common.Minio in 1.x. Minio support removed; SeaweedFS replaces it via its S3-compatible gateway. See Migration from 1.x.

Features

  • Multi-Provider Support: SeaweedFS (via weed s3) and AWS S3
  • Bulk Operations: Upload/delete multiple files efficiently
  • Metadata Management: Store and retrieve custom metadata
  • Progress Tracking: Monitor upload/download progress
  • Thumbnail Generation: Automatically create image thumbnails in multiple sizes
  • File Validation: Validate file types, sizes, and dimensions
  • Comprehensive API: Rich set of methods for all storage operations

Installation

From Private NuGet Feed

dotnet add package Gargar.Common.Storage

From Local Project Reference

<ItemGroup>
  <ProjectReference Include="path\to\Gargar.Common.Storage\Gargar.Common.Storage.csproj" />
</ItemGroup>

Quick Start

1. Configure Services

Using SeaweedFS:

Run a SeaweedFS server with the S3 gateway enabled:

weed server -s3 -dir=./data -s3.port=8333

Register the storage service:

using Gargar.Common.Storage.Extensions;
using Gargar.Common.Storage.Enums;

builder.Services.AddSeaweedFsStorage(options =>
{
    options.Provider = StorageProvider.SeaweedFs;
    options.AccessKey = "your-access-key";
    options.SecretKey = "your-secret-key";
    options.BucketName = "your-bucket";
    options.ServiceUrl = "seaweed.internal";   // host of `weed s3`
    options.Port = 8333;                       // SeaweedFS s3 default
    options.UseSSL = false;
    options.Region = "us-east-1";              // placeholder; SeaweedFS ignores
});

Using AWS S3:

builder.Services.AddAwsS3Storage(options =>
{
    options.Provider = StorageProvider.AwsS3;
    options.AccessKey = "your-aws-access-key";
    options.SecretKey = "your-aws-secret-key";
    options.BucketName = "your-bucket";
    options.Region = "us-east-1";
});

Auto-Detection:

builder.Services.AddStorage(options =>
{
    options.Provider = StorageProvider.SeaweedFs; // or StorageProvider.AwsS3
    // ... other options
});

2. Using Configuration Files

appsettings.json:

{
  "S3Options": {
    "Provider": "SeaweedFs",
    "AccessKey": "your-access-key",
    "SecretKey": "your-secret-key",
    "BucketName": "your-bucket",
    "ServiceUrl": "seaweed.internal",
    "Port": 8333,
    "UseSSL": false,
    "PublicUrlTemplate": "https://cdn.example.com/{bucket}/{filename}",
    "ValidationRules": {
      "MaxFileSizeBytes": 104857600,
      "AllowedExtensions": [".jpg", ".jpeg", ".png", ".gif"],
      "AllowedMimeTypes": ["image/jpeg", "image/png", "image/gif"],
      "MaxImageWidth": 4000,
      "MaxImageHeight": 4000
    }
  }
}

Configure from appsettings:

builder.Services.AddStorageFromConfiguration(configuration, "S3Options");

Migration from 1.x (Minio) → 2.0 (SeaweedFS)

1.x (Minio) 2.0 (SeaweedFS)
Package: Gargar.Common.Minio Package: Gargar.Common.Storage
Namespace: Gargar.Common.Minio.* Namespace: Gargar.Common.Storage.*
services.AddMinioStorage(...) services.AddSeaweedFsStorage(...)
StorageProvider.Minio StorageProvider.SeaweedFs
Minio server (minio server) SeaweedFS s3 gateway (weed server -s3)
Default port: 9000 Default port: 8333
Minio NuGet dependency Removed — uses AWSSDK.S3 against weed s3

IStorageService API surface is unchanged. Only registration call + namespaces change.

Data migration is out of scope for this library — copy buckets between Minio and SeaweedFS with mc mirror or aws s3 sync.

Usage Examples

Basic Upload and Download

public class FileController : ControllerBase
{
    private readonly IStorageService _storage;

    public FileController(IStorageService storage)
    {
        _storage = storage;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> UploadFile(IFormFile file)
    {
        var result = await _storage.UploadAsync(file);

        if (result.Success)
        {
            return Ok(new { url = result.Url, fileName = result.FileName });
        }

        return BadRequest(result.ErrorMessage);
    }

    [HttpGet("download/{fileName}")]
    public async Task<IActionResult> DownloadFile(string fileName)
    {
        var data = await _storage.DownloadAsync(fileName);
        return File(data, "application/octet-stream", fileName);
    }
}

Upload with Options

var options = new UploadOptions
{
    GenerateUniqueName = true,
    MakePublic = false,
    ValidateFile = true,
    Folder = "images/products",
    Metadata = new Dictionary<string, string>
    {
        ["category"] = "product",
        ["uploadedBy"] = "admin"
    },
    UrlExpiry = TimeSpan.FromHours(1)
};

var result = await _storage.UploadAsync(file, customFileName: null, options);

Bulk Operations

// Upload multiple files
var files = Request.Form.Files;
var results = await _storage.UploadManyAsync(files);

foreach (var result in results)
{
    Console.WriteLine($"Uploaded: {result.FileName} - {result.Url}");
}

// Delete multiple files
var fileNames = new[] { "file1.jpg", "file2.png", "file3.pdf" };
await _storage.DeleteManyAsync(fileNames);

Progress Tracking

var progress = new Progress<ProgressInfo>(info =>
{
    Console.WriteLine($"Progress: {info.PercentComplete:F1}% - {info.Operation}");
});

var result = await _storage.UploadAsync(file, progress: progress);

Thumbnail Generation

var sizes = new[]
{
    ThumbnailSize.Small,    // 150x150
    ThumbnailSize.Medium,   // 300x300
    ThumbnailSize.Large     // 600x600
};

var result = await _storage.UploadWithThumbnailAsync(imageFile, sizes);

Console.WriteLine($"Original: {result.Url}");
foreach (var (size, url) in result.ThumbnailUrls)
{
    Console.WriteLine($"Thumbnail {size}: {url}");
}

// Get a specific thumbnail
var thumbnailData = await _storage.GetThumbnailAsync("image.jpg", ThumbnailSize.Medium);

File Validation

var validationResult = await _storage.ValidateFileAsync(file);

if (!validationResult.IsValid)
{
    foreach (var error in validationResult.Errors)
    {
        Console.WriteLine($"Validation error: {error}");
    }
    return BadRequest(validationResult.Errors);
}

var result = await _storage.UploadAsync(file);

Metadata Operations

var metadata = new Dictionary<string, string>
{
    ["author"] = "John Doe",
    ["department"] = "Engineering",
    ["project"] = "ProjectX"
};
await _storage.SetMetadataAsync("document.pdf", metadata);

var fileMetadata = await _storage.GetMetadataAsync("document.pdf");
if (fileMetadata != null)
{
    Console.WriteLine($"Size: {fileMetadata.Size} bytes");
    Console.WriteLine($"Last Modified: {fileMetadata.LastModified}");
    Console.WriteLine($"Content Type: {fileMetadata.ContentType}");

    foreach (var (key, value) in fileMetadata.CustomMetadata)
    {
        Console.WriteLine($"{key}: {value}");
    }
}

File Operations

if (await _storage.ExistsAsync("myfile.pdf"))
{
    Console.WriteLine("File exists!");
}

var url = await _storage.GetUrlAsync("myfile.pdf", TimeSpan.FromHours(24));

var allFiles = await _storage.ListFilesAsync();
var imageFiles = await _storage.ListFilesAsync(prefix: "images/");

await _storage.DeleteAsync("oldfile.pdf");

Stream Operations

using var fileStream = File.OpenRead("local-file.pdf");
var result = await _storage.UploadAsync(fileStream, "uploaded-file.pdf", "application/pdf");

using var stream = await _storage.DownloadAsStreamAsync("file.pdf");
// Process stream...

byte[] data = File.ReadAllBytes("file.pdf");
var result = await _storage.UploadAsync(data, "file.pdf", "application/pdf");

Advanced Configuration

Custom URL Templates

builder.Services.AddSeaweedFsStorage(options =>
{
    options.PublicUrlTemplate = "https://cdn.example.com/{bucket}/{filename}";
    // Files will use this URL pattern instead of presigned URLs
});

Validation Rules

builder.Services.AddSeaweedFsStorage(options =>
{
    options.ValidationRules = new FileValidationRules
    {
        MaxFileSizeBytes = 50 * 1024 * 1024, // 50MB
        AllowedExtensions = new HashSet<string> { ".jpg", ".png", ".pdf" },
        AllowedMimeTypes = new HashSet<string> { "image/jpeg", "image/png", "application/pdf" },
        MaxImageWidth = 3000,
        MaxImageHeight = 3000,
        MinImageWidth = 100,
        MinImageHeight = 100
    };
});

SeaweedFS notes

  • Uses SeaweedFS's S3-compatible gateway (weed s3 / weed server -s3). No native filer HTTP client — keeps the dependency surface small.
  • ForcePathStyle = true is set automatically — required by SeaweedFS.
  • Region defaults to us-east-1 if unset; SeaweedFS ignores the value but the AWS SDK requires one.
  • ACL MakePublic (canned PublicRead) is accepted but enforcement is limited in SeaweedFS. Prefer PublicUrlTemplate + a reverse proxy for public files.

API Reference

IStorageService Interface

Upload Methods

  • UploadAsync(Stream, string, string?, UploadOptions?, IProgress?, CancellationToken) — Upload from stream
  • UploadAsync(byte[], string, string?, UploadOptions?, CancellationToken) — Upload from byte array
  • UploadAsync(IFormFile, string?, UploadOptions?, IProgress?, CancellationToken) — Upload from form file
  • UploadManyAsync(IEnumerable<IFormFile>, UploadOptions?, IProgress?, CancellationToken) — Bulk upload

Download Methods

  • DownloadAsync(string, CancellationToken) — Download as byte array
  • DownloadAsStreamAsync(string, CancellationToken) — Download as stream

File Operations

  • DeleteAsync(string, CancellationToken) — Delete single file
  • DeleteManyAsync(IEnumerable<string>, CancellationToken) — Delete multiple files
  • ExistsAsync(string, CancellationToken) — Check if file exists
  • GetUrlAsync(string, TimeSpan?, CancellationToken) — Get file URL
  • ListFilesAsync(string?, CancellationToken) — List files

Metadata Operations

  • GetMetadataAsync(string, CancellationToken) — Get file metadata
  • SetMetadataAsync(string, Dictionary<string,string>, CancellationToken) — Set metadata

Thumbnail Operations

  • UploadWithThumbnailAsync(IFormFile, ThumbnailSize[], UploadOptions?, CancellationToken) — Upload with thumbnails
  • GetThumbnailAsync(string, ThumbnailSize, CancellationToken) — Get thumbnail

Validation

  • ValidateFileAsync(IFormFile) — Validate file

Models

UploadResult

public class UploadResult
{
    public bool Success { get; set; }
    public string FileName { get; set; }
    public string Url { get; set; }
    public long Size { get; set; }
    public string? ContentType { get; set; }
    public string? ErrorMessage { get; set; }
    public Dictionary<string, string>? Metadata { get; set; }
    public Dictionary<string, string>? ThumbnailUrls { get; set; }
}

FileMetadata

public class FileMetadata
{
    public string FileName { get; set; }
    public string? ContentType { get; set; }
    public long Size { get; set; }
    public DateTime LastModified { get; set; }
    public string? ETag { get; set; }
    public Dictionary<string, string> CustomMetadata { get; set; }
}

ProgressInfo

public class ProgressInfo
{
    public string FileName { get; set; }
    public long TotalBytes { get; set; }
    public long ProcessedBytes { get; set; }
    public double PercentComplete { get; }
    public string Operation { get; set; }
}

Dependencies

  • AWSSDK.S3 (3.7.408) — S3 SDK (used for both AWS S3 and SeaweedFS)
  • SixLabors.ImageSharp (3.1.12) — Image processing
  • Microsoft.Extensions.Options (9.0.1)
  • Microsoft.Extensions.Configuration.Abstractions / .Binder (9.0.1)
  • Microsoft.Extensions.DependencyInjection.Abstractions (9.0.1)
  • Microsoft.AspNetCore.Http (2.3.9)
  • Microsoft.Extensions.Logging.Abstractions (9.0.1)

Target Framework

  • .NET 9.0

Publishing to NuGet

Pack the Library

dotnet pack Gargar.Common.Storage/Gargar.Common.Storage.csproj -c Release

Push to Private Feed

dotnet nuget push Gargar.Common.Storage/bin/Release/Gargar.Common.Storage.2.0.0.nupkg --source "your-feed-url" --api-key "your-key"

Troubleshooting

Connection Issues

  1. Confirm SeaweedFS s3 gateway is reachable: curl http://<host>:8333/
  2. Verify access credentials match the weed s3 config (-iam.config if used)
  3. Confirm UseSSL matches the gateway scheme
  4. Check firewall rules and network connectivity

Validation Errors

  1. Review your ValidationRules configuration
  2. Check file size limits
  3. Verify allowed extensions and MIME types
  4. For images, check dimension requirements

Performance

  1. Use stream-based uploads when possible
  2. Consider using bulk operations for multiple files
  3. Implement progress tracking for user feedback
  4. Use appropriate thumbnail sizes to balance quality and size

License

Private — Gargar

Support

For issues or questions, contact the Gargar development team.

Changelog

Version 2.0.0

  • Breaking: Renamed package Gargar.Common.MinioGargar.Common.Storage
  • Breaking: Removed Minio SDK and AddMinioStorage(); replaced by SeaweedFS via S3 gateway
  • Breaking: StorageProvider.MinioStorageProvider.SeaweedFs
  • Breaking: Namespace Gargar.Common.Minio.*Gargar.Common.Storage.*
  • Renamed AwsS3StorageProviderS3StorageProvider (single impl serves both AWS and SeaweedFS)
  • Added AddSeaweedFsStorage() extension

Version 1.0.0

  • Initial release
  • Support for Minio and AWS S3
  • Bulk upload/delete operations
  • Metadata management
  • Progress tracking
  • Automatic thumbnail generation
  • File validation
  • Comprehensive API

No packages depend on Gargar.Common.Storage.

Version Downloads Last updated
2.0.1 35 04/13/2026
1.0.0 5 04/13/2026