Tooark.Storage
4.7.0
See the version list below for details.
dotnet add package Tooark.Storage --version 4.7.0
NuGet\Install-Package Tooark.Storage -Version 4.7.0
<PackageReference Include="Tooark.Storage" Version="4.7.0" />
<PackageVersion Include="Tooark.Storage" Version="4.7.0" />
<PackageReference Include="Tooark.Storage" />
paket add Tooark.Storage --version 4.7.0
#r "nuget: Tooark.Storage, 4.7.0"
#:package Tooark.Storage@4.7.0
#addin nuget:?package=Tooark.Storage&version=4.7.0
#tool nuget:?package=Tooark.Storage&version=4.7.0
Tooark.Storage
Library with the Tooark family's cloud object storage abstractions: the IStorageService contract (upload,
download, delete, metadata and signed URL), the StorageObjectDto, the common options and the providers' base class.
The package has no cloud SDK: each provider lives in its own package, and the application only loads the SDK of
the cloud it uses.
| Package | Provider | SDK |
|---|---|---|
Tooark.Storage.Aws |
Amazon S3 and compatible services (MinIO, LocalStack, R2) | AWSSDK.S3 |
Tooark.Storage.Gcp |
Google Cloud Storage | Google.Cloud.Storage.V1 |
Azure Blob Storage is planned as Tooark.Storage.Azure.
π Languages: πΊπΈ English (this file) Β· π§π· PortuguΓͺs
π Contents
- Overview
- Installation
- Configuration
- Components
- Usage Examples
- Dependencies
- Best Practices
- Error Codes and Solutions
- Contributing
- Help & Security
- Support
- License
π Overview
- One contract for any cloud β the application code depends only on
IStorageService. Switching from AWS to Google Cloud changes the registration inProgram.cs, not the code that uploads and downloads files. - Read and write signed URLs β the read URL delivers a private file without exposing the bucket; the write URL lets the browser send the file straight to the storage, without going through the API.
- Tooark errors β a missing object is a
NotFoundException; a missing bucket, denied access and provider failures areInternalServerErrorException, with the SDK exception kept inInnerException. - Validation before the provider β a blank or too long key, a missing bucket and an expiration out of range are rejected before any call to the cloud.
- One provider per application β the options live in the
Storagesection, and each operation can point to another bucket.
π§ Installation
Install the provider package, which brings this one:
dotnet add package Tooark.Storage.Aws
# or
dotnet add package Tooark.Storage.Gcp
The Tooark aggregator brings only this package, with the abstractions, and no provider: an application uses one
cloud, and it makes no sense to load both SDKs into every aggregator consumer.
βοΈ Configuration
appsettings.json
The common options live in the Storage section, together with the provider's
(AWS,
Google Cloud):
{
"Storage": {
"Bucket": "my-application-files",
"SignedUrlExpirationMinutes": 15
}
}
StorageOptions properties
| Property | Type | Default | Description |
|---|---|---|---|
Bucket |
string? |
β | Default bucket of the operations. Each operation can provide another |
SignedUrlExpirationMinutes |
int |
5 |
Default signed URL expiration, from 1 to 10080 minutes (7 days) |
The options are validated when the provider is registered: an expiration out of range fails at startup.
π¦ Components
IStorageService
Every operation accepts bucket (optional: without it, the options' bucket applies) and a CancellationToken.
| Method | Returns | Description |
|---|---|---|
UploadAsync(key, content, contentType?) |
StorageObjectDto |
Sends the object, replacing whatever exists at the key. The service does not close the stream |
DownloadAsync(key) |
Stream |
Downloads the content; the caller closes the stream. A missing object throws NotFoundException |
DeleteAsync(key) |
bool |
Deletes the object: true when deleted, false when it did not exist |
ExistsAsync(key) |
bool |
Tells whether the object exists |
GetInfoAsync(key) |
StorageObjectDto? |
Object data without downloading the content; null when it does not exist |
GetSignedUrlAsync(key, expiration?, access = Read) |
Uri |
Temporary URL to read (GET) or write (PUT) without credentials. Without expiration, the options' one applies |
The signed URL is computed locally, without querying the storage: generating it does not check whether the object exists.
StorageObjectDto
| Property | Type | Description |
|---|---|---|
Bucket |
string |
Object bucket |
Key |
string |
Object key (name) |
Size |
long? |
Size in bytes; null when uploading a non-seekable stream |
ContentType |
string? |
Content type (MIME) |
ETag |
string? |
Content version, without quotes; the format varies between providers |
LastModified |
DateTimeOffset? |
Last modification date, when the provider reports it |
ESignedUrlAccess
Read (default) generates a read URL (GET); Write generates a write URL (PUT).
StorageServiceBase
Base for a new provider. The public operations validate the arguments and resolve the bucket and the expiration; the
provider implements the protected methods UploadCoreAsync, DownloadCoreAsync, DeleteCoreAsync,
GetInfoCoreAsync and GetSignedUrlCoreAsync, which receive everything already resolved. ExistsAsync comes from
GetInfoCoreAsync.
| Validation | Exception | Message |
|---|---|---|
| Blank key | BadRequestException |
Storage.KeyRequired |
| Key longer than 1024 UTF-8 bytes | BadRequestException |
Storage.KeyTooLong;1024 |
| No bucket provided nor configured | InternalServerErrorException |
Storage.BucketNotConfigured |
| Expiration not positive or above 7 days | BadRequestException |
Storage.SignedUrlExpirationInvalid;10080 |
Provider errors
| Situation | Exception | Message |
|---|---|---|
| Missing object | NotFoundException |
Storage.ObjectNotFound |
| Missing bucket | InternalServerErrorException |
Storage.BucketNotFound |
| Access denied | InternalServerErrorException |
Storage.AccessDenied |
| Any other provider failure | InternalServerErrorException |
Storage.OperationFailed |
A missing bucket and denied access are InternalServerErrorException because they point to the application's
configuration, not to a caller error. Cancellation still arrives as OperationCanceledException.
π Usage Examples
Uploading a file received by the API
using Microsoft.AspNetCore.Mvc;
using Tooark.Storage.Interfaces;
[ApiController]
[Route("documents")]
public sealed class DocumentController(IStorageService storage) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Upload(IFormFile file, CancellationToken cancellationToken)
{
// The key is generated on the server, not taken from the user's file name
var key = $"documents/{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
await using var content = file.OpenReadStream();
var stored = await storage.UploadAsync(key, content, file.ContentType, cancellationToken: cancellationToken);
return Ok(new { stored.Key, stored.Size });
}
}
Temporary link to a private file
[HttpGet("{**key}")]
public async Task<IActionResult> Open(string key, CancellationToken cancellationToken)
{
if (!await storage.ExistsAsync(key, cancellationToken: cancellationToken))
{
return NotFound();
}
var url = await storage.GetSignedUrlAsync(key, TimeSpan.FromMinutes(10), cancellationToken: cancellationToken);
return Redirect(url.ToString());
}
Direct upload from the browser
The API generates the key and a write URL; the browser sends the file with PUT, without the content going through
the API. The bucket needs a CORS rule that allows PUT from the page's origin.
using Tooark.Storage.Enums;
[HttpPost("upload-url")]
public async Task<IActionResult> CreateUploadUrl(CancellationToken cancellationToken)
{
var key = $"uploads/{Guid.NewGuid()}";
var url = await storage.GetSignedUrlAsync(key, access: ESignedUrlAccess.Write, cancellationToken: cancellationToken);
return Ok(new { key, url });
}
const { key, url } = await api.post("/documents/upload-url");
await fetch(url, { method: "PUT", body: file });
@tooark/wysiwyg media
The @tooark/wysiwyg editor stores images and videos through the
uploadFile hook, and the saved JSON carries the URL. Save the key and return a path of your own API, which
generates a fresh signed URL on each access: a signed URL in the JSON would expire within 7 days at most. The relative
path starts with /, which the component and Tooark.Sanitizers accept.
editor.uploadFile = async (file) => {
const { key } = await api.upload(file); // POST /documents, as in the first example
return { src: `/documents/${key}`, alt: file.name }; // GET /documents/{key} redirects
};
Switching providers
Only the registration changes:
// builder.Services.AddTooarkStorageAws(builder.Configuration);
builder.Services.AddTooarkStorageGcp(builder.Configuration);
π Dependencies
| Package | Version | Description |
|---|---|---|
Tooark.Exceptions |
4.x | Exceptions (BadRequestException, NotFoundException and more) |
The package runs on the base .NET runtime, without ASP.NET Core and without any cloud SDK.
π― Best Practices
- Generate the key on the server β the user's file name can overwrite another object or carry unexpected paths; use a generated identifier and keep the original name elsewhere if you need it.
- Save the key, not the signed URL β the URL expires; generate a new one when the file is accessed.
- Private bucket and signed URL β instead of a public bucket, deliver each file with a short-lived URL.
- Credentials from the environment β prefer the AWS role or Google's Application Default Credentials to keys in the configuration; see each provider's README.
- Large files through a signed URL β downloading through the API holds the connection and, on Google Cloud, the memory; the read URL delivers the file straight from the storage.
β οΈ Error Codes and Solutions
| Message | Exception | Description | Solution |
|---|---|---|---|
Storage.KeyRequired |
BadRequestException |
Blank object key | Provide the key |
Storage.KeyTooLong;1024 |
BadRequestException |
Key longer than 1024 UTF-8 bytes | Shorten the key |
Storage.SignedUrlExpirationInvalid;10080 |
BadRequestException |
URL expiration not positive or above 7 days | Use an expiration between 1 minute and 7 days |
Storage.BucketNotConfigured |
InternalServerErrorException |
No bucket in the call nor in the options | Configure Storage:Bucket or provide the bucket in the call |
Storage.ObjectNotFound |
NotFoundException |
The object does not exist | Check the key; use ExistsAsync when absence is expected |
Storage.BucketNotFound |
InternalServerErrorException |
The bucket does not exist | Check the bucket name and the region or project |
Storage.AccessDenied |
InternalServerErrorException |
The credential has no permission on the bucket | Grant the permission to the application's identity |
Storage.OperationFailed |
InternalServerErrorException |
Provider or SDK failure | See the InnerException |
Options.Storage.SignedUrlExpirationInvalid;10080 |
InternalServerErrorException |
SignedUrlExpirationMinutes outside 1 to 10080 |
Fix the default expiration in the options |
Each provider's own errors are in its README.
π€ Contributing
Contributions are welcome! Start with CONTRIBUTING.md β it covers the development workflow, the coding and commit conventions and the pull request checklist. Bugs and feature requests go through the issue templates of the Tooark repository.
By participating you agree to the Code of Conduct.
π Help & Security
- β Questions, bugs, feature ideas β see SUPPORT.md for the right channel
- π Security vulnerabilities β do not open a public issue; follow SECURITY.md
π Support
If Tooark helps your projects, consider supporting its development:
- π GitHub Sponsors
- β Ko-fi
Every contribution helps keep the project maintained and improving. Thank you! π
π License
This project is licensed under the BSD 3-Clause License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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 is compatible. 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. |
-
net10.0
- Tooark.Exceptions (>= 4.7.0)
-
net8.0
- Tooark.Exceptions (>= 4.7.0)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Tooark.Storage:
| Package | Downloads |
|---|---|
|
Tooark
Package with all Tooark resources for .NET applications. |
|
|
Tooark.Storage.Aws
Package that implements the Tooark IStorageService on Amazon S3 and S3-compatible services (MinIO, LocalStack, Cloudflare R2), with presigned URLs for reading and writing. |
|
|
Tooark.Storage.Gcp
Package that implements the Tooark IStorageService on Google Cloud Storage, with V4 signed URLs for reading and writing. |
GitHub repositories
This package is not used by any popular GitHub repositories.