StashPup.Core
1.0.0
dotnet add package StashPup.Core --version 1.0.0
NuGet\Install-Package StashPup.Core -Version 1.0.0
<PackageReference Include="StashPup.Core" Version="1.0.0" />
<PackageVersion Include="StashPup.Core" Version="1.0.0" />
<PackageReference Include="StashPup.Core" />
paket add StashPup.Core --version 1.0.0
#r "nuget: StashPup.Core, 1.0.0"
#:package StashPup.Core@1.0.0
#addin nuget:?package=StashPup.Core&version=1.0.0
#tool nuget:?package=StashPup.Core&version=1.0.0
StashPup ๐ถ
<p align="center"> <img src="stashpup.png" alt="StashPup Logo" width="200" style="border-radius: 20px;"/> </p>
A flexible, provider-agnostic file storage library for .NET with built-in ASP.NET Core integration.
StashPup simplifies file storage in .NET applications by providing a unified interface for local filesystem, AWS S3, and Azure Blob Storage. It includes advanced features like thumbnail generation, signed URLs, metadata management, and powerful search capabilities.
โจ Features
Core Capabilities
- โ Multi-Provider Support - Seamlessly switch between Local, S3, and Azure Blob Storage
- โ Railway-Oriented Error Handling - Type-safe Result<T> pattern instead of exceptions
- โ Advanced Search - Filter by name patterns, content type, size, dates, and custom metadata
- โ Thumbnail Generation - Automatic image thumbnail creation with configurable sizes
- โ File Metadata - Store and query custom key-value metadata with each file
- โ Bulk Operations - Upload or delete multiple files in a single operation
- โ Signed URLs - Generate time-limited secure download links
- โ Content Type Detection - Magic byte detection for accurate MIME types
- โ Pagination - Built-in paginated listing and search results
- โ SHA-256 Hashing - Optional file integrity verification
ASP.NET Core Integration
- โ Dependency Injection - First-class DI support with fluent configuration
- โ Minimal API Endpoints - Pre-built REST endpoints for common operations
- โ File Serving Middleware - Serve local files via HTTP with optional signed URL validation
- โ IFormFile Support - Direct upload from ASP.NET Core forms
- โ Configuration-Driven - Configure via appsettings.json or code
๐ฆ Installation
# Core library (required)
dotnet add package StashPup.Core
# Choose your storage provider(s)
dotnet add package StashPup.Storage.Local
dotnet add package StashPup.Storage.S3
dotnet add package StashPup.Storage.Azure
# ASP.NET Core integration (optional)
dotnet add package StashPup.AspNetCore
๐ Quick Start
1. Basic Setup (Minimal API)
var builder = WebApplication.CreateBuilder(args);
// Add StashPup with local storage
builder.Services.AddStashPup(stash => stash
.UseLocalStorage(options =>
{
options.BasePath = "./uploads";
options.BaseUrl = "/files";
options.MaxFileSizeBytes = 10 * 1024 * 1024; // 10MB
options.AllowedExtensions = [".jpg", ".png", ".pdf"];
}));
var app = builder.Build();
// Add pre-built endpoints
app.MapStashPupEndpoints("/api/files");
app.Run();
2. Upload a File
app.MapPost("/upload", async (IFormFile file, IFileStorage storage) =>
{
await using var stream = file.OpenReadStream();
var result = await storage.SaveAsync(
stream,
file.FileName,
folder: "documents",
metadata: new Dictionary<string, string>
{
["uploaded-by"] = "john@example.com",
["category"] = "invoice"
});
return result.Success
? Results.Ok(result.Data)
: Results.BadRequest(result.ErrorMessage);
});
3. Download a File
app.MapGet("/download/{id:guid}", async (Guid id, IFileStorage storage) =>
{
var result = await storage.GetAsync(id);
var metadata = await storage.GetMetadataAsync(id);
return result.Success
? Results.File(result.Data!, metadata.Data!.ContentType, metadata.Data.Name)
: Results.NotFound();
});
๐ฏ Core Concepts
Result Pattern
StashPup uses a railway-oriented programming approach. All operations return Result<T>:
public class Result<T>
{
public bool Success { get; }
public T? Data { get; }
public string? ErrorMessage { get; }
public string? ErrorCode { get; }
}
Usage:
var result = await storage.SaveAsync(stream, "photo.jpg");
if (result.Success)
{
Console.WriteLine($"File saved with ID: {result.Data!.Id}");
}
else
{
Console.WriteLine($"Error: {result.ErrorMessage} (Code: {result.ErrorCode})");
}
// Or use implicit bool conversion
if (result)
{
// Success!
}
File Records
Every stored file has an associated FileRecord with rich metadata:
public class FileRecord
{
public Guid Id { get; set; } // Unique identifier
public string Name { get; set; } // Current name
public string OriginalName { get; set; } // Original upload name
public string Extension { get; set; } // File extension
public string ContentType { get; set; } // MIME type
public long SizeBytes { get; set; } // File size
public DateTime CreatedAtUtc { get; set; } // Creation timestamp
public DateTime UpdatedAtUtc { get; set; } // Last modified
public string? Hash { get; set; } // SHA-256 hash (optional)
public string? Folder { get; set; } // Folder/prefix
public string StoragePath { get; set; } // Provider-specific path
public Dictionary<string, string>? Metadata { get; set; } // Custom metadata
}
๐ง Storage Providers
Local Filesystem
builder.Services.AddStashPup(stash => stash
.UseLocalStorage(options =>
{
options.BasePath = "./uploads";
options.BaseUrl = "/files";
options.OverwriteExisting = false;
options.AutoCreateDirectories = true;
options.EnableSignedUrls = true;
options.SigningKey = "your-secret-key-here";
}));
// Enable file serving middleware
app.UseStashPup();
AWS S3
builder.Services.AddStashPup(stash => stash
.UseS3(options =>
{
options.BucketName = "my-bucket";
options.Region = "us-east-1";
options.AccessKeyId = "AKIA...";
options.SecretAccessKey = "secret";
options.PublicRead = false;
options.EnableEncryption = true;
options.StorageClass = "STANDARD";
}));
Azure Blob Storage
builder.Services.AddStashPup(stash => stash
.UseAzureBlob(options =>
{
options.ConnectionString = "DefaultEndpointsProtocol=https;...";
options.ContainerName = "uploads";
options.AccessTier = "Hot";
options.PublicAccess = false;
options.CreateContainerIfNotExists = true;
}));
Configuration via appsettings.json
{
"StashPup": {
"Provider": "S3",
"S3": {
"BucketName": "my-bucket",
"Region": "us-east-1",
"EnableEncryption": true
}
}
}
builder.Services.AddStashPup(builder.Configuration);
๐ Common Operations
Search Files
var searchParams = new SearchParameters
{
NamePattern = "invoice*.pdf",
ContentType = "application/pdf",
MinSizeBytes = 1024,
CreatedAfter = DateTime.UtcNow.AddDays(-30),
Metadata = new Dictionary<string, string>
{
["category"] = "invoice"
},
SortBy = SearchSortField.CreatedAt,
SortDirection = SearchSortDirection.Descending,
Page = 1,
PageSize = 20
};
var result = await storage.SearchAsync(searchParams);
Generate Thumbnails
var thumbnailResult = await storage.GetThumbnailAsync(
fileId,
ThumbnailSize.Medium);
if (thumbnailResult.Success)
{
return Results.File(thumbnailResult.Data!, "image/jpeg");
}
Move Files Between Folders
var result = await storage.MoveAsync(
id: fileId,
newFolder: "archive/2024");
Copy Files
var result = await storage.CopyAsync(
id: fileId,
newFolder: "backups");
// Returns a new FileRecord with a new ID
Console.WriteLine($"Copied to new ID: {result.Data!.Id}");
Bulk Operations
// Bulk upload
var items = new[]
{
new BulkSaveItem(stream1, "file1.jpg", "images"),
new BulkSaveItem(stream2, "file2.jpg", "images"),
new BulkSaveItem(stream3, "file3.jpg", "images")
};
var results = await storage.BulkSaveAsync(items);
// Bulk delete
var idsToDelete = new[] { id1, id2, id3 };
await storage.BulkDeleteAsync(idsToDelete);
// Bulk move to new folder
var idsToMove = new[] { id1, id2, id3 };
var movedFiles = await storage.BulkMoveAsync(idsToMove, "archive/2024");
Folder Operations
StashPup uses a fully virtual folder model - folders are just path prefixes on files. This means:
- โ Folders are created automatically when you upload files to a path
- โ StashPup handles all folder operations (list, move, delete, search)
- โ Your app manages "empty folder" state in its own database if needed
- โ Works consistently across Local, S3, and Azure storage
// List all unique folder paths (folders that have files)
var foldersResult = await storage.ListFoldersAsync();
foreach (var folder in foldersResult.Data!)
{
Console.WriteLine($"Folder: {folder}");
}
// List immediate children of a parent folder
var childFolders = await storage.ListFoldersAsync(parentFolder: "projects");
// Delete folder and all contents (recursive)
var deleteResult = await storage.DeleteFolderAsync(
folder: "temp/uploads",
recursive: true);
Console.WriteLine($"Deleted {deleteResult.Data} files");
// Delete only files in exact folder (non-recursive)
var deleteExact = await storage.DeleteFolderAsync(
folder: "temp",
recursive: false);
Implementing Empty Folders in Your App
StashPup doesn't manage empty folders - that's your app's responsibility! Here's how:
// 1. Create database table for empty folders
public class EmptyFolder
{
public string Path { get; set; }
public DateTime CreatedAt { get; set; }
public Guid UserId { get; set; }
}
// 2. When user creates folder
db.EmptyFolders.Add(new EmptyFolder { Path = "Photos/2024", UserId = currentUserId });
await db.SaveChangesAsync();
// 3. Display merged list of folders
var realFolders = await storage.ListFoldersAsync();
var emptyFolders = await db.EmptyFolders.Where(f => f.UserId == currentUserId).ToListAsync();
var allFolders = realFolders.Data.Union(emptyFolders.Select(f => f.Path));
// 4. When file uploaded to empty folder, remove it
if (await db.EmptyFolders.AnyAsync(f => f.Path == uploadFolder))
{
db.EmptyFolders.RemoveRange(db.EmptyFolders.Where(f => f.Path == uploadFolder));
await db.SaveChangesAsync();
}
This keeps clean separation: StashPup handles files, your app handles empty folder UI!
Advanced Folder Search
// Search with enhanced folder filtering
var searchParams = new SearchParameters
{
FolderStartsWith = "projects/2024", // Match folders starting with prefix
IncludeSubfolders = true, // Include nested subfolders (default)
Page = 1,
PageSize = 50
};
var result = await storage.SearchAsync(searchParams);
// Search only in immediate folder (no subfolders)
var exactFolderSearch = new SearchParameters
{
Folder = "documents",
IncludeSubfolders = false, // Only files in "documents", not "documents/2024"
};
๐ Signed URLs
Generate Signed URLs (S3 & Azure)
// Time-limited download URL
var urlResult = await storage.GetSignedUrlAsync(
fileId,
expiry: TimeSpan.FromHours(1));
if (urlResult.Success)
{
// Share this URL with users
var downloadUrl = urlResult.Data;
}
Local Storage Signed URLs
// Configure
options.EnableSignedUrls = true;
options.SigningKey = "your-secret-key";
// Generate
var url = storage.GetSignedUrl(fileId, TimeSpan.FromMinutes(30));
// Returns: /files/{id}?expires=...&signature=...
// Middleware automatically validates signatures
app.UseStashPup();
๐จ Validation & Security
Configure File Restrictions
options.MaxFileSizeBytes = 50 * 1024 * 1024; // 50MB
options.AllowedExtensions = [".jpg", ".png", ".gif", ".webp"];
options.AllowedContentTypes = ["image/*"]; // Supports wildcards
options.ComputeHash = true; // Enable SHA-256 hashing
Content Type Detection
StashPup automatically detects content types using magic byte analysis:
// Detects based on file signature, not just extension
var result = await storage.SaveAsync(stream, "photo.jpg");
// result.Data.ContentType = "image/jpeg" (verified via magic bytes)
๐ ASP.NET Core Integration
Pre-built Endpoints
app.MapStashPupEndpoints("/api/files", options =>
{
options.RequireAuthorization = true;
options.EnableUpload = true;
options.EnableDownload = true;
options.EnableDelete = true;
options.EnableMetadata = true;
options.EnableList = false; // Disabled by default for security
options.EnableFolderList = false; // Disabled by default for security
options.EnableFolderDelete = true;
options.EnableBulkMove = true;
});
Available Endpoints:
POST /api/files- Upload fileGET /api/files/{id}- Download fileDELETE /api/files/{id}- Delete fileGET /api/files/{id}/metadata- Get metadataGET /api/files?folder=...&page=1&pageSize=20- List files (opt-in)GET /api/files/folders?parent=...- List all folder paths (opt-in)DELETE /api/files/folders/{path}?recursive=true- Delete folder and contentsPOST /api/files/bulk-move- Move multiple files to new folder
Custom Endpoints
app.MapPost("/api/upload-avatar", async (
IFormFile file,
IFileStorage storage,
ClaimsPrincipal user) =>
{
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
await using var stream = file.OpenReadStream();
var result = await storage.SaveAsync(
stream,
file.FileName,
folder: $"avatars/{userId}",
metadata: new Dictionary<string, string>
{
["user-id"] = userId!,
["upload-date"] = DateTime.UtcNow.ToString("O")
});
return result.Success
? Results.Ok(new { fileId = result.Data!.Id, url = $"/files/{result.Data.Id}" })
: Results.BadRequest(new { error = result.ErrorMessage });
});
๐ Advanced Usage
Custom Naming Strategy
options.NamingStrategy = (originalFileName) =>
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var extension = Path.GetExtension(originalFileName);
return $"{timestamp}_{Guid.NewGuid()}{extension}";
};
Custom Subfolder Strategy
options.SubfolderStrategy = (fileRecord) =>
{
// Organize by year/month
var now = DateTime.UtcNow;
return $"{now.Year}/{now.Month:D2}";
};
Direct IFileStorage Usage
public class DocumentService
{
private readonly IFileStorage _storage;
public DocumentService(IFileStorage storage)
{
_storage = storage;
}
public async Task<Result<FileRecord>> SaveInvoice(
Stream pdfStream,
string customerId)
{
return await _storage.SaveAsync(
content: pdfStream,
fileName: $"invoice_{customerId}.pdf",
folder: $"invoices/{DateTime.UtcNow.Year}",
metadata: new Dictionary<string, string>
{
["customer-id"] = customerId,
["document-type"] = "invoice",
["processed"] = "false"
});
}
}
๐งช Testing
StashPup's interface-based design makes testing easy:
public class MockFileStorage : IFileStorage
{
private readonly Dictionary<Guid, FileRecord> _files = new();
public Task<Result<FileRecord>> SaveAsync(...)
{
var record = new FileRecord { Id = Guid.NewGuid(), ... };
_files[record.Id] = record;
return Task.FromResult(Result<FileRecord>.Ok(record));
}
// Implement other methods...
}
๐ Performance Tips
- Use Bulk Operations for multiple files to reduce round-trips
- Enable Hashing Selectively - only when integrity verification is needed
- Cache Thumbnails - they're automatically cached by storage providers
- Use Pagination - don't load all files at once
- Consider Storage Classes - use S3 GLACIER or Azure Cool tier for archives
- Stream Large Files - don't buffer entire files in memory
๐ Error Handling
var result = await storage.SaveAsync(stream, fileName);
if (!result.Success)
{
switch (result.ErrorCode)
{
case FileStorageErrors.MaxFileSizeExceeded:
return Results.BadRequest("File too large");
case FileStorageErrors.InvalidFileExtension:
return Results.BadRequest("File type not allowed");
case FileStorageErrors.FileAlreadyExists:
return Results.Conflict("File already exists");
default:
logger.LogError("Upload failed: {ErrorMessage}", result.ErrorMessage);
return Results.StatusCode(500);
}
}
๐ License
MIT License - see LICENSE file for details
๐ค Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.
๐ Additional Documentation
- Avatar Integration - Complete guide for avatar uploads in other projects
- Git Commit Guide - How to commit changes and publish packages
- Start Here - Quick overview and next steps
๐ Support
For questions, issues, or feature requests, please open an issue on GitHub.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on StashPup.Core:
| Package | Downloads |
|---|---|
|
StashPup.Storage.S3
AWS S3 storage provider for StashPup. Implements IFileStorage for Amazon S3 with pre-signed URLs, server-side encryption, and storage classes. Supports thumbnail caching and metadata storage in S3 object metadata. |
|
|
StashPup.Storage.Local
Local filesystem storage provider for StashPup. Implements IFileStorage for local disk storage with folder organization, metadata files, and thumbnail generation. Perfect for development, testing, or self-hosted scenarios. |
|
|
StashPup.Storage.Azure
Azure Blob Storage provider for StashPup. Implements IFileStorage for Azure Blob Storage with SAS tokens, access tiers, and public access support. Supports thumbnail caching and metadata storage in blob metadata. |
|
|
StashPup.AspNetCore
ASP.NET Core integration for StashPup. Provides pre-built minimal API endpoints, middleware for file serving, and IFormFile support. Includes dependency injection extensions for easy configuration. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version 0.2.0 - Folder Operations Update
- NEW: BulkMoveAsync() - Move multiple files at once
- NEW: ListFoldersAsync() - Get list of all unique folders
- NEW: DeleteFolderAsync() - Delete folder and all contents
- ENHANCED: SearchParameters with FolderStartsWith and IncludeSubfolders
- Better support for nested folder hierarchies
- Improved folder filtering in search operations
Version 0.1.0 - Initial Release
- Core IFileStorage interface
- Result pattern for error handling
- FileRecord model with complete metadata
- SearchParameters for advanced filtering
- Support for pagination, sorting, and bulk operations