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.Minioin 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 = trueis set automatically — required by SeaweedFS.Regiondefaults tous-east-1if unset; SeaweedFS ignores the value but the AWS SDK requires one.- ACL
MakePublic(cannedPublicRead) is accepted but enforcement is limited in SeaweedFS. PreferPublicUrlTemplate+ a reverse proxy for public files.
API Reference
IStorageService Interface
Upload Methods
UploadAsync(Stream, string, string?, UploadOptions?, IProgress?, CancellationToken)— Upload from streamUploadAsync(byte[], string, string?, UploadOptions?, CancellationToken)— Upload from byte arrayUploadAsync(IFormFile, string?, UploadOptions?, IProgress?, CancellationToken)— Upload from form fileUploadManyAsync(IEnumerable<IFormFile>, UploadOptions?, IProgress?, CancellationToken)— Bulk upload
Download Methods
DownloadAsync(string, CancellationToken)— Download as byte arrayDownloadAsStreamAsync(string, CancellationToken)— Download as stream
File Operations
DeleteAsync(string, CancellationToken)— Delete single fileDeleteManyAsync(IEnumerable<string>, CancellationToken)— Delete multiple filesExistsAsync(string, CancellationToken)— Check if file existsGetUrlAsync(string, TimeSpan?, CancellationToken)— Get file URLListFilesAsync(string?, CancellationToken)— List files
Metadata Operations
GetMetadataAsync(string, CancellationToken)— Get file metadataSetMetadataAsync(string, Dictionary<string,string>, CancellationToken)— Set metadata
Thumbnail Operations
UploadWithThumbnailAsync(IFormFile, ThumbnailSize[], UploadOptions?, CancellationToken)— Upload with thumbnailsGetThumbnailAsync(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
- Confirm SeaweedFS s3 gateway is reachable:
curl http://<host>:8333/ - Verify access credentials match the
weed s3config (-iam.configif used) - Confirm
UseSSLmatches the gateway scheme - Check firewall rules and network connectivity
Validation Errors
- Review your
ValidationRulesconfiguration - Check file size limits
- Verify allowed extensions and MIME types
- For images, check dimension requirements
Performance
- Use stream-based uploads when possible
- Consider using bulk operations for multiple files
- Implement progress tracking for user feedback
- 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.Minio→Gargar.Common.Storage - Breaking: Removed Minio SDK and
AddMinioStorage(); replaced by SeaweedFS via S3 gateway - Breaking:
StorageProvider.Minio→StorageProvider.SeaweedFs - Breaking: Namespace
Gargar.Common.Minio.*→Gargar.Common.Storage.* - Renamed
AwsS3StorageProvider→S3StorageProvider(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.
.NET 9.0
- AWSSDK.S3 (>= 3.7.408)
- Microsoft.AspNetCore.Http (>= 2.3.9)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.1)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.1)
- Microsoft.Extensions.Options (>= 9.0.1)
- SixLabors.ImageSharp (>= 3.1.12)
- System.Text.Encodings.Web (>= 10.0.1)