S3
Dosaic.Plugins.Persistence.S3 is a plugin that allows other Dosaic components to interact with S3-compatible storage.
Installation
To install the nuget package follow these steps:
dotnet add package Dosaic.Plugins.Persistence.S3
or add as package reference to your .csproj
<PackageReference Include="Dosaic.Plugins.Persistence.S3" Version=""/>
Appsettings.yml
Configure your appsettings.yml with these properties:
s3:
endpoint: ""
bucketPrefix: "" # optional, used to prefix all bucket names
accessKey: ""
secretKey: ""
region: ""
useSsl: true
healthCheckPath: ""
Registration and Configuration
File Storage with pre-defined buckets
To use the file storage functionality with a pre-defined bucket list, define an enum for your buckets:
public enum MyBucket
{
[FileBucket("logos", FileType.Images)]
Logos = 0,
[FileBucket("avatars", FileType.Images)]
Avatars = 1,
[FileBucket("docs", FileType.Documents)]
Documents = 3
}
Then register the file storage for your bucket enum:
services.AddFileStorage<MyBucket>();
This registers IFileStorage<MyBucket>
which can be injected into your services.
Automatic Bucket Migration Service
To ensure buckets are created automatically when your application starts, register the migration service: The service will automatically create all buckets defined in your enum.
// Register migration service for specific buckets
services.AddBlobStorageBucketMigrationService(MyBucket.Logos);
services.AddBlobStorageBucketMigrationService(MyOtherBucket.Cars);
File Storage without enum based buckets
The plugin automatically registers IFilestorage with the service collection.
When using IFilestorage
instead of IFilestorage<MyBucket>
, there is no bucket migration service since, we don't
know what buckets should exist at runtime.
Therefor you must create your bucket at runtime
public class FileProvider(IFileStorage fileStorage)
{
await fileStorage.CreateBucketAsync("mybucket", cancellationToken);
}
Basic setup without a dosaic web host (optional)
If you don't use the dosaic webhost, which automatically configures the DI container, you'll need to register the S3 plugin manually:
services.AddS3BlobStoragePlugin(new S3Configuration
{
Endpoint = "s3.example.com",
BucketPrefix = "myapp-", // optional, used to prefix all bucket names
AccessKey = "your-access-key",
SecretKey = "your-secret-key",
Region = "us-west-1", // optional
UseSsl = true, // optional
HealthCheckPath = "" // optional
});
Custom mimetype definitions
Filetype Definitions
You can define/override custom definitions for each Filetype
by implementing the IFileTypeDefinitionResolver
interface.
internal class EmptyFileTypeDefinitionResolver : IFileTypeDefinitionResolver
{
public ImmutableArray<Definition> GetDefinitions(FileType fileType)
{
return ImmutableArray<Definition>.Empty;
}
}
And then passing it to the following method so it gets replaced in the serviceColletion:
serviceCollection.ReplaceDefaultFileTypeDefinitionResolver<EmptyFileTypeDefinitionResolver>();
The default implementation is DefaultFileTypeDefinitionResolver
can uses all the default definitions from classMimeDetective.Definitions.DefaultDefinitions
.
ContentInspector Definitions
You can define/override definitions the content inspector by replacing the IContentInspector
in the IoC with your own
implementation.
Example
serviceCollection.ReplaceContentInspector(new List<Definition>());
// OR
serviceCollection.Replace(ServiceDescriptor.Singleton<IContentInspector>(sp =>
new ContentInspectorBuilder
{
Definitions = DefaultDefinitions.All()
.Where(x => x.File.Extensions.Contains("pdf")).ToList() // only use pdf defitions
}
.Build()));
The plugin uses by default Definitions = DefaultDefinitions.All()
.
Working with Files
Blob file creation
// sets original-filename metadata
// sets fileExtension metadata
new BlobFile<MyBucket>(MyBucket.Logos, "someKey").WithFilename("myfile.pdf")
// sets fileExtension metadata
// sets custom metadata
new BlobFile<MyBucket>(MyBucket.Logos, "someKey").WithFileExtension(".pdf")
{
MetaData = new Dictionary<string, string>
{
{ "something-custom", "test" }
},
LastModified = DateTimeOffset.UtcNow
}
Mimetype detection
If the MetaData[BlobFileMetaData.ContentType]
of the BlobFile
is not set,
the plugin will automatically try to detect the mimetype in the following order:
If the
MetaData[BlobFileMetaData.FileExtension]
is set, it will use theIFileTypeDefinitionResolver
to get the mimetype.If the
MetaData[BlobFileMetaData.FileExtension]
is not set, it will use theIContentInspector
to detect the mimetype based on the file content.If the mimetype cannot be detected, it will default to
application/octet-stream
.
Validation
Validation depends on the FileType
of the SetAsync()
method and the detected mimetype result inMetaData[BlobFileMetaData.ContentType]
.
If FileType.Any
is used, no validation is performed.
Otherwise, the detected mimetype must match one of the allowed mimetypes defined in the FileType
enum (can be
customized via IFileTypeDefinitionResolver
).
Usage with permission checks or acl's
Example of using the file storage interface:
public class FileProvider(IFileStorage<MyBucket> fileStorage)
{
private Task CheckPermissionAsync(FileId fileId, CancellationToken cancellationToken)
{
// check permissions or other logic
if (permission == null)
throw Exception("Could not find requested file");
}
public async Task<BlobFile<MyBucket>> GetFileAsync(FileId<MyBucket> id, CancellationToken cancellationToken = default)
{
await CheckPermissionAsync(id, cancellationToken);
return await fileStorage.GetFileAsync(id, cancellationToken);
}
public async Task ConsumeStreamAsync(FileId<MyBucket> id, Func<Stream, CancellationToken, Task> streamConsumer, CancellationToken cancellationToken = default)
{
await CheckPermissionAsync(id, cancellationToken);
await fileStorage.ConsumeStreamAsync(id, streamConsumer, cancellationToken);
}
public async Task<FileId<MyBucket>> SetAsync(BlobFile<MyBucket> file, Stream stream, CancellationToken cancellationToken = default)
{
await CheckPermissionAsync(id, cancellationToken);
return fileStorage.SetAsync(file, stream, cancellationToken);
}
public async Task DeleteFileAsync(FileId<MyBucket> id, CancellationToken cancellationToken = default)
{
await CheckPermissionAsync(id, cancellationToken);
await fileStorage.DeleteFileAsync(id, cancellationToken);
}
}
Example usage in a controller
[[ApiController, Route("/files"), Authorize]
public class FilesController(IFileStorage<MyBucket> fileStorage) : ControllerBase
{
[HttpGet("{key:required}")]
public async Task<IResult> GetFileByKeyAsync([FromRoute] string key, CancellationToken cancellationToken)
{
if (!FileId.TryParse(key, out var fileId))
return Results.StatusCode(StatusCodes.Status404NotFound);
var file = await fileStorage.GetFileAsync(fileId, cancellationToken);
var etag = file.MetaData[BlobFileMetaData.ETag];
var lastModified = file.LastModified;
if (CheckIfResponseIsNotModified(etag, lastModified))
return Results.StatusCode(StatusCodes.Status304NotModified);
var fileName = file.MetaData.TryGetValue(BlobFileMetaData.Filename, out var value) ? value : fileId.Id;
Response.Headers.Append("Content-Length", file.MetaData[BlobFileMetaData.ContentLength]);
Response.Headers.Append("Cache-Control", "private, max-age=300, immutable, must-revalidate");
return Results.Stream(sr => fileStorage.ConsumeStreamAsync(fileId, async (stream, ct) => await stream.CopyToAsync(sr, ct), cancellationToken), file.MetaData[BlobMetaData.ContentType], fileName, lastModified, new EntityTagHeaderValue(etag));
}
private bool CheckIfResponseIsNotModified(string etag, DateTimeOffset lastModified)
{
if (Request.Headers.TryGetValue("If-None-Match", out var ifNoneMatch) && ifNoneMatch == etag)
return true;
return Request.Headers.TryGetValue("If-Modified-Since", out var ifModifiedSince) &&
DateTime.TryParse(ifModifiedSince, out var modifiedSince) &&
modifiedSince >= lastModified;
}
}
Last updated