├── logo.png ├── github-pages ├── assets │ └── images │ │ ├── og-image.png │ │ └── favicon.svg ├── index.md ├── api │ └── index.md ├── setup.md ├── testing.md ├── features │ └── index.md ├── credentials.md ├── adr │ └── index.md ├── robots.txt ├── 404.html ├── sitemap.xml ├── _config.yml └── site-map.md ├── ManagedCode.Storage.Core ├── Models │ ├── DeleteOptions.cs │ ├── LegalHoldOptions.cs │ ├── DownloadOptions.cs │ ├── ExistOptions.cs │ ├── MetadataOptions.cs │ ├── BaseOptions.cs │ ├── BlobMetadata.cs │ └── UploadOptions.cs ├── IStreamer.cs ├── LoggerExtensions.cs ├── Providers │ ├── IStorageProvider.cs │ └── IStorageFactory.cs ├── Exceptions │ └── BadConfigurationException.cs ├── IStorageOptions.cs ├── Extensions │ ├── ServiceCollectionExtensions.cs │ └── StorageOptionsExtensions.cs ├── ManagedCode.Storage.Core.csproj └── StringStream.cs ├── Storages ├── ManagedCode.Storage.FileSystem │ ├── IFileSystemStorage.cs │ ├── Options │ │ └── FileSystemStorageOptions.cs │ ├── ManagedCode.Storage.FileSystem.csproj │ ├── Extensions │ │ └── StorageFactoryExtensions.cs │ └── FileSystemStorageProvider.cs ├── ManagedCode.Storage.CloudKit │ ├── Options │ │ ├── CloudKitEnvironment.cs │ │ ├── CloudKitDatabase.cs │ │ └── CloudKitStorageOptions.cs │ ├── ICloudKitStorage.cs │ ├── Clients │ │ ├── CloudKitRecord.cs │ │ └── ICloudKitClient.cs │ ├── ManagedCode.Storage.CloudKit.csproj │ └── CloudKitStorageProvider.cs ├── ManagedCode.Storage.Google │ ├── Options │ │ ├── BucketOptions.cs │ │ └── GCPStorageOptions.cs │ ├── IGCPStorage.cs │ ├── Extensions │ │ └── StorageFactoryExtensions.cs │ ├── ManagedCode.Storage.Google.csproj │ └── GCPStorageProvider.cs ├── ManagedCode.Storage.Aws │ ├── IAWSStorage.cs │ ├── Extensions │ │ └── StorageFactoryExtensions.cs │ ├── ManagedCode.Storage.Aws.csproj │ ├── Options │ │ └── AWSStorageOptions.cs │ └── AWSStorageProvider.cs ├── ManagedCode.Storage.Azure.DataLake │ ├── Options │ │ ├── OpenWriteStreamOptions.cs │ │ ├── OpenReadStreamOptions.cs │ │ └── AzureDataLakeStorageOptions.cs │ ├── Extensions │ │ └── StorageFactoryExtensions.cs │ ├── IAzureDataLakeStorage.cs │ ├── ManagedCode.Storage.Azure.DataLake.csproj │ └── AzureDataLakeStorageProvider.cs ├── ManagedCode.Storage.Dropbox │ ├── IDropboxStorage.cs │ ├── Clients │ │ ├── DropboxItemMetadata.cs │ │ └── IDropboxClientWrapper.cs │ ├── PLAN.md │ ├── ManagedCode.Storage.Dropbox.csproj │ ├── Options │ │ └── DropboxStorageOptions.cs │ └── DropboxStorageProvider.cs ├── ManagedCode.Storage.OneDrive │ ├── IOneDriveStorage.cs │ ├── Options │ │ └── OneDriveStorageOptions.cs │ ├── ManagedCode.Storage.OneDrive.csproj │ ├── Clients │ │ └── IOneDriveClient.cs │ ├── PLAN.md │ └── OneDriveStorageProvider.cs ├── ManagedCode.Storage.GoogleDrive │ ├── IGoogleDriveStorage.cs │ ├── Options │ │ └── GoogleDriveStorageOptions.cs │ ├── ManagedCode.Storage.GoogleDrive.csproj │ ├── PLAN.md │ ├── Clients │ │ └── IGoogleDriveClient.cs │ └── GoogleDriveStorageProvider.cs ├── ManagedCode.Storage.Azure │ ├── Options │ │ ├── IAzureStorageOptions.cs │ │ ├── AzureStorageOptions.cs │ │ └── AzureStorageCredentialsOptions.cs │ ├── IAzureStorage.cs │ ├── Extensions │ │ └── StorageFactoryExtensions.cs │ └── ManagedCode.Storage.Azure.csproj └── ManagedCode.Storage.Sftp │ ├── ISftpStorage.cs │ └── ManagedCode.Storage.Sftp.csproj ├── Tests ├── ManagedCode.Storage.Tests │ ├── Storages │ │ ├── Abstracts │ │ │ ├── StreamTests.cs │ │ │ ├── DownloadTests.cs │ │ │ └── ContainerTests.cs │ │ ├── Sftp │ │ │ ├── SftpContainerExtensions.cs │ │ │ ├── SftpContainerFactory.cs │ │ │ ├── SftpBlobTests.cs │ │ │ ├── SftpStreamTests.cs │ │ │ ├── SftpContainerTests.cs │ │ │ ├── SftpDownloadTests.cs │ │ │ ├── SftpUploadTests.cs │ │ │ └── SftpConfigurator.cs │ │ ├── FileSystem │ │ │ ├── FileSystemContainerTests.cs │ │ │ ├── FileSystemDownloadTests.cs │ │ │ ├── FileSystemBlobTests.cs │ │ │ ├── FileSystemTests.cs │ │ │ ├── FileSystemConfigurator.cs │ │ │ └── FileSystemUnicodeSanitizerTests.cs │ │ ├── AWS │ │ │ ├── AWSBlobTests.cs │ │ │ ├── AWSUploadTests.cs │ │ │ ├── AWSContainerTests.cs │ │ │ ├── AWSDownloadTests.cs │ │ │ ├── AwsContainerFactory.cs │ │ │ └── AWSConfigurator.cs │ │ ├── GCS │ │ │ ├── GCSDownloadTests.cs │ │ │ ├── GCSContainerTests.cs │ │ │ ├── GCSBlobTests.cs │ │ │ ├── GCSUploadTests.cs │ │ │ └── GCSConfigurator.cs │ │ ├── Azure │ │ │ ├── AzureDataLakeTests.cs │ │ │ ├── AzureBlobTests.cs │ │ │ ├── AzureUploadTests.cs │ │ │ ├── AzureContainerTests.cs │ │ │ ├── AzureDownloadTests.cs │ │ │ ├── AzureConfigurator.cs │ │ │ └── AzureConfigTests.cs │ │ └── CloudKit │ │ │ └── CloudKitStorageTests.cs │ ├── VirtualFileSystem │ │ ├── VirtualFileSystemCollection.cs │ │ ├── Fixtures │ │ │ ├── IVirtualFileSystemFixture.cs │ │ │ ├── VirtualFileSystemCapabilities.cs │ │ │ ├── FileSystemVirtualFileSystemFixture.cs │ │ │ ├── SftpVirtualFileSystemFixture.cs │ │ │ └── AzureVirtualFileSystemFixture.cs │ │ └── UnicodeVfsTestCases.cs │ ├── Common │ │ ├── ContainerImages.cs │ │ └── TestApp │ │ │ ├── Controllers │ │ │ └── AzureTestController.cs │ │ │ └── HttpHostProgram.cs │ ├── AspNetTests │ │ ├── Azure │ │ │ ├── AzureStreamControllerTests.cs │ │ │ ├── AzureUploadControllerTests.cs │ │ │ └── AzureDownloadControllerTests.cs │ │ └── Abstracts │ │ │ ├── BaseControllerTests.cs │ │ │ └── BaseSignalRStorageTests.cs │ ├── Constants │ │ └── ApiEndpoints.cs │ ├── ExtensionsTests │ │ └── ReplaceExtensionsTests.cs │ └── Core │ │ └── Crc32HelperTests.cs └── ManagedCode.Storage.TestsOnly.sln ├── Integraions ├── ManagedCode.Storage.Server │ ├── Models │ │ ├── FileUploadPayload.cs │ │ ├── ChunkUploadCompleteResponse.cs │ │ ├── ChunkSegment.cs │ │ ├── FilePayload.cs │ │ ├── ChunkUploadCompleteRequest.cs │ │ ├── UploadStreamDescriptor.cs │ │ └── TransferStatus.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Extensions │ │ ├── DependencyInjection │ │ │ ├── StorageServiceCollectionExtensions.cs │ │ │ ├── ChunkUploadServiceCollectionExtensions.cs │ │ │ ├── StorageSignalRServiceCollectionExtensions.cs │ │ │ └── StorageServerBuilderExtensions.cs │ │ ├── File │ │ │ ├── BrowserFileExtensions.cs │ │ │ └── FormFileExtensions.cs │ │ ├── StorageEndpointRouteBuilderExtensions.cs │ │ └── Storage │ │ │ ├── StorageExtensions.cs │ │ │ ├── StorageFromFileExtensions.cs │ │ │ └── StorageBrowserFileExtensions.cs │ ├── ChunkUpload │ │ ├── ChunkUploadDescriptor.cs │ │ ├── ChunkUploadOptions.cs │ │ └── ChunkUploadSession.cs │ ├── StorageSetupBackgroundService.cs │ ├── Hubs │ │ ├── StorageHub.cs │ │ └── StorageHubOptions.cs │ ├── Controllers │ │ └── StorageController.cs │ ├── ManagedCode.Storage.Server.csproj │ └── Helpers │ │ └── StreamHelper.cs ├── ManagedCode.Storage.Client │ ├── ProgressStatus.cs │ ├── ManagedCode.Storage.Client.csproj │ └── IStorageClient.cs └── ManagedCode.Storage.Client.SignalR │ ├── StorageSignalREventNames.cs │ ├── ManagedCode.Storage.Client.SignalR.csproj │ └── Models │ └── StorageUploadStreamDescriptor.cs ├── docs ├── API │ └── index.md ├── templates │ ├── ADR-Template.md │ └── Feature-Template.md ├── Features │ ├── mime-and-crc.md │ ├── testfakes.md │ ├── integration-signalr-client.md │ ├── provider-azure-datalake.md │ ├── index.md │ ├── provider-sftp.md │ ├── integration-dotnet-client.md │ ├── provider-filesystem.md │ ├── provider-aws-s3.md │ ├── provider-google-cloud-storage.md │ └── provider-azure-blob.md └── Development │ └── setup.md ├── ManagedCode.Storage.TestFakes ├── FakeAWSStorage.cs ├── FakeGoogleStorage.cs └── ManagedCode.Storage.TestFakes.csproj ├── LICENSE ├── .github └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── ManagedCode.Storage.VirtualFileSystem └── ManagedCode.Storage.VirtualFileSystem.csproj ├── ManagedCode.Storage.slnx └── Directory.Build.props /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/managedcode/Storage/HEAD/logo.png -------------------------------------------------------------------------------- /github-pages/assets/images/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/managedcode/Storage/HEAD/github-pages/assets/images/og-image.png -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/DeleteOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Core.Models; 2 | 3 | public class DeleteOptions : BaseOptions 4 | { 5 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/LegalHoldOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Core.Models; 2 | 3 | public class LegalHoldOptions : BaseOptions 4 | { 5 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/DownloadOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Core.Models; 2 | 3 | public class DownloadOptions : BaseOptions 4 | { 5 | public string? LocalPath { get; set; } 6 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.FileSystem/IFileSystemStorage.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | 3 | namespace ManagedCode.Storage.FileSystem; 4 | 5 | public interface IFileSystemStorage : IStorage 6 | { 7 | } -------------------------------------------------------------------------------- /github-pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | is_home: true 5 | permalink: / 6 | nav_order: 1 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/Options/CloudKitEnvironment.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.CloudKit.Options; 2 | 3 | public enum CloudKitEnvironment 4 | { 5 | Development, 6 | Production 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/Options/CloudKitDatabase.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.CloudKit.Options; 2 | 3 | public enum CloudKitDatabase 4 | { 5 | Public, 6 | Private, 7 | Shared 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Google/Options/BucketOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Google.Options; 2 | 3 | public class BucketOptions 4 | { 5 | public string ProjectId { get; set; } 6 | 7 | public string Bucket { get; set; } 8 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Aws/IAWSStorage.cs: -------------------------------------------------------------------------------- 1 | using Amazon.S3; 2 | using ManagedCode.Storage.Aws.Options; 3 | using ManagedCode.Storage.Core; 4 | 5 | namespace ManagedCode.Storage.Aws; 6 | 7 | public interface IAWSStorage : IStorage 8 | { 9 | } -------------------------------------------------------------------------------- /github-pages/api/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: API 4 | description: HTTP and SignalR API documentation for ManagedCode.Storage.Server. 5 | permalink: /api/ 6 | nav_order: 6 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/Options/OpenWriteStreamOptions.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core.Models; 2 | 3 | namespace ManagedCode.Storage.Azure.DataLake.Options; 4 | 5 | public class OpenWriteStreamOptions : BaseOptions 6 | { 7 | public bool Overwrite { get; set; } 8 | } -------------------------------------------------------------------------------- /github-pages/setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Setup 4 | description: How to clone, build, and run tests for ManagedCode.Storage. 5 | permalink: /setup/ 6 | nav_order: 2 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /github-pages/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Testing 4 | description: Test strategy and how to run the ManagedCode.Storage test suite. 5 | permalink: /testing/ 6 | nav_order: 4 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Google/IGCPStorage.cs: -------------------------------------------------------------------------------- 1 | using Google.Cloud.Storage.V1; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Google.Options; 4 | 5 | namespace ManagedCode.Storage.Google; 6 | 7 | public interface IGCPStorage : IStorage 8 | { 9 | } -------------------------------------------------------------------------------- /github-pages/features/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Features 4 | description: Documentation for major modules and providers in ManagedCode.Storage. 5 | permalink: /features/ 6 | nav_order: 5 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Abstracts/StreamTests.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | using ManagedCode.Storage.Tests.Common; 3 | 4 | namespace ManagedCode.Storage.Tests.Storages.Abstracts; 5 | 6 | public abstract class StreamTests : BaseContainer where T : IContainer 7 | { 8 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/FileUploadPayload.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace ManagedCode.Storage.Server.Models; 4 | 5 | public class FileUploadPayload 6 | { 7 | public IFormFile File { get; set; } = default!; 8 | public FilePayload Payload { get; set; } = new(); 9 | } 10 | -------------------------------------------------------------------------------- /github-pages/credentials.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Credentials 4 | description: How to obtain credentials for OneDrive, Google Drive, Dropbox, and CloudKit. 5 | permalink: /credentials/ 6 | nav_order: 3 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/ChunkUploadCompleteResponse.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core.Models; 2 | 3 | namespace ManagedCode.Storage.Server.Models; 4 | 5 | public class ChunkUploadCompleteResponse 6 | { 7 | public uint Checksum { get; set; } 8 | public BlobMetadata? Metadata { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/ExistOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Core.Models; 2 | 3 | public class ExistOptions : BaseOptions 4 | { 5 | public static ExistOptions FromBaseOptions(BaseOptions options) 6 | { 7 | return new ExistOptions { FileName = options.FileName, Directory = options.Directory }; 8 | } 9 | } -------------------------------------------------------------------------------- /github-pages/adr/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: ADR 4 | description: Architecture Decision Records (ADR) for ManagedCode.Storage. 5 | permalink: /adr/ 6 | nav_order: 6 7 | --- 8 | 9 | # ADR 10 | 11 | This page is generated from `docs/ADR/` in CI (GitHub Pages workflow). 12 | 13 | Start here: `docs/ADR/index.md`. 14 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/Options/OpenReadStreamOptions.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core.Models; 2 | 3 | namespace ManagedCode.Storage.Azure.DataLake.Options; 4 | 5 | public class OpenReadStreamOptions : BaseOptions 6 | { 7 | public long Position { get; set; } 8 | 9 | public int BufferSize { get; set; } 10 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/IDropboxStorage.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using ManagedCode.Storage.Dropbox.Clients; 3 | using ManagedCode.Storage.Dropbox.Options; 4 | 5 | namespace ManagedCode.Storage.Dropbox; 6 | 7 | public interface IDropboxStorage : IStorage 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.OneDrive/IOneDriveStorage.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using ManagedCode.Storage.OneDrive.Clients; 3 | using ManagedCode.Storage.OneDrive.Options; 4 | 5 | namespace ManagedCode.Storage.OneDrive; 6 | 7 | public interface IOneDriveStorage : IStorage 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/ICloudKitStorage.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using ManagedCode.Storage.CloudKit.Clients; 3 | using ManagedCode.Storage.CloudKit.Options; 4 | 5 | namespace ManagedCode.Storage.CloudKit; 6 | 7 | public interface ICloudKitStorage : IStorage 8 | { 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/VirtualFileSystemCollection.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace ManagedCode.Storage.Tests.VirtualFileSystem; 4 | 5 | [CollectionDefinition(Name, DisableParallelization = true)] 6 | public sealed class VirtualFileSystemCollection 7 | { 8 | public const string Name = "VirtualFileSystem"; 9 | } 10 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Client/ProgressStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Client; 4 | 5 | public record ProgressStatus( 6 | string File, 7 | float Progress, 8 | long TotalBytes, 9 | long TransferredBytes, 10 | TimeSpan Elapsed, 11 | TimeSpan Remaining, 12 | string Speed, 13 | string? Error = null); -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.FileSystem/Options/FileSystemStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | 3 | namespace ManagedCode.Storage.FileSystem.Options; 4 | 5 | public class FileSystemStorageOptions : IStorageOptions 6 | { 7 | public string? BaseFolder { get; set; } 8 | 9 | public bool CreateContainerIfNotExists { get; set; } = true; 10 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.GoogleDrive/IGoogleDriveStorage.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using ManagedCode.Storage.GoogleDrive.Clients; 3 | using ManagedCode.Storage.GoogleDrive.Options; 4 | 5 | namespace ManagedCode.Storage.GoogleDrive; 6 | 7 | public interface IGoogleDriveStorage : IStorage 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Common/ContainerImages.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Tests.Common; 2 | 3 | public class ContainerImages 4 | { 5 | public const string Azurite = "mcr.microsoft.com/azure-storage/azurite:latest"; 6 | public const string FakeGCSServer = "fsouza/fake-gcs-server:latest"; 7 | public const string LocalStack = "localstack/localstack:latest"; 8 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/Fixtures/IVirtualFileSystemFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ManagedCode.Storage.Tests.VirtualFileSystem.Fixtures; 4 | 5 | public interface IVirtualFileSystemFixture 6 | { 7 | Task CreateContextAsync(); 8 | VirtualFileSystemCapabilities Capabilities { get; } 9 | } 10 | -------------------------------------------------------------------------------- /github-pages/robots.txt: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | {% assign effective_baseurl = site.baseurl %} 5 | {% if site.url != nil and site.url != '' %} 6 | {% unless site.url contains 'github.io' %} 7 | {% assign effective_baseurl = '' %} 8 | {% endunless %} 9 | {% endif %} 10 | User-agent: * 11 | Allow: / 12 | 13 | Sitemap: {{ site.url }}{{ effective_baseurl }}/sitemap.xml 14 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ManagedCode.Storage.AspNetExtensions": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:59086;http://localhost:59087" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/MetadataOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Core.Models; 2 | 3 | public class MetadataOptions : BaseOptions 4 | { 5 | public static MetadataOptions FromBaseOptions(BaseOptions options) 6 | { 7 | return new MetadataOptions { FileName = options.FileName, Directory = options.Directory }; 8 | } 9 | 10 | public string ETag { get; set; } = string.Empty; 11 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/Clients/CloudKitRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.CloudKit.Clients; 4 | 5 | public sealed record CloudKitRecord( 6 | string RecordName, 7 | string RecordType, 8 | string Path, 9 | DateTimeOffset CreatedOn, 10 | DateTimeOffset LastModified, 11 | string? ContentType, 12 | ulong Size, 13 | Uri? DownloadUrl); 14 | 15 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/BaseOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Core.Models; 4 | 5 | public abstract class BaseOptions 6 | { 7 | public string FileName { get; set; } = string.Empty; 8 | public string? Directory { get; set; } 9 | 10 | // TODO: Check this 11 | public string FullPath => string.IsNullOrWhiteSpace(Directory) ? FileName : $"{Directory}/{FileName}"; 12 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/IStreamer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Communication; 5 | 6 | namespace ManagedCode.Storage.Core; 7 | 8 | public interface IStreamer 9 | { 10 | /// 11 | /// Gets file stream. 12 | /// 13 | Task> GetStreamAsync(string fileName, CancellationToken cancellationToken = default); 14 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace ManagedCode.Storage.Core; 6 | 7 | public static class LoggerExtensions 8 | { 9 | public static void LogException(this ILogger? logger, Exception ex, [CallerMemberName] string methodName = default!) 10 | { 11 | logger?.LogError(ex, methodName); 12 | } 13 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/Clients/DropboxItemMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Dropbox.Clients; 4 | 5 | public class DropboxItemMetadata 6 | { 7 | public required string Name { get; set; } 8 | public required string Path { get; set; } 9 | public ulong Size { get; set; } 10 | public DateTime ClientModified { get; set; } 11 | public DateTime ServerModified { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/Fixtures/VirtualFileSystemCapabilities.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Tests.VirtualFileSystem.Fixtures; 2 | 3 | public sealed record VirtualFileSystemCapabilities( 4 | bool Enabled = true, 5 | bool SupportsListing = true, 6 | bool SupportsDirectoryDelete = true, 7 | bool SupportsDirectoryCopy = true, 8 | bool SupportsMove = true, 9 | bool SupportsDirectoryStats = true); 10 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Client.SignalR/StorageSignalREventNames.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Client.SignalR; 2 | 3 | internal static class StorageSignalREventNames 4 | { 5 | public const string TransferProgress = "TransferProgress"; 6 | public const string TransferCompleted = "TransferCompleted"; 7 | public const string TransferCanceled = "TransferCanceled"; 8 | public const string TransferFaulted = "TransferFaulted"; 9 | } 10 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/ChunkSegment.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Server.Models; 2 | 3 | public class ChunkSegment 4 | { 5 | public string UploadId { get; set; } = string.Empty; 6 | public int Index { get; set; } 7 | public int TotalChunks { get; set; } 8 | public int Size { get; set; } 9 | public long? FileSize { get; set; } 10 | public byte[] Data { get; set; } = []; // assumes base64 from client 11 | } 12 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/FilePayload.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Server.Models; 2 | 3 | public class FilePayload 4 | { 5 | public string UploadId { get; set; } = string.Empty; 6 | public string? FileName { get; set; } 7 | public string? ContentType { get; set; } 8 | public long? FileSize { get; set; } 9 | public int ChunkIndex { get; set; } 10 | public int ChunkSize { get; set; } 11 | public int TotalChunks { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Providers/IStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Core.Providers 4 | { 5 | public interface IStorageProvider 6 | { 7 | Type StorageOptionsType { get; } 8 | TStorage CreateStorage(TOptions options) 9 | where TStorage : class, IStorage 10 | where TOptions : class, IStorageOptions; 11 | 12 | 13 | IStorageOptions GetDefaultOptions(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Exceptions/BadConfigurationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Core.Exceptions; 4 | 5 | public class BadConfigurationException : Exception 6 | { 7 | public BadConfigurationException() 8 | { 9 | } 10 | 11 | public BadConfigurationException(string message) : base(message) 12 | { 13 | } 14 | 15 | public BadConfigurationException(string message, Exception inner) : base(message, inner) 16 | { 17 | } 18 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpContainerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Testcontainers.Sftp; 2 | 3 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 4 | 5 | internal static class SftpContainerExtensions 6 | { 7 | public static string GetHost(this SftpContainer container) 8 | { 9 | return container.Hostname; 10 | } 11 | 12 | public static int GetPort(this SftpContainer container) 13 | { 14 | return container.GetMappedPublicPort(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/DependencyInjection/StorageServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace ManagedCode.Storage.Server.Extensions.DependencyInjection; 4 | 5 | public static class StorageServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddStorageSetupService(this IServiceCollection services) 8 | { 9 | return services.AddHostedService(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.TestsOnly.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.0.31612.314 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(SolutionProperties) = preSolution 11 | HideSolutionNode = FALSE 12 | EndGlobalSection 13 | EndGlobal 14 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/AspNetTests/Azure/AzureStreamControllerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.AspNetTests.Abstracts; 2 | using ManagedCode.Storage.Tests.Common; 3 | using ManagedCode.Storage.Tests.Constants; 4 | 5 | namespace ManagedCode.Storage.Tests.AspNetTests.Azure; 6 | 7 | public class AzureStreamControllerTests : BaseStreamControllerTests 8 | { 9 | public AzureStreamControllerTests(StorageTestApplication testApplication) : base(testApplication, ApiEndpoints.Azure) 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/AspNetTests/Azure/AzureUploadControllerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.AspNetTests.Abstracts; 2 | using ManagedCode.Storage.Tests.Common; 3 | using ManagedCode.Storage.Tests.Constants; 4 | 5 | namespace ManagedCode.Storage.Tests.AspNetTests.Azure; 6 | 7 | public class AzureUploadControllerTests : BaseUploadControllerTests 8 | { 9 | public AzureUploadControllerTests(StorageTestApplication testApplication) : base(testApplication, ApiEndpoints.Azure) 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/AspNetTests/Azure/AzureDownloadControllerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.AspNetTests.Abstracts; 2 | using ManagedCode.Storage.Tests.Common; 3 | using ManagedCode.Storage.Tests.Constants; 4 | 5 | namespace ManagedCode.Storage.Tests.AspNetTests.Azure; 6 | 7 | public class AzureDownloadControllerTests : BaseDownloadControllerTests 8 | { 9 | public AzureDownloadControllerTests(StorageTestApplication testApplication) : base(testApplication, ApiEndpoints.Azure) 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/ChunkUpload/ChunkUploadDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Server.Models; 3 | 4 | namespace ManagedCode.Storage.Server.ChunkUpload; 5 | 6 | internal static class ChunkUploadDescriptor 7 | { 8 | public static string ResolveUploadId(FilePayload payload) 9 | { 10 | return string.IsNullOrWhiteSpace(payload.UploadId) 11 | ? throw new InvalidOperationException("UploadId must be provided for chunk uploads.") 12 | : payload.UploadId; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /github-pages/assets/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | M 10 | 11 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Common/TestApp/Controllers/AzureTestController.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Azure; 2 | using ManagedCode.Storage.Server.ChunkUpload; 3 | using ManagedCode.Storage.Tests.Common.TestApp.Controllers.Base; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ManagedCode.Storage.Tests.Common.TestApp.Controllers; 7 | 8 | [Route("azure")] 9 | [ApiController] 10 | public class AzureTestController(IAzureStorage storage, ChunkUploadService chunkUploadService) 11 | : BaseTestController(storage, chunkUploadService); 12 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/IStorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Core; 2 | 3 | /// 4 | /// Provides the options for storage operations. 5 | /// 6 | public interface IStorageOptions 7 | { 8 | /// 9 | /// Gets or sets a value indicating whether to create the container if it does not exist. 10 | /// 11 | /// 12 | /// true if the container should be created if it does not exist; otherwise, false. 13 | /// 14 | public bool CreateContainerIfNotExists { get; set; } 15 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure/Options/IAzureStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Models; 4 | using ManagedCode.Storage.Core; 5 | 6 | namespace ManagedCode.Storage.Azure.Options; 7 | 8 | public interface IAzureStorageOptions : IStorageOptions 9 | { 10 | public string? Container { get; set; } 11 | public PublicAccessType PublicAccessType { get; set; } 12 | public BlobClientOptions? OriginalOptions { get; set; } 13 | public StorageTransferOptions? UploadTransferOptions { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core.Providers; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace ManagedCode.Storage.Core.Extensions; 6 | 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddStorageFactory(this IServiceCollection serviceCollection) 10 | { 11 | serviceCollection.TryAddSingleton(); 12 | return serviceCollection; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/ChunkUploadCompleteRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ManagedCode.Storage.Server.Models; 4 | 5 | public class ChunkUploadCompleteRequest 6 | { 7 | public string UploadId { get; set; } = default!; 8 | public string? FileName { get; set; } 9 | public string? Directory { get; set; } 10 | public string? ContentType { get; set; } 11 | public Dictionary? Metadata { get; set; } 12 | public bool CommitToStorage { get; set; } = true; 13 | public bool KeepMergedFile { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Constants/ApiEndpoints.cs: -------------------------------------------------------------------------------- 1 | namespace ManagedCode.Storage.Tests.Constants; 2 | 3 | public static class ApiEndpoints 4 | { 5 | public const string Azure = "azure"; 6 | 7 | public static class Base 8 | { 9 | public const string UploadFile = "{0}/upload"; 10 | public const string DownloadFile = "{0}/download/{1}"; 11 | public const string DownloadBytes = "{0}/download-bytes/{1}"; 12 | public const string StreamFile = "{0}/stream/{1}"; 13 | public const string UploadLargeFile = "{0}/upload-chunks"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/StorageSetupBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Storage.Core; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace ManagedCode.Storage.Server; 8 | 9 | public class StorageSetupBackgroundService(IEnumerable storages) : BackgroundService 10 | { 11 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 12 | { 13 | foreach (var storage in storages) 14 | await storage.CreateContainerAsync(stoppingToken); 15 | } 16 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.OneDrive/Options/OneDriveStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using ManagedCode.Storage.OneDrive.Clients; 3 | using Microsoft.Graph; 4 | 5 | namespace ManagedCode.Storage.OneDrive.Options; 6 | 7 | public class OneDriveStorageOptions : IStorageOptions 8 | { 9 | public Clients.IOneDriveClient? Client { get; set; } 10 | 11 | public GraphServiceClient? GraphClient { get; set; } 12 | 13 | public string DriveId { get; set; } = "me"; 14 | 15 | public string RootPath { get; set; } = "/"; 16 | 17 | public bool CreateContainerIfNotExists { get; set; } = true; 18 | } 19 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/BlobMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ManagedCode.Storage.Core.Models; 5 | 6 | public class BlobMetadata 7 | { 8 | public string FullName { get; set; } = null!; 9 | public string Name { get; set; } = null!; 10 | public Uri? Uri { get; set; } 11 | public Dictionary? Metadata { get; set; } 12 | public DateTimeOffset LastModified { get; set; } 13 | public DateTimeOffset CreatedOn { get; set; } 14 | public string? Container { get; set; } 15 | public string? MimeType { get; set; } 16 | public ulong Length { get; set; } 17 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.GoogleDrive/Options/GoogleDriveStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Google.Apis.Drive.v3; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.GoogleDrive.Clients; 4 | 5 | namespace ManagedCode.Storage.GoogleDrive.Options; 6 | 7 | public class GoogleDriveStorageOptions : IStorageOptions 8 | { 9 | public IGoogleDriveClient? Client { get; set; } 10 | 11 | public DriveService? DriveService { get; set; } 12 | 13 | public string RootFolderId { get; set; } = "root"; 14 | 15 | public bool CreateContainerIfNotExists { get; set; } = true; 16 | 17 | public bool SupportsAllDrives { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemContainerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.FileSystem; 6 | 7 | public class FileSystemContainerTests : ContainerTests 8 | { 9 | protected override EmptyContainer Build() 10 | { 11 | return new EmptyContainer(); 12 | } 13 | 14 | protected override ServiceProvider ConfigureServices() 15 | { 16 | return FileSystemConfigurator.ConfigureServices("managed-code-blob"); 17 | } 18 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemDownloadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.FileSystem; 6 | 7 | public class FileSystemDownloadTests : DownloadTests 8 | { 9 | protected override EmptyContainer Build() 10 | { 11 | return new EmptyContainer(); 12 | } 13 | 14 | protected override ServiceProvider ConfigureServices() 15 | { 16 | return FileSystemConfigurator.ConfigureServices("managed-code-blob"); 17 | } 18 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure/Options/AzureStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Models; 4 | 5 | namespace ManagedCode.Storage.Azure.Options; 6 | 7 | public class AzureStorageOptions : IAzureStorageOptions 8 | { 9 | public string? ConnectionString { get; set; } 10 | public string? Container { get; set; } 11 | public PublicAccessType PublicAccessType { get; set; } 12 | public BlobClientOptions? OriginalOptions { get; set; } 13 | public StorageTransferOptions? UploadTransferOptions { get; set; } 14 | 15 | public bool CreateContainerIfNotExists { get; set; } = true; 16 | } 17 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/UnicodeVfsTestCases.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ManagedCode.Storage.Tests.VirtualFileSystem; 4 | 5 | public static class UnicodeVfsTestCases 6 | { 7 | public static IEnumerable FolderScenarios => new[] 8 | { 9 | new object[] { "Українська-папка", "лист-привіт", "Привіт з Києва!" }, 10 | new object[] { "中文目錄", "測試文件", "雲端中的內容" }, 11 | new object[] { "日本語ディレクトリ", "テストファイル", "東京からこんにちは" }, 12 | new object[] { "한국어_폴더", "테스트-파일", "부산에서 안녕하세요" }, 13 | new object[] { "emoji📁", "😀-файл", "multi🌐lingual content" } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Google/Options/GCPStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Google.Apis.Auth.OAuth2; 2 | using Google.Cloud.Storage.V1; 3 | using ManagedCode.Storage.Core; 4 | 5 | namespace ManagedCode.Storage.Google.Options; 6 | 7 | public class GCPStorageOptions : IStorageOptions 8 | { 9 | public string AuthFileName { get; set; } = null!; 10 | public BucketOptions BucketOptions { get; set; } 11 | public GoogleCredential? GoogleCredential { get; set; } 12 | public CreateBucketOptions? OriginalOptions { get; set; } 13 | public StorageClientBuilder? StorageClientBuilder { get; set; } 14 | 15 | public bool CreateContainerIfNotExists { get; set; } = true; 16 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/Options/AzureDataLakeStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Files.DataLake.Models; 2 | using ManagedCode.Storage.Core; 3 | 4 | namespace ManagedCode.Storage.Azure.DataLake.Options; 5 | 6 | public class AzureDataLakeStorageOptions : IStorageOptions 7 | { 8 | public string ConnectionString { get; set; } 9 | public string FileSystem { get; set; } 10 | 11 | public DataLakeFileSystemCreateOptions PublicAccessType { get; set; } = new() 12 | { 13 | PublicAccessType = global::Azure.Storage.Files.DataLake.Models.PublicAccessType.None 14 | }; 15 | 16 | public bool CreateContainerIfNotExists { get; set; } = true; 17 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSBlobTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.LocalStack; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.AWS; 7 | 8 | public class AWSBlobTests : BlobTests 9 | { 10 | protected override LocalStackContainer Build() 11 | { 12 | return AwsContainerFactory.Create(); 13 | } 14 | 15 | protected override ServiceProvider ConfigureServices() 16 | { 17 | return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSUploadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.LocalStack; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.AWS; 7 | 8 | public class AWSUploadTests : UploadTests 9 | { 10 | protected override LocalStackContainer Build() 11 | { 12 | return AwsContainerFactory.Create(); 13 | } 14 | 15 | protected override ServiceProvider ConfigureServices() 16 | { 17 | return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpContainerFactory.cs: -------------------------------------------------------------------------------- 1 | using Testcontainers.Sftp; 2 | 3 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 4 | 5 | internal static class SftpContainerFactory 6 | { 7 | public const string Username = "storage"; 8 | public const string Password = "storage-password"; 9 | public const string RemoteDirectory = "/upload"; 10 | 11 | public static SftpContainer Create() 12 | { 13 | return new SftpBuilder() 14 | .WithUsername(Username) 15 | .WithPassword(Password) 16 | .WithUploadDirectory(RemoteDirectory) 17 | .WithCleanUp(true) 18 | .Build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Providers/IStorageFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Core.Providers 4 | { 5 | public interface IStorageFactory 6 | { 7 | IStorage CreateStorage(IStorageOptions options); 8 | IStorage CreateStorage(Action options); 9 | 10 | TStorage CreateStorage(TOptions options) 11 | where TStorage : class, IStorage 12 | where TOptions : class, IStorageOptions; 13 | 14 | TStorage CreateStorage(Action options) 15 | where TStorage : class, IStorage 16 | where TOptions : class, IStorageOptions; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSContainerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.LocalStack; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.AWS; 7 | 8 | public class AWSContainerTests : ContainerTests 9 | { 10 | protected override LocalStackContainer Build() 11 | { 12 | return AwsContainerFactory.Create(); 13 | } 14 | 15 | protected override ServiceProvider ConfigureServices() 16 | { 17 | return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSDownloadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.LocalStack; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.AWS; 7 | 8 | public class AWSDownloadTests : DownloadTests 9 | { 10 | protected override LocalStackContainer Build() 11 | { 12 | return AwsContainerFactory.Create(); 13 | } 14 | 15 | protected override ServiceProvider ConfigureServices() 16 | { 17 | return AWSConfigurator.ConfigureServices(Container.GetConnectionString()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemBlobTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | // ReSharper disable MethodHasAsyncOverload 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.FileSystem; 8 | 9 | public class FileSystemBlobTests : BlobTests 10 | { 11 | protected override EmptyContainer Build() 12 | { 13 | return new EmptyContainer(); 14 | } 15 | 16 | protected override ServiceProvider ConfigureServices() 17 | { 18 | return FileSystemConfigurator.ConfigureServices("managed-code-blob"); 19 | } 20 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure/IAzureStorage.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Azure.Storage.Blobs; 5 | using ManagedCode.Communication; 6 | using ManagedCode.Storage.Core; 7 | 8 | namespace ManagedCode.Storage.Azure; 9 | 10 | public interface IAzureStorage : IStorage 11 | { 12 | Task> OpenReadStreamAsync(string fileName, CancellationToken cancellationToken = default); 13 | Task> OpenWriteStreamAsync(string fileName, CancellationToken cancellationToken = default); 14 | 15 | Stream GetBlobStream(string fileName, bool userBuffer = true, int bufferSize = BlobStream.DefaultBufferSize); 16 | } -------------------------------------------------------------------------------- /docs/API/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | description: "HTTP and SignalR API documentation for ManagedCode.Storage.Server." 4 | keywords: "storage API, HTTP, SignalR, ASP.NET controllers, upload, download, streaming, chunked uploads, ranged downloads, ManagedCode.Storage.Server" 5 | permalink: /api/ 6 | nav_order: 7 7 | --- 8 | 9 | # API 10 | 11 | ManagedCode.Storage exposes an HTTP + SignalR integration surface via `ManagedCode.Storage.Server`. 12 | 13 | ```mermaid 14 | flowchart LR 15 | App[Client app] --> Http[HTTP Controllers] 16 | App --> Hub[SignalR Hub] 17 | Http --> Storage[IStorage] 18 | Hub --> Storage 19 | Storage --> Provider[Concrete storage provider] 20 | ``` 21 | 22 | - [Storage server (HTTP + SignalR)](storage-server.md) 23 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSDownloadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.FakeGcsServer; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.GCS; 7 | 8 | public class GCSDownloadTests : DownloadTests 9 | { 10 | protected override FakeGcsServerContainer Build() 11 | { 12 | return new FakeGcsServerBuilder().WithImage(ContainerImages.FakeGCSServer) 13 | .Build(); 14 | } 15 | 16 | protected override ServiceProvider ConfigureServices() 17 | { 18 | return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); 19 | } 20 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.Core 10 | ManagedCode.Storage.Core 11 | Base interfaces for ManagedCode.StorageS 12 | managedcode, aws, azure, gcp, storage, cloud, blob 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSContainerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.FakeGcsServer; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.GCS; 7 | 8 | public class GCSContainerTests : ContainerTests 9 | { 10 | protected override FakeGcsServerContainer Build() 11 | { 12 | return new FakeGcsServerBuilder() 13 | .WithImage(ContainerImages.FakeGCSServer) 14 | .Build(); 15 | } 16 | 17 | protected override ServiceProvider ConfigureServices() 18 | { 19 | return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); 20 | } 21 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/File/BrowserFileExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using ManagedCode.Storage.Core.Models; 4 | using Microsoft.AspNetCore.Components.Forms; 5 | 6 | namespace ManagedCode.Storage.Server.Extensions.File; 7 | 8 | public static class BrowserFileExtensions 9 | { 10 | public static async Task ToLocalFileAsync(this IBrowserFile formFile, CancellationToken cancellationToken = default) 11 | { 12 | var localFile = LocalFile.FromRandomNameWithExtension(formFile.Name); 13 | await using (var stream = formFile.OpenReadStream()) 14 | { 15 | await localFile.CopyFromStreamAsync(stream, cancellationToken); 16 | } 17 | return localFile; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureDataLakeTests.cs: -------------------------------------------------------------------------------- 1 | // using ManagedCode.Storage.Azure.DataLake.Extensions; 2 | // using Microsoft.Extensions.DependencyInjection; 3 | // 4 | // namespace ManagedCode.Storage.Tests.Azure; 5 | // 6 | // public class AzureDataLakeTests : StorageBaseTests 7 | // { 8 | // protected override ServiceProvider ConfigureServices() 9 | // { 10 | // var services = new ServiceCollection(); 11 | // services.AddLogging(); 12 | // 13 | // services.AddAzureDataLakeStorageAsDefault(opt => 14 | // { 15 | // opt.FileSystem = ""; 16 | // 17 | // opt.ConnectionString = 18 | // ""; 19 | // }); 20 | // 21 | // return services.BuildServiceProvider(); 22 | // } 23 | // } 24 | 25 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureBlobTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.Azurite; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.Azure; 7 | 8 | public class AzureBlobTests : BlobTests 9 | { 10 | protected override AzuriteContainer Build() 11 | { 12 | return new AzuriteBuilder() 13 | .WithImage(ContainerImages.Azurite) 14 | .WithCommand("--skipApiVersionCheck") 15 | .Build(); 16 | } 17 | 18 | protected override ServiceProvider ConfigureServices() 19 | { 20 | return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); 21 | } 22 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSBlobTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.FakeGcsServer; 5 | 6 | // ReSharper disable MethodHasAsyncOverload 7 | 8 | namespace ManagedCode.Storage.Tests.Storages.GCS; 9 | 10 | public class GCSBlobTests : BlobTests 11 | { 12 | protected override FakeGcsServerContainer Build() 13 | { 14 | return new FakeGcsServerBuilder().WithImage(ContainerImages.FakeGCSServer) 15 | .Build(); 16 | } 17 | 18 | protected override ServiceProvider ConfigureServices() 19 | { 20 | return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); 21 | } 22 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureUploadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.Azurite; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.Azure; 7 | 8 | public class AzureUploadTests : UploadTests 9 | { 10 | protected override AzuriteContainer Build() 11 | { 12 | return new AzuriteBuilder() 13 | .WithImage(ContainerImages.Azurite) 14 | .WithCommand("--skipApiVersionCheck") 15 | .Build(); 16 | } 17 | 18 | protected override ServiceProvider ConfigureServices() 19 | { 20 | return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); 21 | } 22 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureContainerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.Azurite; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.Azure; 7 | 8 | public class AzureContainerTests : ContainerTests 9 | { 10 | protected override AzuriteContainer Build() 11 | { 12 | return new AzuriteBuilder() 13 | .WithImage(ContainerImages.Azurite) 14 | .WithCommand("--skipApiVersionCheck") 15 | .Build(); 16 | } 17 | 18 | protected override ServiceProvider ConfigureServices() 19 | { 20 | return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); 21 | } 22 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureDownloadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Common; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.Azurite; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.Azure; 7 | 8 | public class AzureDownloadTests : DownloadTests 9 | { 10 | protected override AzuriteContainer Build() 11 | { 12 | return new AzuriteBuilder() 13 | .WithImage(ContainerImages.Azurite) 14 | .WithCommand("--skipApiVersionCheck") 15 | .Build(); 16 | } 17 | 18 | protected override ServiceProvider ConfigureServices() 19 | { 20 | return AzureConfigurator.ConfigureServices(Container.GetConnectionString()); 21 | } 22 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.FileSystem; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Xunit; 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.FileSystem; 8 | 9 | public class FileSystemTests 10 | { 11 | [Fact] 12 | public void StorageAsDefaultTest() 13 | { 14 | var storage = FileSystemConfigurator.ConfigureServices("test") 15 | .GetService(); 16 | var defaultStorage = FileSystemConfigurator.ConfigureServices("test") 17 | .GetService(); 18 | storage?.GetType() 19 | .FullName 20 | .ShouldBe(defaultStorage?.GetType() 21 | .FullName); 22 | } 23 | } -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Models/UploadOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ManagedCode.Storage.Core.Models; 4 | 5 | public class UploadOptions : BaseOptions 6 | { 7 | public UploadOptions() 8 | { 9 | } 10 | 11 | public UploadOptions(string? fileName = null, string? directory = null, string? mimeType = null, Dictionary? metadata = null, 12 | string? fileNamePrefix = null) 13 | { 14 | FileName = fileName ?? FileName; 15 | MimeType = mimeType; 16 | Directory = directory; 17 | Metadata = metadata; 18 | FileNamePrefix = fileNamePrefix; 19 | } 20 | 21 | public string? FileNamePrefix { get; set; } 22 | public string? MimeType { get; set; } 23 | public Dictionary? Metadata { get; set; } 24 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure/Options/AzureStorageCredentialsOptions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.Storage; 3 | using Azure.Storage.Blobs; 4 | using Azure.Storage.Blobs.Models; 5 | 6 | namespace ManagedCode.Storage.Azure.Options; 7 | 8 | public class AzureStorageCredentialsOptions : IAzureStorageOptions 9 | { 10 | public string AccountName { get; set; } 11 | public string ContainerName { get; set; } 12 | 13 | public TokenCredential Credentials { get; set; } 14 | 15 | public string? Container { get; set; } 16 | public PublicAccessType PublicAccessType { get; set; } 17 | public BlobClientOptions? OriginalOptions { get; set; } 18 | public StorageTransferOptions? UploadTransferOptions { get; set; } 19 | 20 | public bool CreateContainerIfNotExists { get; set; } = true; 21 | } 22 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/Extensions/StorageOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | // using System; 2 | // using System.Reflection; 3 | // using System.Text.Json; 4 | // 5 | // namespace ManagedCode.Storage.Core.Extensions; 6 | // 7 | // public static class StorageOptionsExtensions 8 | // { 9 | // public static T DeepCopy(this T? source) where T : class, IStorageOptions 10 | // { 11 | // if (source == null) 12 | // return default; 13 | // 14 | // var options = new JsonSerializerOptions 15 | // { 16 | // WriteIndented = false, 17 | // PropertyNameCaseInsensitive = true 18 | // }; 19 | // 20 | // var json = JsonSerializer.Serialize(source, source.GetType(), options); 21 | // return (T)JsonSerializer.Deserialize(json, source.GetType(), options)!; 22 | // } 23 | // } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/PLAN.md: -------------------------------------------------------------------------------- 1 | # Dropbox integration plan 2 | 3 | - [x] Reference the official `Dropbox.Api` SDK and expose injection through `DropboxStorageOptions`. 4 | - [x] Implement `IDropboxClientWrapper` with a wrapper over `DropboxClient` that aligns with documented upload, download, list, and metadata APIs. 5 | - [x] Connect `DropboxStorage` to the shared abstractions and normalize path handling for custom root prefixes. 6 | - [ ] Add user guidance for creating an app in Dropbox, generating access tokens, and scoping permissions for file access. 7 | - [ ] Build mocks for `IDropboxClientWrapper` that mirror Dropbox metadata shapes so tests can validate uploads, downloads, and deletions without network calls. 8 | - [ ] Provide DI samples (keyed and default) so ASP.NET apps can register Dropbox storage with configuration-bound options. 9 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/AspNetTests/Abstracts/BaseControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using ManagedCode.Storage.Client; 3 | using ManagedCode.Storage.Tests.Common; 4 | using Xunit; 5 | 6 | namespace ManagedCode.Storage.Tests.AspNetTests.Abstracts; 7 | 8 | [Collection(nameof(StorageTestApplication))] 9 | public abstract class BaseControllerTests(StorageTestApplication testApplication, string apiEndpoint) 10 | { 11 | protected readonly string ApiEndpoint = apiEndpoint; 12 | protected readonly StorageTestApplication TestApplication = testApplication; 13 | 14 | protected HttpClient GetHttpClient() 15 | { 16 | return TestApplication.CreateClient(); 17 | } 18 | 19 | protected IStorageClient GetStorageClient() 20 | { 21 | return new StorageClient(TestApplication.CreateClient()); 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpBlobTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Storages.Abstracts; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Testcontainers.Sftp; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 6 | 7 | /// 8 | /// Blob tests for SFTP storage. 9 | /// 10 | public class SftpBlobTests : BlobTests 11 | { 12 | protected override SftpContainer Build() => SftpContainerFactory.Create(); 13 | 14 | protected override ServiceProvider ConfigureServices() 15 | { 16 | return SftpConfigurator.ConfigureServices( 17 | Container.GetHost(), 18 | Container.GetPort(), 19 | SftpContainerFactory.Username, 20 | SftpContainerFactory.Password, 21 | SftpContainerFactory.RemoteDirectory); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/templates/ADR-Template.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "{{Short SEO description}}" 3 | keywords: "{{comma-separated keywords}}" 4 | --- 5 | 6 | # ADR {{NNN}}: {{Title}} 7 | 8 | - Status: Proposed | Accepted | Superseded 9 | - Date: {{YYYY-MM-DD}} 10 | 11 | ## Context 12 | 13 | What problem are we solving? What constraints matter? 14 | 15 | ## Decision 16 | 17 | What did we decide and why? 18 | 19 | ## Diagram 20 | 21 | ```mermaid 22 | flowchart LR 23 | A[Before] --> B[Decision] 24 | B --> C[After] 25 | ``` 26 | 27 | ## Options Considered 28 | 29 | ### Option A 30 | 31 | - Pros: 32 | - Cons: 33 | 34 | ### Option B 35 | 36 | - Pros: 37 | - Cons: 38 | 39 | ## Consequences 40 | 41 | What changes as a result (code, tests, docs, operational impact)? 42 | 43 | ## Links 44 | 45 | - Related features: 46 | - Related code: 47 | - External references: 48 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Hubs/StorageHub.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace ManagedCode.Storage.Server.Hubs; 5 | 6 | /// 7 | /// Default hub implementation that proxies operations to the shared instance. 8 | /// 9 | public class StorageHub : StorageHubBase 10 | { 11 | /// 12 | /// Initialises a new instance of the storage hub. 13 | /// 14 | /// The storage instance hosted by the application. 15 | /// Hub options. 16 | /// Logger. 17 | public StorageHub(IStorage storage, StorageHubOptions options, ILogger logger) 18 | : base(storage, options, logger) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpStreamTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Storages.Abstracts; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Testcontainers.Sftp; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 6 | 7 | /// 8 | /// Stream tests for SFTP storage. 9 | /// 10 | public class SftpStreamTests : StreamTests 11 | { 12 | protected override SftpContainer Build() => SftpContainerFactory.Create(); 13 | 14 | protected override ServiceProvider ConfigureServices() 15 | { 16 | return SftpConfigurator.ConfigureServices( 17 | Container.GetHost(), 18 | Container.GetPort(), 19 | SftpContainerFactory.Username, 20 | SftpContainerFactory.Password, 21 | SftpContainerFactory.RemoteDirectory); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /github-pages/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "404" 4 | description: This page doesn't exist. Check the documentation navigation or go back home. 5 | permalink: /404.html 6 | --- 7 | 8 | {% assign effective_baseurl = site.baseurl %} 9 | {% if site.url != nil and site.url != '' %} 10 | {% unless site.url contains 'github.io' %} 11 | {% assign effective_baseurl = '' %} 12 | {% endunless %} 13 | {% endif %} 14 | 15 |
16 |

404

17 |

This page doesn't exist.

18 |

Check the navigation links or return to the home page.

19 |
20 | Go Home 21 | GitHub Repo 22 |
23 |
24 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpContainerTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Storages.Abstracts; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Testcontainers.Sftp; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 6 | 7 | /// 8 | /// Container tests for SFTP storage. 9 | /// 10 | public class SftpContainerTests : ContainerTests 11 | { 12 | protected override SftpContainer Build() => SftpContainerFactory.Create(); 13 | 14 | protected override ServiceProvider ConfigureServices() 15 | { 16 | return SftpConfigurator.ConfigureServices( 17 | Container.GetHost(), 18 | Container.GetPort(), 19 | SftpContainerFactory.Username, 20 | SftpContainerFactory.Password, 21 | SftpContainerFactory.RemoteDirectory); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpDownloadTests.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Tests.Storages.Abstracts; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Testcontainers.Sftp; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 6 | 7 | /// 8 | /// Download tests for SFTP storage. 9 | /// 10 | public class SftpDownloadTests : DownloadTests 11 | { 12 | protected override SftpContainer Build() => SftpContainerFactory.Create(); 13 | 14 | protected override ServiceProvider ConfigureServices() 15 | { 16 | return SftpConfigurator.ConfigureServices( 17 | Container.GetHost(), 18 | Container.GetPort(), 19 | SftpContainerFactory.Username, 20 | SftpContainerFactory.Password, 21 | SftpContainerFactory.RemoteDirectory); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/ManagedCode.Storage.Dropbox.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | ManagedCode.Storage.Dropbox 7 | ManagedCode.Storage.Dropbox 8 | Dropbox provider for ManagedCode.Storage. 9 | managedcode, storage, dropbox 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Abstracts/DownloadTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DotNet.Testcontainers.Containers; 3 | using Shouldly; 4 | using ManagedCode.Storage.Tests.Common; 5 | using Xunit; 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.Abstracts; 8 | 9 | public abstract class DownloadTests : BaseContainer where T : IContainer 10 | { 11 | [Fact] 12 | public async Task DownloadAsync_WithoutOptions_AsLocalFile() 13 | { 14 | // Arrange 15 | var fileInfo = await UploadTestFileAsync(); 16 | 17 | // Act 18 | var result = await Storage.DownloadAsync(fileInfo.Name); 19 | 20 | // Assert 21 | result.IsSuccess 22 | .ShouldBeTrue(); 23 | result.Value!.FileInfo 24 | .Length 25 | .ShouldBe(fileInfo.Length); 26 | 27 | await Storage.DeleteAsync(fileInfo.Name); 28 | } 29 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Client/ManagedCode.Storage.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Library 6 | 7 | 8 | 9 | 10 | ManagedCode.Storage.Client 11 | ManagedCode.Storage.Client 12 | Extensions for ASP.NET for Storage 13 | managedcode, aws, gcp, azure storage, cloud, asp.net, file, upload, download 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.FileSystem/ManagedCode.Storage.FileSystem.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.FileSystem 10 | ManagedCode.Storage.FileSystem 11 | Storage for FileSystem 12 | managedcode, file, storage, filesystem 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.GoogleDrive/ManagedCode.Storage.GoogleDrive.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | ManagedCode.Storage.GoogleDrive 7 | ManagedCode.Storage.GoogleDrive 8 | Google Drive provider for ManagedCode.Storage. 9 | managedcode, storage, google drive 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.OneDrive/ManagedCode.Storage.OneDrive.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | ManagedCode.Storage.OneDrive 7 | ManagedCode.Storage.OneDrive 8 | Storage provider for Microsoft OneDrive built on Microsoft Graph. 9 | managedcode, storage, onedrive, microsoft graph 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.GoogleDrive/PLAN.md: -------------------------------------------------------------------------------- 1 | # Google Drive integration plan 2 | 3 | - [x] Reference the official `Google.Apis.Drive.v3` client and thread it through `GoogleDriveStorageOptions`. 4 | - [x] Build `IGoogleDriveClient` with a Drive-service backed implementation that honors folder hierarchies, metadata fields, and official upload/download patterns. 5 | - [x] Adapt `GoogleDriveStorage` to produce `BlobMetadata` results and operate through the shared `BaseStorage` contract. 6 | - [ ] Provide quick-start instructions for OAuth client configuration, service account usage, and refresh-token setup for console and ASP.NET apps. 7 | - [ ] Expand tests with deterministic `IGoogleDriveClient` fakes that simulate Drive folder traversal, file uploads, range downloads, deletions, and metadata fetches. 8 | - [ ] Add docs showing the minimal Drive scopes (`https://www.googleapis.com/auth/drive.file`) and how to inject authenticated `DriveService` instances via DI. 9 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Aws/Extensions/StorageFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Aws.Options; 3 | using ManagedCode.Storage.Core.Providers; 4 | 5 | namespace ManagedCode.Storage.Aws.Extensions; 6 | 7 | public static class StorageFactoryExtensions 8 | { 9 | public static IAWSStorage CreateAWSStorage(this IStorageFactory factory, string bucketName) 10 | { 11 | return factory.CreateStorage(options => options.Bucket = bucketName); 12 | } 13 | 14 | public static IAWSStorage CreateAWSStorage(this IStorageFactory factory, AWSStorageOptions options) 15 | { 16 | return factory.CreateStorage(options); 17 | } 18 | 19 | 20 | public static IAWSStorage CreateAWSStorage(this IStorageFactory factory, Action options) 21 | { 22 | return factory.CreateStorage(options); 23 | } 24 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/Clients/ICloudKitClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ManagedCode.Storage.CloudKit.Clients; 7 | 8 | public interface ICloudKitClient 9 | { 10 | Task UploadAsync(string recordName, string internalPath, Stream content, string contentType, CancellationToken cancellationToken); 11 | 12 | Task DownloadAsync(string recordName, CancellationToken cancellationToken); 13 | 14 | Task DeleteAsync(string recordName, CancellationToken cancellationToken); 15 | 16 | Task ExistsAsync(string recordName, CancellationToken cancellationToken); 17 | 18 | Task GetRecordAsync(string recordName, CancellationToken cancellationToken); 19 | 20 | IAsyncEnumerable QueryByPathPrefixAsync(string pathPrefix, CancellationToken cancellationToken); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/ManagedCode.Storage.CloudKit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | ManagedCode.Storage.CloudKit 7 | ManagedCode.Storage.CloudKit 8 | Apple CloudKit (iCloud app data) provider for ManagedCode.Storage. 9 | managedcode, storage, cloudkit, icloud 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigurator.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Azure.Extensions; 2 | using ManagedCode.Storage.Azure.Options; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | // ReSharper disable MethodHasAsyncOverload 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.Azure; 8 | 9 | public class AzureConfigurator 10 | { 11 | public static ServiceProvider ConfigureServices(string connectionString) 12 | { 13 | var services = new ServiceCollection(); 14 | 15 | services.AddAzureStorageAsDefault(opt => 16 | { 17 | opt.Container = "managed-code-bucket"; 18 | opt.ConnectionString = connectionString; 19 | }); 20 | 21 | services.AddAzureStorage(new AzureStorageOptions 22 | { 23 | Container = "managed-code-bucket", 24 | ConnectionString = connectionString 25 | }); 26 | return services.BuildServiceProvider(); 27 | } 28 | } -------------------------------------------------------------------------------- /github-pages/sitemap.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | {% assign effective_baseurl = site.baseurl %} 5 | {% if site.url != nil and site.url != '' %} 6 | {% unless site.url contains 'github.io' %} 7 | {% assign effective_baseurl = '' %} 8 | {% endunless %} 9 | {% endif %} 10 | 11 | 12 | {% for page in site.pages %} 13 | {% assign last_char = page.url | slice: -1 %} 14 | {% if page.url == '/' or page.url contains '.html' or last_char == '/' %} 15 | {% if page.url != '/404.html' and page.url != '/sitemap.xml' and page.url != '/robots.txt' %} 16 | 17 | {{ site.url }}{{ effective_baseurl }}{{ page.url | remove: 'index.html' }} 18 | {{ site.time | date: '%Y-%m-%d' }} 19 | {% if page.url == '/' %}1.0{% else %}0.8{% endif %} 20 | 21 | {% endif %} 22 | {% endif %} 23 | {% endfor %} 24 | 25 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemConfigurator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using ManagedCode.Storage.FileSystem.Extensions; 4 | using ManagedCode.Storage.FileSystem.Options; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.FileSystem; 8 | 9 | public class FileSystemConfigurator 10 | { 11 | public static ServiceProvider ConfigureServices(string connectionString) 12 | { 13 | connectionString += Random.Shared.NextInt64(); 14 | var services = new ServiceCollection(); 15 | 16 | services.AddFileSystemStorageAsDefault(opt => { opt.BaseFolder = Path.Combine(Environment.CurrentDirectory, connectionString); }); 17 | services.AddFileSystemStorage(new FileSystemStorageOptions 18 | { 19 | BaseFolder = Path.Combine(Environment.CurrentDirectory, connectionString) 20 | }); 21 | return services.BuildServiceProvider(); 22 | } 23 | } -------------------------------------------------------------------------------- /github-pages/_config.yml: -------------------------------------------------------------------------------- 1 | title: Storage 2 | tagline: Cross-provider storage toolkit for .NET 3 | description: ManagedCode.Storage wraps vendor SDKs behind a single IStorage abstraction so uploads, downloads, metadata, streaming, and retention behave the same across providers. 4 | url: "https://storage.managed-code.com" 5 | baseurl: "" 6 | 7 | # SEO & AEO (AI Engine Optimization) 8 | keywords: "ManagedCode.Storage, IStorage, blob storage, Azure Blob Storage, Azure Data Lake, Amazon S3, Google Cloud Storage, OneDrive, Google Drive, Dropbox, CloudKit, SFTP, .NET, ASP.NET, SignalR, streaming uploads, chunked uploads" 9 | author: ManagedCode 10 | 11 | # Structured Data for LLMs 12 | organization: ManagedCode 13 | github_repo: managedcode/Storage 14 | license: MIT 15 | og_image: /assets/images/og-image.png 16 | 17 | markdown: kramdown 18 | kramdown: 19 | auto_ids: true 20 | input: GFM 21 | 22 | plugins: 23 | - jekyll-seo-tag 24 | 25 | exclude: 26 | - Gemfile 27 | - Gemfile.lock 28 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Google/Extensions/StorageFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core.Providers; 3 | using ManagedCode.Storage.Google.Options; 4 | 5 | namespace ManagedCode.Storage.Google.Extensions; 6 | 7 | public static class StorageFactoryExtensions 8 | { 9 | public static IGCPStorage CreateGCPStorage(this IStorageFactory factory, string containerName) 10 | { 11 | return factory.CreateStorage(options => options.BucketOptions.Bucket = containerName); 12 | } 13 | 14 | public static IGCPStorage CreateGCPStorage(this IStorageFactory factory, GCPStorageOptions options) 15 | { 16 | return factory.CreateStorage(options); 17 | } 18 | 19 | 20 | public static IGCPStorage CreateGCPStorage(this IStorageFactory factory, Action options) 21 | { 22 | return factory.CreateStorage(options); 23 | } 24 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/AWS/AwsContainerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using DotNet.Testcontainers.Builders; 4 | using ManagedCode.Storage.Tests.Common; 5 | using Testcontainers.LocalStack; 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.AWS; 8 | 9 | internal static class AwsContainerFactory 10 | { 11 | private const int EdgePort = 4566; 12 | 13 | public static LocalStackContainer Create() 14 | { 15 | return new LocalStackBuilder() 16 | .WithImage(ContainerImages.LocalStack) 17 | .WithEnvironment("SERVICES", "s3") 18 | .WithWaitStrategy(Wait.ForUnixContainer() 19 | .UntilHttpRequestIsSucceeded(request => request 20 | .ForPort(EdgePort) 21 | .ForPath("/_localstack/health") 22 | .ForStatusCode(HttpStatusCode.OK), 23 | wait => wait.WithTimeout(TimeSpan.FromMinutes(5)))) 24 | .Build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/DependencyInjection/ChunkUploadServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Server.ChunkUpload; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ManagedCode.Storage.Server.Extensions.DependencyInjection; 6 | 7 | /// 8 | /// Provides DI helpers for configuring chunk upload services. 9 | /// 10 | public static class ChunkUploadServiceCollectionExtensions 11 | { 12 | /// 13 | /// Registers with optional configuration. 14 | /// 15 | public static IServiceCollection AddChunkUploadHandling(this IServiceCollection services, Action? configure = null) 16 | { 17 | var options = new ChunkUploadOptions(); 18 | configure?.Invoke(options); 19 | 20 | services.AddSingleton(options); 21 | services.AddSingleton(); 22 | return services; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure/Extensions/StorageFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Azure.Options; 3 | using ManagedCode.Storage.Core.Providers; 4 | 5 | namespace ManagedCode.Storage.Azure.Extensions; 6 | 7 | public static class StorageFactoryExtensions 8 | { 9 | public static IAzureStorage CreateAzureStorage(this IStorageFactory factory, string containerName) 10 | { 11 | return factory.CreateStorage(options => options.Container = containerName); 12 | } 13 | 14 | public static IAzureStorage CreateAzureStorage(this IStorageFactory factory, IAzureStorageOptions options) 15 | { 16 | return factory.CreateStorage(options); 17 | } 18 | 19 | 20 | public static IAzureStorage CreateAzureStorage(this IStorageFactory factory, Action options) 21 | { 22 | return factory.CreateStorage(options); 23 | } 24 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Sftp/ISftpStorage.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Communication; 5 | using ManagedCode.Storage.Core; 6 | using ManagedCode.Storage.Sftp.Options; 7 | 8 | namespace ManagedCode.Storage.Sftp; 9 | 10 | /// 11 | /// Contract implemented by the SFTP storage provider for stream-oriented operations. 12 | /// 13 | public interface ISftpStorage : IStorage 14 | { 15 | Task> OpenReadStreamAsync(string fileName, CancellationToken cancellationToken = default); 16 | Task> OpenWriteStreamAsync(string fileName, CancellationToken cancellationToken = default); 17 | Task> TestConnectionAsync(CancellationToken cancellationToken = default); 18 | Task> GetWorkingDirectoryAsync(CancellationToken cancellationToken = default); 19 | Task ChangeWorkingDirectoryAsync(string directory, CancellationToken cancellationToken = default); 20 | } 21 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/StorageEndpointRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Server.Hubs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Routing; 4 | 5 | namespace ManagedCode.Storage.Server.Extensions; 6 | 7 | /// 8 | /// Provides convenience routing extensions for storage endpoints. 9 | /// 10 | public static class StorageEndpointRouteBuilderExtensions 11 | { 12 | /// 13 | /// Maps the default storage SignalR hub to the specified route pattern. 14 | /// 15 | /// Endpoint route builder. 16 | /// Route pattern for the hub. 17 | /// The original . 18 | public static IEndpointRouteBuilder MapStorageHub(this IEndpointRouteBuilder endpoints, string pattern = "/hubs/storage") 19 | { 20 | endpoints.MapHub(pattern); 21 | return endpoints; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ManagedCode.Storage.TestFakes/FakeAWSStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Amazon.S3; 5 | using ManagedCode.Communication; 6 | using ManagedCode.Storage.Aws; 7 | using ManagedCode.Storage.Aws.Options; 8 | using ManagedCode.Storage.FileSystem; 9 | using ManagedCode.Storage.FileSystem.Options; 10 | 11 | namespace ManagedCode.Storage.TestFakes; 12 | 13 | public class FakeAWSStorage : FileSystemStorage, IAWSStorage 14 | { 15 | public FakeAWSStorage() : base(new FileSystemStorageOptions()) 16 | { 17 | } 18 | 19 | public IAmazonS3 StorageClient { get; } 20 | 21 | public Task SetStorageOptions(AWSStorageOptions options, CancellationToken cancellationToken = default) 22 | { 23 | return Task.FromResult(Result.Succeed()); 24 | } 25 | 26 | public Task SetStorageOptions(Action options, CancellationToken cancellationToken = default) 27 | { 28 | return Task.FromResult(Result.Succeed()); 29 | } 30 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/ChunkUpload/ChunkUploadOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace ManagedCode.Storage.Server.ChunkUpload; 5 | 6 | /// 7 | /// Options controlling how chunked uploads are persisted while all parts arrive. 8 | /// 9 | public class ChunkUploadOptions 10 | { 11 | /// 12 | /// Absolute path where temporary chunk data is persisted. Defaults to . 13 | /// 14 | public string TempPath { get; set; } = Path.Combine(Path.GetTempPath(), "managedcode-storage", "chunks"); 15 | 16 | /// 17 | /// How long chunks are kept on disk after the last write. Expired sessions are cleaned up on completion or abort. 18 | /// 19 | public TimeSpan SessionTtl { get; set; } = TimeSpan.FromHours(1); 20 | 21 | /// 22 | /// Maximum number of concurrent active chunk sessions cached in memory. 23 | /// 24 | public int MaxActiveSessions { get; set; } = 100; 25 | } 26 | -------------------------------------------------------------------------------- /ManagedCode.Storage.TestFakes/FakeGoogleStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Google.Cloud.Storage.V1; 5 | using ManagedCode.Communication; 6 | using ManagedCode.Storage.FileSystem; 7 | using ManagedCode.Storage.FileSystem.Options; 8 | using ManagedCode.Storage.Google; 9 | using ManagedCode.Storage.Google.Options; 10 | 11 | namespace ManagedCode.Storage.TestFakes; 12 | 13 | public class FakeGoogleStorage : FileSystemStorage, IGCPStorage 14 | { 15 | public FakeGoogleStorage() : base(new FileSystemStorageOptions()) 16 | { 17 | } 18 | 19 | public StorageClient StorageClient { get; } 20 | 21 | public Task SetStorageOptions(GCPStorageOptions options, CancellationToken cancellationToken = default) 22 | { 23 | return Task.FromResult(Result.Succeed()); 24 | } 25 | 26 | public Task SetStorageOptions(Action options, CancellationToken cancellationToken = default) 27 | { 28 | return Task.FromResult(Result.Succeed()); 29 | } 30 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Controllers/StorageController.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core; 2 | using ManagedCode.Storage.Server.ChunkUpload; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace ManagedCode.Storage.Server.Controllers; 6 | 7 | /// 8 | /// Default storage controller exposing all storage endpoints using the shared instance. 9 | /// 10 | [Route("api/storage")] 11 | public class StorageController : StorageControllerBase 12 | { 13 | /// 14 | /// Initialises a new instance of the default storage controller. 15 | /// 16 | /// The shared storage instance. 17 | /// Chunk upload coordinator. 18 | /// Server behaviour options. 19 | public StorageController( 20 | IStorage storage, 21 | ChunkUploadService chunkUploadService, 22 | StorageServerOptions options) : base(storage, chunkUploadService, options) 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Aws/ManagedCode.Storage.Aws.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.Aws 10 | ManagedCode.Storage.Aws 11 | Storage for AWS 12 | managedcode, aws, storage, cloud, s3, blob 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.FileSystem/Extensions/StorageFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core.Providers; 3 | using ManagedCode.Storage.FileSystem.Options; 4 | 5 | namespace ManagedCode.Storage.FileSystem.Extensions; 6 | 7 | public static class StorageFactoryExtensions 8 | { 9 | public static IFileSystemStorage CreateFileSystemStorage(this IStorageFactory factory, string baseFolder) 10 | { 11 | return factory.CreateStorage(options => options.BaseFolder = baseFolder); 12 | } 13 | 14 | public static IFileSystemStorage CreateFileSystemStorage(this IStorageFactory factory, FileSystemStorageOptions options) 15 | { 16 | return factory.CreateStorage(options); 17 | } 18 | 19 | 20 | public static IFileSystemStorage CreateFileSystemStorage(this IStorageFactory factory, Action options) 21 | { 22 | return factory.CreateStorage(options); 23 | } 24 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Sftp/ManagedCode.Storage.Sftp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.Sftp 10 | ManagedCode.Storage.Sftp 11 | ManagedCode storage provider backed by SFTP over SSH. 12 | managedcode, sftp, storage, ssh, blob, file 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/AspNetTests/Abstracts/BaseSignalRStorageTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Client.SignalR; 3 | using ManagedCode.Storage.Client.SignalR.Models; 4 | using ManagedCode.Storage.Tests.Common; 5 | 6 | namespace ManagedCode.Storage.Tests.AspNetTests.Abstracts; 7 | 8 | public abstract class BaseSignalRStorageTests : BaseControllerTests 9 | { 10 | protected BaseSignalRStorageTests(StorageTestApplication testApplication, string apiEndpoint) 11 | : base(testApplication, apiEndpoint) 12 | { 13 | } 14 | 15 | protected StorageSignalRClient CreateClient(Action? configure = null) 16 | { 17 | return TestApplication.CreateSignalRClient(configure); 18 | } 19 | 20 | protected static StorageUploadStreamDescriptor CreateDescriptor(string fileName, string contentType, long? length) 21 | { 22 | return new StorageUploadStreamDescriptor 23 | { 24 | FileName = fileName, 25 | ContentType = contentType, 26 | FileSize = length 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.OneDrive/Clients/IOneDriveClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Graph.Models; 6 | 7 | namespace ManagedCode.Storage.OneDrive.Clients; 8 | 9 | public interface IOneDriveClient 10 | { 11 | Task EnsureRootAsync(string driveId, string rootPath, bool createIfNotExists, CancellationToken cancellationToken); 12 | 13 | Task UploadAsync(string driveId, string path, Stream content, string? contentType, CancellationToken cancellationToken); 14 | 15 | Task DownloadAsync(string driveId, string path, CancellationToken cancellationToken); 16 | 17 | Task DeleteAsync(string driveId, string path, CancellationToken cancellationToken); 18 | 19 | Task ExistsAsync(string driveId, string path, CancellationToken cancellationToken); 20 | 21 | Task GetMetadataAsync(string driveId, string path, CancellationToken cancellationToken); 22 | 23 | IAsyncEnumerable ListAsync(string driveId, string? directory, CancellationToken cancellationToken); 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Managed-Code 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/Extensions/StorageFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Azure.DataLake.Options; 3 | using ManagedCode.Storage.Core.Providers; 4 | 5 | namespace ManagedCode.Storage.Azure.DataLake.Extensions; 6 | 7 | public static class StorageFactoryExtensions 8 | { 9 | public static IAzureDataLakeStorage CreateAzureDataLakeStorage(this IStorageFactory factory, string fileSystemName) 10 | { 11 | return factory.CreateStorage(options => options.FileSystem = fileSystemName); 12 | } 13 | 14 | public static IAzureDataLakeStorage CreateAzureDataLakeStorage(this IStorageFactory factory, AzureDataLakeStorageOptions options) 15 | { 16 | return factory.CreateStorage(options); 17 | } 18 | 19 | 20 | public static IAzureDataLakeStorage CreateAzureDataLakeStorage(this IStorageFactory factory, Action options) 21 | { 22 | return factory.CreateStorage(options); 23 | } 24 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/IAzureDataLakeStorage.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Azure.Storage.Files.DataLake; 5 | using ManagedCode.Communication; 6 | using ManagedCode.Storage.Azure.DataLake.Options; 7 | using ManagedCode.Storage.Core; 8 | 9 | namespace ManagedCode.Storage.Azure.DataLake; 10 | 11 | public interface IAzureDataLakeStorage : IStorage 12 | { 13 | /// 14 | /// Create directory 15 | /// 16 | Task CreateDirectoryAsync(string directory, CancellationToken cancellationToken = default); 17 | 18 | /// 19 | /// Rename directory 20 | /// 21 | Task RenameDirectory(string directory, string newDirectory, CancellationToken cancellationToken = default); 22 | 23 | Task> OpenWriteStreamAsync(OpenWriteStreamOptions options, CancellationToken cancellationToken = default); 24 | 25 | Task> OpenReadStreamAsync(OpenReadStreamOptions options, CancellationToken cancellationToken = default); 26 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.Azure 10 | ManagedCode.Storage.Azure 11 | Storage for Azure 12 | managedcode, azure, storage, cloud, blob 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/Clients/IDropboxClientWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Dropbox.Api.Files; 6 | 7 | namespace ManagedCode.Storage.Dropbox.Clients; 8 | 9 | public interface IDropboxClientWrapper 10 | { 11 | Task EnsureRootAsync(string rootPath, bool createIfNotExists, CancellationToken cancellationToken); 12 | 13 | Task UploadAsync(string rootPath, string path, Stream content, string? contentType, CancellationToken cancellationToken); 14 | 15 | Task DownloadAsync(string rootPath, string path, CancellationToken cancellationToken); 16 | 17 | Task DeleteAsync(string rootPath, string path, CancellationToken cancellationToken); 18 | 19 | Task ExistsAsync(string rootPath, string path, CancellationToken cancellationToken); 20 | 21 | Task GetMetadataAsync(string rootPath, string path, CancellationToken cancellationToken); 22 | 23 | IAsyncEnumerable ListAsync(string rootPath, string? directory, CancellationToken cancellationToken); 24 | } 25 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.OneDrive/PLAN.md: -------------------------------------------------------------------------------- 1 | # OneDrive integration plan 2 | 3 | - [x] Reference the official `Microsoft.Graph` SDK and configure `GraphServiceClient` injection through `OneDriveStorageOptions`. 4 | - [x] Implement `IOneDriveClient` plus `GraphOneDriveClient` to mirror upload, download, metadata, and listing APIs documented for Microsoft Graph drives. 5 | - [x] Create `OneDriveStorage` that adapts `BaseStorage` to OneDrive paths, normalizes root prefixes, and returns `BlobMetadata` compatible with the shared abstractions. 6 | - [x] Provide DI-friendly `OneDriveStorageProvider` so ASP.NET and worker hosts can register the provider alongside keyed/default storage bindings. 7 | - [ ] Add sample ASP.NET controller snippets showing how to request delegated or app-only permissions and pass a configured `GraphServiceClient` into `OneDriveStorageOptions`. 8 | - [ ] Extend tests with `IOneDriveClient` mocks that mirror Graph responses for uploads, downloads, listings, deletion, and metadata resolution. 9 | - [ ] Document user-facing setup: Azure App Registration, scopes (`Files.ReadWrite.All`), and the minimal token acquisition steps for CLI and ASP.NET hosts. 10 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/DependencyInjection/StorageSignalRServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Server.Hubs; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ManagedCode.Storage.Server.Extensions.DependencyInjection; 6 | 7 | /// 8 | /// Provides registration helpers for SignalR-based storage streaming. 9 | /// 10 | public static class StorageSignalRServiceCollectionExtensions 11 | { 12 | /// 13 | /// Registers for SignalR storage hubs. 14 | /// 15 | /// Target service collection. 16 | /// Optional configuration delegate for hub options. 17 | /// The original . 18 | public static IServiceCollection AddStorageSignalR(this IServiceCollection services, Action? configure = null) 19 | { 20 | var options = new StorageHubOptions(); 21 | configure?.Invoke(options); 22 | services.AddSingleton(options); 23 | return services; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | env: 10 | DOTNET_VERSION: '10.0.x' 11 | 12 | jobs: 13 | build-and-test: 14 | name: build-and-test 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v5 20 | 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: ${{ env.DOTNET_VERSION }} 25 | 26 | - name: Restore dependencies 27 | run: dotnet restore ManagedCode.Storage.slnx 28 | 29 | - name: Build 30 | run: dotnet build ManagedCode.Storage.slnx --configuration Release --no-restore 31 | 32 | - name: Test 33 | run: dotnet test Tests/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" 34 | 35 | - name: Upload coverage reports to Codecov 36 | uses: codecov/codecov-action@v5 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | files: ./**/coverage.cobertura.xml 40 | fail_ci_if_error: false 41 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Hubs/StorageHubOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ManagedCode.Storage.Server.Hubs; 4 | 5 | /// 6 | /// Configures runtime behaviour of the storage SignalR hub. 7 | /// 8 | public class StorageHubOptions 9 | { 10 | /// 11 | /// Temporary folder where incoming SignalR uploads are staged before being committed to storage. 12 | /// 13 | public string TempPath { get; set; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "managedcode-storage-hub"); 14 | 15 | /// 16 | /// Size of the buffer used when streaming data to and from storage. 17 | /// 18 | public int StreamBufferSize { get; set; } = 64 * 1024; 19 | 20 | /// 21 | /// Maximum number of simultaneous streaming transfers per hub instance. Zero or negative disables the limit. 22 | /// 23 | public int MaxConcurrentTransfers { get; set; } = 0; 24 | 25 | /// 26 | /// Gets or sets the timeout after which idle transfers are canceled. 27 | /// 28 | public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(10); 29 | } 30 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/ManagedCode.Storage.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Library 6 | 7 | 8 | 9 | 10 | ManagedCode.Storage.Server 11 | ManagedCode.Storage.Server 12 | Extensions for ASP.NET for Storage 13 | managedcode, aws, gcp, azure storage, cloud, asp.net, file, upload, download 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSUploadTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using ManagedCode.Storage.Tests.Common; 3 | using ManagedCode.Storage.Tests.Storages.Abstracts; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Testcontainers.FakeGcsServer; 6 | using Xunit; 7 | 8 | namespace ManagedCode.Storage.Tests.Storages.GCS; 9 | 10 | public class GCSUploadTests : UploadTests 11 | { 12 | protected override FakeGcsServerContainer Build() 13 | { 14 | return new FakeGcsServerBuilder().WithImage(ContainerImages.FakeGCSServer) 15 | .Build(); 16 | } 17 | 18 | protected override ServiceProvider ConfigureServices() 19 | { 20 | return GCSConfigurator.ConfigureServices(Container.GetConnectionString()); 21 | } 22 | 23 | [Theory(Skip = "FakeGcsServer currently throttles uploads beyond ~10MB; skip large-stream scenario for emulator")] 24 | [Trait("Category", "LargeFile")] 25 | [InlineData(1)] 26 | [InlineData(3)] 27 | [InlineData(5)] 28 | public override Task UploadAsync_LargeStream_ShouldRoundTrip(int gigabytes) 29 | { 30 | _ = gigabytes; 31 | return Task.CompletedTask; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Amazon.S3; 2 | using ManagedCode.Storage.Aws.Extensions; 3 | using ManagedCode.Storage.Aws.Options; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | // ReSharper disable MethodHasAsyncOverload 7 | 8 | namespace ManagedCode.Storage.Tests.Storages.AWS; 9 | 10 | public class AWSConfigurator 11 | { 12 | public static ServiceProvider ConfigureServices(string connectionString) 13 | { 14 | var services = new ServiceCollection(); 15 | 16 | var config = new AmazonS3Config(); 17 | config.ServiceURL = connectionString; 18 | 19 | services.AddAWSStorageAsDefault(opt => 20 | { 21 | opt.PublicKey = "localkey"; 22 | opt.SecretKey = "localsecret"; 23 | opt.Bucket = "managed-code-bucket"; 24 | opt.OriginalOptions = config; 25 | }); 26 | 27 | services.AddAWSStorage(new AWSStorageOptions 28 | { 29 | PublicKey = "localkey", 30 | SecretKey = "localsecret", 31 | Bucket = "managed-code-bucket", 32 | OriginalOptions = config 33 | }); 34 | return services.BuildServiceProvider(); 35 | } 36 | } -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/ManagedCode.Storage.Azure.DataLake.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.AzureDataLake 10 | ManagedCode.Storage.Azure.DataLake 11 | Storage for AzureDataLake 12 | managedcode, azure, storage, cloud, blob, datalake, data, lake 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/UploadStreamDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ManagedCode.Storage.Server.Models; 4 | 5 | /// 6 | /// Describes the metadata associated with a streamed upload request. 7 | /// 8 | public class UploadStreamDescriptor 9 | { 10 | /// 11 | /// Gets or sets the optional transfer identifier supplied by the caller. 12 | /// 13 | public string? TransferId { get; set; } 14 | /// 15 | /// Gets or sets the file name persisted to storage. 16 | /// 17 | public string FileName { get; set; } = string.Empty; 18 | /// 19 | /// Gets or sets the target directory. 20 | /// 21 | public string? Directory { get; set; } 22 | /// 23 | /// Gets or sets the MIME type associated with the upload. 24 | /// 25 | public string? ContentType { get; set; } 26 | /// 27 | /// Gets or sets the expected file size, if known. 28 | /// 29 | public long? FileSize { get; set; } 30 | /// 31 | /// Gets or sets optional metadata which will be forwarded to storage. 32 | /// 33 | public Dictionary? Metadata { get; set; } 34 | } 35 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/File/FormFileExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using ManagedCode.Storage.Core.Models; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | namespace ManagedCode.Storage.Server.Extensions.File; 10 | 11 | public static class FormFileExtensions 12 | { 13 | public static async Task ToLocalFileAsync(this IFormFile formFile, CancellationToken cancellationToken = default) 14 | { 15 | var localFile = LocalFile.FromRandomNameWithExtension(formFile.FileName); 16 | await using (var stream = formFile.OpenReadStream()) 17 | { 18 | await localFile.CopyFromStreamAsync(stream, cancellationToken); 19 | } 20 | return localFile; 21 | } 22 | 23 | public static async IAsyncEnumerable ToLocalFilesAsync(this IFormFileCollection formFileCollection, 24 | [EnumeratorCancellation] CancellationToken cancellationToken = default) 25 | { 26 | foreach (var localFileTask in formFileCollection.Select(formFile => formFile.ToLocalFileAsync(cancellationToken))) 27 | { 28 | yield return await localFileTask; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ManagedCode.Storage.TestFakes/ManagedCode.Storage.TestFakes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.TestFakes 10 | ManagedCode.Storage.TestFakes 11 | Fake implementaions for units tests 12 | managedcode, aws, gcp, azure storage, cloud, asp.net, file, upload, download 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Google/ManagedCode.Storage.Google.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | ManagedCode.Storage.Gcp 10 | ManagedCode.Storage.Gcp 11 | Storage for Google Cloud Storage 12 | managedcode, gcs, storage, cloud, blob 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpUploadTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using ManagedCode.Storage.Tests.Storages.Abstracts; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Testcontainers.Sftp; 5 | using Xunit; 6 | 7 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 8 | 9 | /// 10 | /// Upload tests for SFTP storage. 11 | /// 12 | public class SftpUploadTests : UploadTests 13 | { 14 | protected override SftpContainer Build() => SftpContainerFactory.Create(); 15 | 16 | protected override ServiceProvider ConfigureServices() 17 | { 18 | return SftpConfigurator.ConfigureServices( 19 | Container.GetHost(), 20 | Container.GetPort(), 21 | SftpContainerFactory.Username, 22 | SftpContainerFactory.Password, 23 | SftpContainerFactory.RemoteDirectory); 24 | } 25 | 26 | [Fact(Skip = "Cancellation not working reliably with containerized SFTP server - uploads complete too quickly to cancel")] 27 | public override async Task UploadAsync_WithCancellationToken_BigFile_ShouldCancel() 28 | { 29 | // This method is skipped - the containerized SFTP server completes uploads too quickly to be cancelled effectively 30 | await Task.CompletedTask; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.GoogleDrive/Clients/IGoogleDriveClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using DriveFile = Google.Apis.Drive.v3.Data.File; 6 | 7 | namespace ManagedCode.Storage.GoogleDrive.Clients; 8 | 9 | public interface IGoogleDriveClient 10 | { 11 | Task EnsureRootAsync(string rootFolderId, bool createIfNotExists, CancellationToken cancellationToken); 12 | 13 | Task UploadAsync(string rootFolderId, string path, Stream content, string? contentType, bool supportsAllDrives, CancellationToken cancellationToken); 14 | 15 | Task DownloadAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken); 16 | 17 | Task DeleteAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken); 18 | 19 | Task ExistsAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken); 20 | 21 | Task GetMetadataAsync(string rootFolderId, string path, bool supportsAllDrives, CancellationToken cancellationToken); 22 | 23 | IAsyncEnumerable ListAsync(string rootFolderId, string? directory, bool supportsAllDrives, CancellationToken cancellationToken); 24 | } 25 | -------------------------------------------------------------------------------- /docs/templates/Feature-Template.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "{{Short SEO description}}" 3 | keywords: "{{comma-separated keywords}}" 4 | --- 5 | 6 | # Feature: {{Feature Name}} 7 | 8 | ## Purpose 9 | 10 | Explain what the feature does and why it exists. 11 | 12 | ## Main Flows 13 | 14 | Describe the primary user / system flows. 15 | 16 | ```mermaid 17 | flowchart TD 18 | A[Caller] --> B[Component] 19 | B --> C[Dependency] 20 | C --> D[Result] 21 | ``` 22 | 23 | ## Components 24 | 25 | List the main code components involved (projects, folders, classes, public APIs). 26 | 27 | - Projects: 28 | - Key types: 29 | - Key entry points: 30 | 31 | ## Current Behavior 32 | 33 | Describe how it behaves today (happy path, errors, edge cases, constraints). 34 | 35 | ## Configuration 36 | 37 | Document configuration and DI wiring (options, required secrets, environment variables). 38 | 39 | ## Tests 40 | 41 | List the tests that cover this feature and what they assert. 42 | 43 | - Existing tests: 44 | - Gaps / TODO: 45 | 46 | ## Definition of Done 47 | 48 | - Core flow is implemented and documented. 49 | - Automated tests verify real behaviour (not only mocked interactions). 50 | - `dotnet test` passes for the repository test project. 51 | - Docs updated (README + `docs/`). 52 | 53 | ## References 54 | 55 | - Related docs: 56 | - Related ADRs: 57 | - External specs / SDK docs: 58 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Client.SignalR/ManagedCode.Storage.Client.SignalR.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Library 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | ManagedCode.Storage.Client.SignalR 13 | ManagedCode.Storage.Client.SignalR 14 | SignalR client for ManagedCode.Storage streaming and transfer operations. 15 | managedcode, storage, signalr, streaming, upload, download 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ManagedCode.Storage.VirtualFileSystem/ManagedCode.Storage.VirtualFileSystem.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ManagedCode.Storage.VirtualFileSystem 5 | ManagedCode.Storage.VirtualFileSystem 6 | Virtual FileSystem abstraction over ManagedCode.Storage blob providers 7 | ManagedCode 8 | https://github.com/managedcode/Storage 9 | https://github.com/managedcode/Storage 10 | git 11 | storage;blob;azure;aws;s3;gcp;filesystem;virtual;vfs 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/Features/mime-and-crc.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "MimeHelper, content-type, CRC32, integrity, ManagedCode.MimeTypes, ManagedCode.Storage.Core" 3 | --- 4 | 5 | # Feature: MIME & Integrity Helpers (MimeHelper + CRC32) 6 | 7 | ## Purpose 8 | 9 | Provide consistent content-type and integrity behaviour across providers and integrations: 10 | 11 | - MIME type resolution via `MimeHelper` 12 | - streamed CRC32 calculation via `Crc32Helper` for large-file validation and mirroring scenarios 13 | 14 | ## Main Flows 15 | 16 | ```mermaid 17 | flowchart LR 18 | FileName[File name] --> Mime[MimeHelper.GetMimeType] 19 | Stream[Stream] --> CRC[Crc32Helper] 20 | CRC --> Validation[Compare checksums] 21 | ``` 22 | 23 | ## Components 24 | 25 | - MIME: 26 | - `ManagedCode.MimeTypes` (`MimeHelper`) 27 | - CRC: 28 | - `ManagedCode.Storage.Core/Helpers/Crc32Helper.cs` 29 | - `ManagedCode.Storage.Core/Helpers/PathHelper.cs` (shared path utilities used by storages) 30 | 31 | ## Current Behavior 32 | 33 | - MIME type is typically derived from the file name and stored as `BlobMetadata.MimeType` where providers support it. 34 | - CRC32 can be computed without loading full content into memory (streamed processing). 35 | - All MIME lookups should go through `MimeHelper` (avoid provider-specific or ad-hoc MIME detection). 36 | 37 | ## Tests 38 | 39 | - `Tests/ManagedCode.Storage.Tests/Core/Crc32HelperTests.cs` 40 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Helpers/StreamHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Net.Http.Headers; 4 | 5 | namespace ManagedCode.Storage.Server.Helpers; 6 | 7 | internal static class StreamHelper 8 | { 9 | public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) 10 | { 11 | var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; 12 | 13 | if (string.IsNullOrWhiteSpace(boundary)) 14 | { 15 | throw new InvalidDataException("Missing content-type boundary."); 16 | } 17 | 18 | if (boundary.Length > lengthLimit) 19 | { 20 | throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); 21 | } 22 | 23 | return boundary; 24 | } 25 | 26 | public static bool IsMultipartContentType(string? contentType) 27 | { 28 | return !string.IsNullOrEmpty(contentType) 29 | && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; 30 | } 31 | 32 | public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) 33 | { 34 | return contentDisposition != null 35 | && contentDisposition.DispositionType.Equals("form-data") 36 | && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) 37 | || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/Options/DropboxStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Dropbox.Api; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Dropbox.Clients; 4 | 5 | namespace ManagedCode.Storage.Dropbox.Options; 6 | 7 | public class DropboxStorageOptions : IStorageOptions 8 | { 9 | public IDropboxClientWrapper? Client { get; set; } 10 | 11 | public DropboxClient? DropboxClient { get; set; } 12 | 13 | /// 14 | /// OAuth2 access token (short-lived or long-lived) used to create a when is not provided. 15 | /// 16 | public string? AccessToken { get; set; } 17 | 18 | /// 19 | /// OAuth2 refresh token used to create a when is not provided. 20 | /// 21 | public string? RefreshToken { get; set; } 22 | 23 | /// 24 | /// Dropbox app key (required when using ). 25 | /// 26 | public string? AppKey { get; set; } 27 | 28 | /// 29 | /// Dropbox app secret (optional when using PKCE refresh tokens). 30 | /// 31 | public string? AppSecret { get; set; } 32 | 33 | public DropboxClientConfig? DropboxClientConfig { get; set; } 34 | 35 | public string RootPath { get; set; } = string.Empty; 36 | 37 | public bool CreateContainerIfNotExists { get; set; } = true; 38 | } 39 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Client/IStorageClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using ManagedCode.Communication; 6 | using ManagedCode.Storage.Core.Models; 7 | 8 | namespace ManagedCode.Storage.Client; 9 | 10 | public interface IStorageClient 11 | { 12 | void SetChunkSize(long size); 13 | 14 | event EventHandler OnProgressStatusChanged; 15 | 16 | Task> UploadFile(string base64, string apiUrl, string contentName, CancellationToken cancellationToken = default); 17 | 18 | Task> UploadFile(byte[] bytes, string apiUrl, string contentName, CancellationToken cancellationToken = default); 19 | 20 | Task> UploadFile(FileInfo fileInfo, string apiUrl, string contentName, CancellationToken cancellationToken = default); 21 | 22 | Task> UploadFile(Stream stream, string apiUrl, string contentName, CancellationToken cancellationToken = default); 23 | 24 | Task> DownloadFile(string fileName, string apiUrl, string? path = null, CancellationToken cancellationToken = default); 25 | 26 | Task> UploadLargeFile(Stream file, string uploadApiUrl, string completeApiUrl, Action? onProgressChanged, 27 | CancellationToken cancellationToken = default); 28 | 29 | Task> GetFileStream(string fileName, string apiUrl, CancellationToken cancellationToken = default); 30 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Google.Cloud.Storage.V1; 2 | using ManagedCode.Storage.Google.Extensions; 3 | using ManagedCode.Storage.Google.Options; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace ManagedCode.Storage.Tests.Storages.GCS; 7 | 8 | public class GCSConfigurator 9 | { 10 | public static ServiceProvider ConfigureServices(string connectionString) 11 | { 12 | var services = new ServiceCollection(); 13 | 14 | services.AddGCPStorageAsDefault(opt => 15 | { 16 | opt.BucketOptions = new BucketOptions 17 | { 18 | ProjectId = "api-project-0000000000000", 19 | Bucket = "managed-code-bucket" 20 | }; 21 | opt.StorageClientBuilder = new StorageClientBuilder 22 | { 23 | UnauthenticatedAccess = true, 24 | BaseUri = connectionString 25 | }; 26 | }); 27 | 28 | services.AddGCPStorage(new GCPStorageOptions 29 | { 30 | BucketOptions = new BucketOptions 31 | { 32 | ProjectId = "api-project-0000000000000", 33 | Bucket = "managed-code-bucket" 34 | }, 35 | StorageClientBuilder = new StorageClientBuilder 36 | { 37 | UnauthenticatedAccess = true, 38 | BaseUri = connectionString 39 | } 40 | }); 41 | return services.BuildServiceProvider(); 42 | } 43 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpConfigurator.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Sftp.Extensions; 2 | using ManagedCode.Storage.Sftp.Options; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ManagedCode.Storage.Tests.Storages.Sftp; 6 | 7 | /// 8 | /// Configures DI for SFTP storage tests. 9 | /// 10 | public static class SftpConfigurator 11 | { 12 | public static ServiceProvider ConfigureServices(string host, int port, string username, string password, string remoteDirectory) 13 | { 14 | var services = new ServiceCollection(); 15 | 16 | services.AddLogging(); 17 | 18 | services.AddSftpStorageAsDefault(opt => 19 | { 20 | opt.Host = host; 21 | opt.Port = port; 22 | opt.Username = username; 23 | opt.Password = password; 24 | opt.RemoteDirectory = remoteDirectory; 25 | opt.CreateContainerIfNotExists = true; 26 | opt.ConnectTimeout = 30000; 27 | opt.OperationTimeout = 30000; 28 | }); 29 | 30 | services.AddSftpStorage(new SftpStorageOptions 31 | { 32 | Host = host, 33 | Port = port, 34 | Username = username, 35 | Password = password, 36 | RemoteDirectory = remoteDirectory, 37 | CreateContainerIfNotExists = true, 38 | ConnectTimeout = 30000, 39 | OperationTimeout = 30000 40 | }); 41 | 42 | return services.BuildServiceProvider(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Common/TestApp/HttpHostProgram.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using ManagedCode.Storage.Azure.Extensions; 3 | using ManagedCode.Storage.Server.Extensions; 4 | using ManagedCode.Storage.Tests.Common.TestApp.Controllers; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http.Features; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace ManagedCode.Storage.Tests.Common.TestApp; 10 | 11 | public class HttpHostProgram 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var options = new WebApplicationOptions 16 | { 17 | Args = args, 18 | ContentRootPath = Directory.GetCurrentDirectory() 19 | }; 20 | var builder = WebApplication.CreateBuilder(options); 21 | 22 | builder.Services.AddControllers(); 23 | builder.Services.AddSignalR(options => 24 | { 25 | options.EnableDetailedErrors = true; 26 | options.MaximumReceiveMessageSize = 8L * 1024 * 1024; // 8 MB 27 | }); 28 | builder.Services.AddEndpointsApiExplorer(); 29 | 30 | // Configure form options for large file uploads 31 | builder.Services.Configure(options => 32 | { 33 | options.ValueLengthLimit = int.MaxValue; 34 | options.MultipartBodyLengthLimit = long.MaxValue; 35 | options.MultipartHeadersLengthLimit = int.MaxValue; 36 | }); 37 | 38 | 39 | var app = builder.Build(); 40 | 41 | app.UseRouting(); 42 | app.MapControllers(); 43 | app.MapStorageHub(); 44 | 45 | app.Run(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Client.SignalR/Models/StorageUploadStreamDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace ManagedCode.Storage.Client.SignalR.Models; 5 | 6 | /// 7 | /// Describes the payload associated with a streaming upload request. 8 | /// 9 | public class StorageUploadStreamDescriptor 10 | { 11 | /// 12 | /// Gets or sets the client-specified transfer identifier. 13 | /// 14 | [JsonPropertyName("transferId")] 15 | public string? TransferId { get; set; } 16 | 17 | /// 18 | /// Gets or sets the file name stored in the backing storage. 19 | /// 20 | [JsonPropertyName("fileName")] 21 | public string FileName { get; set; } = string.Empty; 22 | 23 | /// 24 | /// Gets or sets the optional directory or folder path. 25 | /// 26 | [JsonPropertyName("directory")] 27 | public string? Directory { get; set; } 28 | 29 | /// 30 | /// Gets or sets the MIME type associated with the upload. 31 | /// 32 | [JsonPropertyName("contentType")] 33 | public string? ContentType { get; set; } 34 | 35 | /// 36 | /// Gets or sets the expected file size in bytes. 37 | /// 38 | [JsonPropertyName("fileSize")] 39 | public long? FileSize { get; set; } 40 | 41 | /// 42 | /// Gets or sets optional metadata forwarded to the storage provider. 43 | /// 44 | [JsonPropertyName("metadata")] 45 | public Dictionary? Metadata { get; set; } 46 | } 47 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.FileSystem/FileSystemStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Core.Extensions; 4 | using ManagedCode.Storage.Core.Providers; 5 | using ManagedCode.Storage.FileSystem.Options; 6 | 7 | namespace ManagedCode.Storage.FileSystem 8 | { 9 | public class FileSystemStorageProvider(IServiceProvider serviceProvider, FileSystemStorageOptions defaultOptions) : IStorageProvider 10 | { 11 | public Type StorageOptionsType => typeof(FileSystemStorageOptions); 12 | 13 | public TStorage CreateStorage(TOptions options) 14 | where TStorage : class, IStorage 15 | where TOptions : class, IStorageOptions 16 | { 17 | if (options is not FileSystemStorageOptions azureOptions) 18 | { 19 | throw new ArgumentException($"Options must be of type {typeof(FileSystemStorageOptions)}", nameof(options)); 20 | } 21 | 22 | //var logger = serviceProvider.GetService>(); 23 | var storage = new FileSystemStorage(azureOptions); 24 | 25 | return storage as TStorage 26 | ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 27 | } 28 | 29 | public IStorageOptions GetDefaultOptions() 30 | { 31 | return new FileSystemStorageOptions 32 | { 33 | BaseFolder = defaultOptions.BaseFolder, 34 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/Features/testfakes.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "test fakes, provider doubles, Testcontainers, integration tests, ManagedCode.Storage.TestFakes" 3 | --- 4 | 5 | # Feature: Test Fakes (`ManagedCode.Storage.TestFakes`) 6 | 7 | ## Purpose 8 | 9 | Provide lightweight storage doubles for tests and demos, allowing consumers to replace real provider registrations without provisioning cloud resources. 10 | 11 | These fakes are intended for fast tests where provider-specific behaviour is not the subject under test. 12 | 13 | ## Main Flows 14 | 15 | - Register a real provider (for production wiring). 16 | - Replace it with a fake in tests using DI replacement helpers. 17 | 18 | ```mermaid 19 | flowchart LR 20 | App[App/Test] --> DI[DI container] 21 | DI --> Real[Real provider registration] 22 | DI --> Fake["Replace*() -> Fake provider"] 23 | Fake --> Tests[Fast tests without cloud accounts] 24 | ``` 25 | 26 | ## Components 27 | 28 | - `ManagedCode.Storage.TestFakes/FakeAzureStorage.cs` 29 | - `ManagedCode.Storage.TestFakes/FakeAzureDataLakeStorage.cs` 30 | - `ManagedCode.Storage.TestFakes/FakeAWSStorage.cs` 31 | - `ManagedCode.Storage.TestFakes/FakeGoogleStorage.cs` 32 | - `ManagedCode.Storage.TestFakes/MockCollectionExtensions.cs` (replacement helpers) 33 | 34 | ## Current Behavior 35 | 36 | - Fakes are resolved through `Microsoft.Extensions.DependencyInjection` and implement the same provider interfaces as the real storages. 37 | - Prefer full integration tests (Testcontainers / HTTP fakes) for verifying provider-specific behaviour; use fakes for “consumer wiring” tests. 38 | 39 | ## Tests 40 | 41 | - `Tests/ManagedCode.Storage.Tests/ExtensionsTests/ReplaceExtensionsTests.cs` 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | branches: [ main ] 19 | schedule: 20 | - cron: '0 0 * * 1' 21 | 22 | env: 23 | DOTNET_VERSION: '10.0.x' 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'csharp' ] 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v5 42 | 43 | - name: Setup .NET 44 | uses: actions/setup-dotnet@v4 45 | with: 46 | dotnet-version: ${{ env.DOTNET_VERSION }} 47 | 48 | - name: Initialize CodeQL 49 | uses: github/codeql-action/init@v3 50 | with: 51 | languages: ${{ matrix.language }} 52 | 53 | - name: Restore dependencies 54 | run: dotnet restore ManagedCode.Storage.slnx 55 | 56 | - name: Build 57 | run: dotnet build ManagedCode.Storage.slnx --no-restore 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v3 61 | -------------------------------------------------------------------------------- /docs/Features/integration-signalr-client.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "SignalR client, streaming, upload, download, progress, StorageSignalRClient, ManagedCode.Storage.Client.SignalR" 3 | --- 4 | 5 | # Feature: .NET SignalR Client (`ManagedCode.Storage.Client.SignalR`) 6 | 7 | ## Purpose 8 | 9 | Typed .NET SignalR client (`StorageSignalRClient`) for `StorageHub`: streaming upload/download helpers plus progress reporting and reconnection support. 10 | 11 | - streaming upload/download helpers 12 | - progress reporting and reconnection support via `StorageSignalRClientOptions` 13 | 14 | ## Main Flows 15 | 16 | ```mermaid 17 | sequenceDiagram 18 | participant App as App 19 | participant Client as StorageSignalRClient 20 | participant Hub as StorageHub 21 | participant S as IStorage 22 | App->>Client: UploadAsync(stream, descriptor) 23 | Client->>Hub: SignalR stream upload 24 | Hub->>S: UploadAsync(...) 25 | S-->>Hub: Result 26 | Hub-->>Client: status/progress 27 | ``` 28 | 29 | ## Components 30 | 31 | - `Integraions/ManagedCode.Storage.Client.SignalR/StorageSignalRClient.cs` 32 | - `Integraions/ManagedCode.Storage.Client.SignalR/StorageSignalRClientOptions.cs` 33 | - `Integraions/ManagedCode.Storage.Client.SignalR/StorageSignalREventNames.cs` 34 | - `Integraions/ManagedCode.Storage.Client.SignalR/Models/*` 35 | 36 | ## Current Behavior 37 | 38 | - The client is transport-agnostic as long as it can connect to the server hub URL. 39 | - Progress updates are exposed via strongly-typed status models. 40 | 41 | ## Tests 42 | 43 | - `Tests/ManagedCode.Storage.Tests/AspNetTests/Abstracts/BaseSignalRStorageTests.cs` 44 | - `Tests/ManagedCode.Storage.Tests/AspNetTests/Azure/AzureSignalRStorageTests.cs` 45 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/DependencyInjection/StorageServerBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Server.ChunkUpload; 3 | using ManagedCode.Storage.Server.Controllers; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace ManagedCode.Storage.Server.Extensions.DependencyInjection; 8 | 9 | /// 10 | /// Provides helpers for wiring storage server components into an . 11 | /// 12 | public static class StorageServerBuilderExtensions 13 | { 14 | /// 15 | /// Registers server-side services required for HTTP controllers and chunk uploads. 16 | /// 17 | /// The service collection. 18 | /// Optional configuration for . 19 | /// Optional configuration for . 20 | /// The original for chaining. 21 | public static IServiceCollection AddStorageServer(this IServiceCollection services, Action? configureServer = null, Action? configureChunks = null) 22 | { 23 | var serverOptions = new StorageServerOptions(); 24 | configureServer?.Invoke(serverOptions); 25 | services.AddSingleton(serverOptions); 26 | 27 | services.Configure(options => 28 | { 29 | options.SuppressModelStateInvalidFilter = true; 30 | }); 31 | 32 | services.AddChunkUploadHandling(configureChunks); 33 | return services; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/ExtensionsTests/ReplaceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Shouldly; 3 | using ManagedCode.Storage.Azure; 4 | using ManagedCode.Storage.Azure.Extensions; 5 | using ManagedCode.Storage.Azure.Options; 6 | using ManagedCode.Storage.Core; 7 | using ManagedCode.Storage.TestFakes; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Xunit; 10 | 11 | namespace ManagedCode.Storage.Tests.ExtensionsTests; 12 | 13 | public class ReplaceExtensionsTests 14 | { 15 | [Fact] 16 | public void ReplaceAzureStorageAsDefault() 17 | { 18 | var options = new AzureStorageOptions 19 | { 20 | Container = "test", 21 | ConnectionString = "ConnectionString" 22 | }; 23 | 24 | var services = new ServiceCollection(); 25 | 26 | services.AddAzureStorageAsDefault(options); 27 | 28 | services.ReplaceAzureStorageAsDefault(); 29 | 30 | var build = services.BuildServiceProvider(); 31 | build.GetService() 32 | !.GetType() 33 | .ShouldBe(typeof(FakeAzureStorage)); 34 | } 35 | 36 | [Fact] 37 | public void ReplaceAzureStorage() 38 | { 39 | var options = new AzureStorageOptions 40 | { 41 | Container = "test", 42 | ConnectionString = "ConnectionString" 43 | }; 44 | 45 | var services = new ServiceCollection(); 46 | 47 | services.AddAzureStorage(options); 48 | 49 | services.ReplaceAzureStorage(); 50 | 51 | var build = services.BuildServiceProvider(); 52 | build.GetService() 53 | !.GetType() 54 | .ShouldBe(typeof(FakeAzureStorage)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.GoogleDrive/GoogleDriveStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Core.Extensions; 4 | using ManagedCode.Storage.Core.Providers; 5 | using ManagedCode.Storage.GoogleDrive.Options; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ManagedCode.Storage.GoogleDrive; 9 | 10 | public class GoogleDriveStorageProvider(IServiceProvider serviceProvider, GoogleDriveStorageOptions defaultOptions) : IStorageProvider 11 | { 12 | public Type StorageOptionsType => typeof(GoogleDriveStorageOptions); 13 | 14 | public TStorage CreateStorage(TOptions options) 15 | where TStorage : class, IStorage 16 | where TOptions : class, IStorageOptions 17 | { 18 | if (options is not GoogleDriveStorageOptions driveOptions) 19 | { 20 | throw new ArgumentException($"Options must be of type {typeof(GoogleDriveStorageOptions)}", nameof(options)); 21 | } 22 | 23 | var logger = serviceProvider.GetService(typeof(ILogger)) as ILogger; 24 | var storage = new GoogleDriveStorage(driveOptions, logger); 25 | return storage as TStorage ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 26 | } 27 | 28 | public IStorageOptions GetDefaultOptions() 29 | { 30 | return new GoogleDriveStorageOptions 31 | { 32 | RootFolderId = defaultOptions.RootFolderId, 33 | DriveService = defaultOptions.DriveService, 34 | Client = defaultOptions.Client, 35 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.OneDrive/OneDriveStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Core.Extensions; 4 | using ManagedCode.Storage.Core.Providers; 5 | using ManagedCode.Storage.OneDrive.Options; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ManagedCode.Storage.OneDrive; 9 | 10 | public class OneDriveStorageProvider(IServiceProvider serviceProvider, OneDriveStorageOptions defaultOptions) : IStorageProvider 11 | { 12 | public Type StorageOptionsType => typeof(OneDriveStorageOptions); 13 | 14 | public TStorage CreateStorage(TOptions options) 15 | where TStorage : class, IStorage 16 | where TOptions : class, IStorageOptions 17 | { 18 | if (options is not OneDriveStorageOptions driveOptions) 19 | { 20 | throw new ArgumentException($"Options must be of type {typeof(OneDriveStorageOptions)}", nameof(options)); 21 | } 22 | 23 | var logger = serviceProvider.GetService(typeof(ILogger)) as ILogger; 24 | var storage = new OneDriveStorage(driveOptions, logger); 25 | 26 | return storage as TStorage ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 27 | } 28 | 29 | public IStorageOptions GetDefaultOptions() 30 | { 31 | return new OneDriveStorageOptions 32 | { 33 | DriveId = defaultOptions.DriveId, 34 | RootPath = defaultOptions.RootPath, 35 | GraphClient = defaultOptions.GraphClient, 36 | Client = defaultOptions.Client, 37 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Shouldly; 3 | using ManagedCode.Storage.Azure; 4 | using ManagedCode.Storage.Azure.Extensions; 5 | using ManagedCode.Storage.Core; 6 | using ManagedCode.Storage.Core.Exceptions; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Xunit; 9 | 10 | namespace ManagedCode.Storage.Tests.Storages.Azure; 11 | 12 | public class AzureConfigTests 13 | { 14 | [Fact] 15 | public void BadConfigurationForStorage_WithoutContainer_ThrowException() 16 | { 17 | var services = new ServiceCollection(); 18 | 19 | Action action = () => services.AddAzureStorage(opt => { opt.ConnectionString = "test"; }); 20 | 21 | Should.Throw(action); 22 | } 23 | 24 | [Fact] 25 | public void BadConfigurationForStorage_WithoutConnectionString_ThrowException() 26 | { 27 | var services = new ServiceCollection(); 28 | 29 | Action action = () => services.AddAzureStorageAsDefault(options => 30 | { 31 | options.Container = "managed-code-bucket"; 32 | options.ConnectionString = null; 33 | }); 34 | 35 | Should.Throw(action); 36 | } 37 | 38 | [Fact] 39 | public void StorageAsDefaultTest() 40 | { 41 | var connectionString = "UseDevelopmentStorage=true"; 42 | var storage = AzureConfigurator.ConfigureServices(connectionString) 43 | .GetService(); 44 | var defaultStorage = AzureConfigurator.ConfigureServices(connectionString) 45 | .GetService(); 46 | storage?.GetType() 47 | .FullName 48 | .ShouldBe(defaultStorage?.GetType() 49 | .FullName); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/Storage/StorageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Communication; 5 | using ManagedCode.MimeTypes; 6 | using ManagedCode.Storage.Core; 7 | using ManagedCode.Storage.Core.Models; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace ManagedCode.Storage.Server.Extensions.Storage; 11 | 12 | public static class StorageExtensions 13 | { 14 | public static async Task> DownloadAsFileResult(this IStorage storage, string blobName, 15 | CancellationToken cancellationToken = default) 16 | { 17 | var result = await storage.DownloadAsync(blobName, cancellationToken); 18 | 19 | if (result.IsFailed) 20 | return Result.Fail(result.Problem!); 21 | 22 | var fileStream = new FileStreamResult(result.Value!.FileStream, MimeHelper.GetMimeType(result.Value.FileInfo.Extension)) 23 | { 24 | FileDownloadName = result.Value.Name 25 | }; 26 | 27 | return Result.Succeed(fileStream); 28 | } 29 | 30 | public static async Task> DownloadAsFileResult(this IStorage storage, BlobMetadata blobMetadata, 31 | CancellationToken cancellationToken = default) 32 | { 33 | var result = await storage.DownloadAsync(blobMetadata.Name, cancellationToken); 34 | 35 | if (result.IsFailed) 36 | return Result.Fail(result.Problem!); 37 | 38 | var fileStream = new FileStreamResult(result.Value!.FileStream, MimeHelper.GetMimeType(result.Value.FileInfo.Extension)) 39 | { 40 | FileDownloadName = result.Value.Name 41 | }; 42 | 43 | return Result.Succeed(fileStream); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/Development/setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup 3 | description: "How to clone, build, and run tests for ManagedCode.Storage." 4 | keywords: "ManagedCode.Storage setup, .NET 10, dotnet restore, dotnet build, dotnet test, Docker, Testcontainers, Azurite, LocalStack, FakeGcsServer, SFTP" 5 | permalink: /setup/ 6 | nav_order: 2 7 | --- 8 | 9 | # Development Setup 10 | 11 | ## Prerequisites 12 | 13 | - .NET SDK: **.NET 10** (`10.0.x`) 14 | - Docker: required for Testcontainers-backed integration tests (Azurite / LocalStack / FakeGcsServer / SFTP) 15 | 16 | ## Workflow (Local) 17 | 18 | ```mermaid 19 | flowchart LR 20 | A[Clone repo] --> B[dotnet restore] 21 | B --> C[dotnet build] 22 | C --> D[dotnet test] 23 | D --> E[dotnet format] 24 | D --> F[Docker daemon] 25 | ``` 26 | 27 | ## Clone 28 | 29 | ```bash 30 | git clone https://github.com/managedcode/Storage.git 31 | cd Storage 32 | ``` 33 | 34 | ## Restore / Build / Test 35 | 36 | Canonical commands (see `AGENTS.md`): 37 | 38 | ```bash 39 | dotnet restore ManagedCode.Storage.slnx 40 | dotnet build ManagedCode.Storage.slnx --configuration Release 41 | dotnet test Tests/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj --configuration Release 42 | ``` 43 | 44 | ## Testing Strategy 45 | 46 | The full test strategy (suite layout, categories, containers, cloud-drive HTTP fakes) lives in `docs/Testing/strategy.md`: 47 | 48 | - [Testing Strategy](../Testing/strategy.md) 49 | 50 | ## Formatting 51 | 52 | ```bash 53 | dotnet format ManagedCode.Storage.slnx 54 | ``` 55 | 56 | ## Notes 57 | 58 | - Start Docker Desktop (or your Docker daemon) before running the full test suite. 59 | - Never commit secrets (cloud keys, OAuth tokens, connection strings). Use environment variables or user secrets. 60 | - Credentials for cloud-drive providers are documented in `docs/Development/credentials.md`. 61 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemUnicodeSanitizerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Storage.FileSystem; 5 | using ManagedCode.Storage.FileSystem.Options; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | namespace ManagedCode.Storage.Tests.Storages.FileSystem; 10 | 11 | public class FileSystemUnicodeSanitizerTests 12 | { 13 | [Fact] 14 | public async Task ShouldResolveExistingUnicodeFileByPathOnly() 15 | { 16 | var root = Path.Combine(Environment.CurrentDirectory, "managedcode-vfs-existing", Guid.NewGuid().ToString("N")); 17 | var directory = Path.Combine(root, "international", "Українська-папка"); 18 | Directory.CreateDirectory(directory); 19 | 20 | var expectedFilePath = Path.Combine(directory, "лист-привіт.txt"); 21 | await File.WriteAllTextAsync(expectedFilePath, "Привіт"); 22 | 23 | var storage = new FileSystemStorage(new FileSystemStorageOptions 24 | { 25 | BaseFolder = root, 26 | CreateContainerIfNotExists = true 27 | }); 28 | 29 | try 30 | { 31 | var exists = await storage.ExistsAsync("international/Українська-папка/лист-привіт.txt"); 32 | exists.IsSuccess.ShouldBeTrue(); 33 | exists.Value.ShouldBeTrue(); 34 | 35 | var metadata = await storage.GetBlobMetadataAsync("international/Українська-папка/лист-привіт.txt"); 36 | metadata.IsSuccess.ShouldBeTrue(metadata.Problem?.ToString()); 37 | metadata.Value!.FullName.ShouldBe("international/Українська-папка/лист-привіт.txt"); 38 | } 39 | finally 40 | { 41 | if (Directory.Exists(root)) 42 | { 43 | Directory.Delete(root, recursive: true); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ManagedCode.Storage.Core/StringStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace ManagedCode.Storage.Core 5 | { 6 | public class StringStream(string str) : Stream 7 | { 8 | private readonly string _string = str ?? throw new ArgumentNullException(nameof(str)); 9 | 10 | public override bool CanRead => true; 11 | public override bool CanSeek => true; 12 | public override bool CanWrite => false; 13 | public override long Length => _string.Length * 2; 14 | public override long Position { get; set; } 15 | 16 | private byte this[int i] => (byte)(_string[i / 2] >> ((i & 1) * 8)); 17 | 18 | public override long Seek(long offset, SeekOrigin origin) 19 | { 20 | Position = origin switch 21 | { 22 | SeekOrigin.Begin => offset, 23 | SeekOrigin.Current => Position + offset, 24 | SeekOrigin.End => Length + offset, 25 | _ => throw new ArgumentOutOfRangeException(nameof(origin), origin, null) 26 | }; 27 | 28 | return Position; 29 | } 30 | 31 | public override int Read(byte[] buffer, int offset, int count) 32 | { 33 | var length = Math.Min(count, Length - Position); 34 | for (var i = 0; i < length; i++) 35 | buffer[offset + i] = this[(int)Position++]; 36 | 37 | return (int)length; 38 | } 39 | 40 | public override int ReadByte() => Position < Length ? this[(int)Position++] : -1; 41 | 42 | public override void Flush() { } 43 | 44 | public override void SetLength(long value) => throw new NotSupportedException(); 45 | 46 | public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); 47 | 48 | public override string ToString() => _string; 49 | } 50 | } -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Models/TransferStatus.cs: -------------------------------------------------------------------------------- 1 | using ManagedCode.Storage.Core.Models; 2 | 3 | namespace ManagedCode.Storage.Server.Models; 4 | 5 | /// 6 | /// Represents the status of a streaming transfer processed by the storage hub. 7 | /// 8 | public class TransferStatus 9 | { 10 | /// 11 | /// Gets or sets the unique identifier associated with the transfer. 12 | /// 13 | public string TransferId { get; init; } = string.Empty; 14 | 15 | /// 16 | /// Gets or sets the operation type (e.g. upload, download). 17 | /// 18 | public string Operation { get; init; } = string.Empty; 19 | 20 | /// 21 | /// Gets or sets the logical resource name involved in the transfer. 22 | /// 23 | public string? ResourceName { get; init; } 24 | 25 | /// 26 | /// Gets or sets the cumulative number of bytes processed. 27 | /// 28 | public long BytesTransferred { get; set; } 29 | 30 | /// 31 | /// Gets or sets the total number of bytes expected, when known. 32 | /// 33 | public long? TotalBytes { get; set; } 34 | 35 | /// 36 | /// Gets or sets a value indicating whether the transfer completed successfully. 37 | /// 38 | public bool IsCompleted { get; set; } 39 | 40 | /// 41 | /// Gets or sets a value indicating whether the transfer was canceled. 42 | /// 43 | public bool IsCanceled { get; set; } 44 | 45 | /// 46 | /// Gets or sets error details when the transfer fails. 47 | /// 48 | public string? Error { get; set; } 49 | 50 | /// 51 | /// Gets or sets the metadata returned by the storage provider after upload. 52 | /// 53 | public BlobMetadata? Metadata { get; set; } 54 | } 55 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Azure.DataLake/AzureDataLakeStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Azure.DataLake.Options; 3 | using ManagedCode.Storage.Core; 4 | using ManagedCode.Storage.Core.Extensions; 5 | using ManagedCode.Storage.Core.Providers; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace ManagedCode.Storage.Azure.DataLake 10 | { 11 | public class AzureDataLakeStorageProvider(IServiceProvider serviceProvider, AzureDataLakeStorageOptions defaultOptions) : IStorageProvider 12 | { 13 | public Type StorageOptionsType => typeof(AzureDataLakeStorageOptions); 14 | 15 | public TStorage CreateStorage(TOptions options) 16 | where TStorage : class, IStorage 17 | where TOptions : class, IStorageOptions 18 | { 19 | if (options is not AzureDataLakeStorageOptions azureOptions) 20 | { 21 | throw new ArgumentException($"Options must be of type {typeof(AzureDataLakeStorageOptions)}", nameof(options)); 22 | } 23 | 24 | var logger = serviceProvider.GetService>(); 25 | var storage = new AzureDataLakeStorage(azureOptions, logger); 26 | 27 | return storage as TStorage 28 | ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 29 | } 30 | 31 | public IStorageOptions GetDefaultOptions() 32 | { 33 | return new AzureDataLakeStorageOptions() 34 | { 35 | ConnectionString = defaultOptions.ConnectionString, 36 | FileSystem = defaultOptions.FileSystem, 37 | PublicAccessType = defaultOptions.PublicAccessType, 38 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 39 | }; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /docs/Features/provider-azure-datalake.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "Azure Data Lake Gen2, ADLS, ManagedCode.Storage.Azure.DataLake, IStorage, filesystem, directory, .NET" 3 | --- 4 | 5 | # Feature: Azure Data Lake Gen2 Provider (`ManagedCode.Storage.Azure.DataLake`) 6 | 7 | ## Purpose 8 | 9 | Implement `IStorage` on top of **Azure Data Lake Storage Gen2**. 10 | 11 | ## Main Flows 12 | 13 | ```mermaid 14 | flowchart LR 15 | App --> ADL[AzureDataLakeStorage : IAzureDataLakeStorage] 16 | ADL --> SDK[Azure.Storage.Files.DataLake] 17 | SDK --> ADLS[(ADLS Gen2)] 18 | ``` 19 | 20 | ## Components 21 | 22 | - `Storages/ManagedCode.Storage.Azure.DataLake/AzureDataLakeStorage.cs` 23 | - `Storages/ManagedCode.Storage.Azure.DataLake/AzureDataLakeStorageProvider.cs` 24 | - DI: 25 | - `Storages/ManagedCode.Storage.Azure.DataLake/Extensions/ServiceCollectionExtensions.cs` 26 | - `Storages/ManagedCode.Storage.Azure.DataLake/Extensions/StorageFactoryExtensions.cs` 27 | - Options: 28 | - `Storages/ManagedCode.Storage.Azure.DataLake/Options/AzureDataLakeStorageOptions.cs` 29 | - `Storages/ManagedCode.Storage.Azure.DataLake/Options/OpenReadStreamOptions.cs` 30 | - `Storages/ManagedCode.Storage.Azure.DataLake/Options/OpenWriteStreamOptions.cs` 31 | 32 | ## DI Wiring 33 | 34 | ```bash 35 | dotnet add package ManagedCode.Storage.Azure.DataLake 36 | ``` 37 | 38 | ```csharp 39 | using ManagedCode.Storage.Azure.DataLake.Extensions; 40 | 41 | builder.Services.AddAzureDataLakeStorageAsDefault(options => 42 | { 43 | options.FileSystem = "my-filesystem"; 44 | options.ConnectionString = configuration["AzureDataLake:ConnectionString"]!; 45 | }); 46 | ``` 47 | 48 | ## Current Behavior 49 | 50 | - Uses `ConnectionString` + `FileSystem` as the “container” equivalent. 51 | - Creates the filesystem automatically when `CreateContainerIfNotExists = true`. 52 | 53 | ## Tests 54 | 55 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureDataLakeTests.cs` 56 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Dropbox/DropboxStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Core.Extensions; 4 | using ManagedCode.Storage.Core.Providers; 5 | using ManagedCode.Storage.Dropbox.Options; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ManagedCode.Storage.Dropbox; 9 | 10 | public class DropboxStorageProvider(IServiceProvider serviceProvider, DropboxStorageOptions defaultOptions) : IStorageProvider 11 | { 12 | public Type StorageOptionsType => typeof(DropboxStorageOptions); 13 | 14 | public TStorage CreateStorage(TOptions options) 15 | where TStorage : class, IStorage 16 | where TOptions : class, IStorageOptions 17 | { 18 | if (options is not DropboxStorageOptions dropboxOptions) 19 | { 20 | throw new ArgumentException($"Options must be of type {typeof(DropboxStorageOptions)}", nameof(options)); 21 | } 22 | 23 | var logger = serviceProvider.GetService(typeof(ILogger)) as ILogger; 24 | var storage = new DropboxStorage(dropboxOptions, logger); 25 | return storage as TStorage ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 26 | } 27 | 28 | public IStorageOptions GetDefaultOptions() 29 | { 30 | return new DropboxStorageOptions 31 | { 32 | RootPath = defaultOptions.RootPath, 33 | DropboxClient = defaultOptions.DropboxClient, 34 | Client = defaultOptions.Client, 35 | AccessToken = defaultOptions.AccessToken, 36 | RefreshToken = defaultOptions.RefreshToken, 37 | AppKey = defaultOptions.AppKey, 38 | AppSecret = defaultOptions.AppSecret, 39 | DropboxClientConfig = defaultOptions.DropboxClientConfig, 40 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Aws/Options/AWSStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.S3; 2 | using ManagedCode.Storage.Core; 3 | 4 | namespace ManagedCode.Storage.Aws.Options; 5 | 6 | /// 7 | /// Configuration options for AWS S3 storage. 8 | /// 9 | public class AWSStorageOptions : IStorageOptions 10 | { 11 | /// 12 | /// The public key to access the AWS S3 storage bucket. 13 | /// 14 | public string? PublicKey { get; set; } 15 | 16 | /// 17 | /// The secret key to access the AWS S3 storage bucket. 18 | /// 19 | public string? SecretKey { get; set; } 20 | 21 | /// 22 | /// The name of the IAM role. 23 | /// 24 | /// 25 | /// If this is set, the and will be ignored. 26 | /// Note that this can only be used when running on an EC2 instance. 27 | /// 28 | public string? RoleName { get; set; } 29 | 30 | /// 31 | /// The name of the bucket to use. 32 | /// 33 | public string? Bucket { get; set; } 34 | 35 | /// 36 | /// The underlying Amazon S3 configuration. 37 | /// 38 | public AmazonS3Config? OriginalOptions { get; set; } = new(); 39 | 40 | /// 41 | /// Whether to create the container if it does not exist. Default is true. 42 | /// 43 | public bool CreateContainerIfNotExists { get; set; } = true; 44 | 45 | /// 46 | /// Whether to use the instance profile credentials. Default is false. 47 | /// 48 | /// 49 | /// If this is set to true, the and will be ignored. 50 | /// Note that this can only be used when running on an EC2 instance. 51 | /// 52 | public bool UseInstanceProfileCredentials { get; set; } = false; 53 | } -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Core/Crc32HelperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Shouldly; 4 | using ManagedCode.Communication; 5 | using ManagedCode.Storage.Core.Helpers; 6 | using Xunit; 7 | 8 | namespace ManagedCode.Storage.Tests.Core; 9 | 10 | public class Crc32HelperTests 11 | { 12 | [Fact] 13 | public void CalculateFileCrc_ShouldMatchInMemoryCalculation() 14 | { 15 | var tempPath = Path.Combine(Environment.CurrentDirectory, $"crc-test-{Guid.NewGuid():N}.bin"); 16 | try 17 | { 18 | var payload = new byte[4096 + 123]; 19 | new Random(17).NextBytes(payload); 20 | File.WriteAllBytes(tempPath, payload); 21 | 22 | var fileCrc = Crc32Helper.CalculateFileCrc(tempPath); 23 | var inMemory = Crc32Helper.Calculate(payload); 24 | 25 | fileCrc.ShouldBe(inMemory); 26 | } 27 | finally 28 | { 29 | if (File.Exists(tempPath)) 30 | { 31 | File.Delete(tempPath); 32 | } 33 | } 34 | } 35 | 36 | [Fact] 37 | public void CalculateFileCrc_ForSparseGeneratedFile_ShouldBeNonZero() 38 | { 39 | using var localFile = ManagedCode.Storage.Core.Models.LocalFile.FromRandomNameWithExtension(".bin"); 40 | ManagedCode.Storage.Tests.Common.FileHelper.GenerateLocalFile(localFile, 50); 41 | var crc = Crc32Helper.CalculateFileCrc(localFile.FilePath); 42 | crc.ShouldBeGreaterThan(0U); 43 | } 44 | 45 | [Fact] 46 | public void ResultSucceed_ShouldCarryValue() 47 | { 48 | var result = ManagedCode.Communication.Result.Succeed(123u); 49 | result.IsSuccess.ShouldBeTrue(); 50 | result.Value.ShouldBe(123u); 51 | } 52 | 53 | [Fact] 54 | public void Calculate_ForZeroBytes_ShouldNotBeZero() 55 | { 56 | var bytes = new byte[51]; 57 | var crc = Crc32Helper.Calculate(bytes); 58 | crc.ShouldNotBe(0u); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ManagedCode.Storage.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Aws/AWSStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Aws.Options; 3 | using ManagedCode.Storage.Core; 4 | using ManagedCode.Storage.Core.Extensions; 5 | using ManagedCode.Storage.Core.Providers; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace ManagedCode.Storage.Aws 10 | { 11 | public class AWSStorageProvider(IServiceProvider serviceProvider, AWSStorageOptions defaultOptions) : IStorageProvider 12 | { 13 | public Type StorageOptionsType => typeof(AWSStorageOptions); 14 | 15 | public TStorage CreateStorage(TOptions options) 16 | where TStorage : class, IStorage 17 | where TOptions : class, IStorageOptions 18 | { 19 | if (options is not AWSStorageOptions azureOptions) 20 | { 21 | throw new ArgumentException($"Options must be of type {typeof(AWSStorageOptions)}", nameof(options)); 22 | } 23 | 24 | var logger = serviceProvider.GetService>(); 25 | var storage = new AWSStorage(azureOptions, logger); 26 | 27 | return storage as TStorage 28 | ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 29 | } 30 | 31 | public IStorageOptions GetDefaultOptions() 32 | { 33 | return new AWSStorageOptions() 34 | { 35 | PublicKey = defaultOptions.PublicKey, 36 | SecretKey = defaultOptions.SecretKey, 37 | RoleName = defaultOptions.RoleName, 38 | Bucket = defaultOptions.Bucket, 39 | OriginalOptions = defaultOptions.OriginalOptions, 40 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists, 41 | UseInstanceProfileCredentials = defaultOptions.UseInstanceProfileCredentials 42 | }; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/Features/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features 3 | description: "Documentation for major modules and providers in ManagedCode.Storage." 4 | keywords: "IStorage, providers, Azure Blob, AWS S3, Google Cloud Storage, OneDrive, Google Drive, Dropbox, CloudKit, FileSystem, SFTP, Virtual File System, ASP.NET Server, SignalR" 5 | permalink: /features/ 6 | nav_order: 5 7 | --- 8 | 9 | # Features 10 | 11 | This folder documents the major modules in the repository. 12 | 13 | > Note: the GitHub Pages docs generator publishes every `docs/Features/*.md` page automatically, but to make a page “visible” in the catalog/navigation you should also link it from this index. 14 | 15 | ```mermaid 16 | flowchart LR 17 | App[Application code] --> Core["IStorage (Core)"] 18 | Core --> Providers[Providers] 19 | Core --> VFS[Virtual File System] 20 | App --> Server[ASP.NET Server] 21 | App --> Clients[Client SDKs] 22 | Server --> Core 23 | Clients --> Server 24 | ``` 25 | 26 | ## Core 27 | 28 | - [Storage core abstraction](storage-core.md) 29 | - [Dependency injection & keyed registrations](dependency-injection.md) 30 | - [Virtual File System (VFS)](virtual-file-system.md) 31 | - [MIME & integrity helpers (MimeHelper + CRC32)](mime-and-crc.md) 32 | - [Test fakes](testfakes.md) 33 | 34 | ## Integrations 35 | 36 | - [ASP.NET server (controllers + SignalR)](integration-aspnet-server.md) 37 | - [.NET HTTP client](integration-dotnet-client.md) 38 | - [.NET SignalR client](integration-signalr-client.md) 39 | - [Chunked uploads (HTTP + client)](chunked-uploads.md) 40 | 41 | ## Providers 42 | 43 | - [Azure Blob](provider-azure-blob.md) 44 | - [Azure Data Lake Gen2](provider-azure-datalake.md) 45 | - [Amazon S3](provider-aws-s3.md) 46 | - [Google Cloud Storage](provider-google-cloud-storage.md) 47 | - [File system](provider-filesystem.md) 48 | - [SFTP](provider-sftp.md) 49 | - [OneDrive (Microsoft Graph)](provider-onedrive.md) 50 | - [Google Drive](provider-googledrive.md) 51 | - [Dropbox](provider-dropbox.md) 52 | - [CloudKit (iCloud app data)](provider-cloudkit.md) 53 | -------------------------------------------------------------------------------- /docs/Features/provider-sftp.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "SFTP storage, SSH.NET, ManagedCode.Storage.Sftp, IStorage, upload, download, .NET" 3 | --- 4 | 5 | # Feature: SFTP Provider (`ManagedCode.Storage.Sftp`) 6 | 7 | ## Purpose 8 | 9 | Implement `IStorage` on top of SFTP using SSH (for legacy systems and air-gapped environments). 10 | 11 | ## Main Flows 12 | 13 | ```mermaid 14 | flowchart LR 15 | App --> S[SftpStorage : ISftpStorage] 16 | S --> SSH[SSH.NET] 17 | SSH --> Server[(SFTP Server)] 18 | ``` 19 | 20 | ## Components 21 | 22 | - `Storages/ManagedCode.Storage.Sftp/SftpStorage.cs` 23 | - `Storages/ManagedCode.Storage.Sftp/SftpStorageProvider.cs` 24 | - DI: 25 | - `Storages/ManagedCode.Storage.Sftp/Extensions/ServiceCollectionExtensions.cs` 26 | - `Storages/ManagedCode.Storage.Sftp/Extensions/StorageFactoryExtensions.cs` 27 | - Options: 28 | - `Storages/ManagedCode.Storage.Sftp/Options/SftpStorageOptions.cs` 29 | 30 | ## DI Wiring 31 | 32 | ```bash 33 | dotnet add package ManagedCode.Storage.Sftp 34 | ``` 35 | 36 | ```csharp 37 | using ManagedCode.Storage.Sftp.Extensions; 38 | 39 | builder.Services.AddSftpStorageAsDefault(options => 40 | { 41 | options.Host = "sftp.example.com"; 42 | options.Username = configuration["Sftp:Username"]; 43 | options.Password = configuration["Sftp:Password"]; 44 | options.RemoteDirectory = "/uploads"; 45 | }); 46 | ``` 47 | 48 | ## Current Behavior 49 | 50 | - Supports password and key-based auth (`PrivateKeyPath` / `PrivateKeyContent`). 51 | - `AcceptAnyHostKey` exists for dev/test only; production should use `HostKeyFingerprint`. 52 | - Can create directories automatically (`CreateDirectoryIfNotExists`). 53 | 54 | ## Tests 55 | 56 | - `Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpUploadTests.cs` 57 | - `Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpDownloadTests.cs` 58 | - `Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpBlobTests.cs` 59 | - `Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpStreamTests.cs` 60 | - `Tests/ManagedCode.Storage.Tests/Storages/Sftp/SftpContainerTests.cs` 61 | -------------------------------------------------------------------------------- /docs/Features/integration-dotnet-client.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: ".NET HTTP client, StorageClient, upload, download, chunked upload, CRC32, ManagedCode.Storage.Client" 3 | --- 4 | 5 | # Feature: .NET HTTP Client (`ManagedCode.Storage.Client`) 6 | 7 | ## Purpose 8 | 9 | Typed .NET HTTP client for `ManagedCode.Storage.Server` endpoints: multipart uploads, downloads to `LocalFile`, and chunked uploads with progress + CRC32. 10 | 11 | - multipart uploads 12 | - downloads to `LocalFile` 13 | - chunked uploads with progress + CRC32 14 | 15 | ## Main Flows 16 | 17 | ### Chunked upload with CRC 18 | 19 | ```mermaid 20 | flowchart TD 21 | A[Stream/File] --> B[StorageClient] 22 | B --> C[Split into chunks] 23 | C --> D[POST /chunks/upload] 24 | D --> E[POST /chunks/complete] 25 | E --> F[Result + CRC] 26 | ``` 27 | 28 | ## Quickstart 29 | 30 | ```bash 31 | dotnet add package ManagedCode.Storage.Client 32 | ``` 33 | 34 | ```csharp 35 | using ManagedCode.Storage.Client; 36 | 37 | var http = new HttpClient { BaseAddress = new Uri("https://my-api.example") }; 38 | var client = new StorageClient(http); 39 | client.SetChunkSize(5 * 1024 * 1024); // 5 MB 40 | 41 | await using var stream = File.OpenRead("video.mp4"); 42 | var result = await client.UploadLargeFile( 43 | stream, 44 | uploadApiUrl: "/api/storage/upload-chunks/upload", 45 | completeApiUrl: "/api/storage/upload-chunks/complete", 46 | onProgressChanged: percent => Console.WriteLine($"{percent:F1}%")); 47 | ``` 48 | 49 | ## Components 50 | 51 | - `Integraions/ManagedCode.Storage.Client/IStorageClient.cs` 52 | - `Integraions/ManagedCode.Storage.Client/StorageClient.cs` 53 | - `Integraions/ManagedCode.Storage.Client/ProgressStatus.cs` 54 | 55 | ## Current Behavior 56 | 57 | - `StorageClient.ChunkSize` must be set before `UploadLargeFile(...)`. 58 | - CRC is computed during upload using `ManagedCode.Storage.Core.Helpers.Crc32Helper`. 59 | - MIME type is resolved via `MimeHelper` based on file name. 60 | 61 | ## Tests 62 | 63 | - `Tests/ManagedCode.Storage.Tests/Core/StorageClientChunkTests.cs` 64 | -------------------------------------------------------------------------------- /github-pages/site-map.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Sitemap 4 | description: A complete index of all documentation pages on this site. 5 | keywords: Storage sitemap, documentation index, features, ADR, API, setup, credentials, testing 6 | permalink: /sitemap/ 7 | nav_order: 99 8 | --- 9 | 10 | {% assign effective_baseurl = site.baseurl %} 11 | {% if site.url != nil and site.url != '' %} 12 | {% unless site.url contains 'github.io' %} 13 | {% assign effective_baseurl = '' %} 14 | {% endunless %} 15 | {% endif %} 16 | 17 | # Sitemap 18 | 19 | This page lists the docs sections and every generated page (Features, ADRs, API) so it’s always easy to find content. 20 | 21 | ## Main pages 22 | 23 | - [Home]({{ effective_baseurl }}/) 24 | - [Setup]({{ effective_baseurl }}/setup/) 25 | - [Credentials]({{ effective_baseurl }}/credentials/) 26 | - [Testing]({{ effective_baseurl }}/testing/) 27 | - [Features]({{ effective_baseurl }}/features/) 28 | - [ADR]({{ effective_baseurl }}/adr/) 29 | - [API]({{ effective_baseurl }}/api/) 30 | - [GitHub](https://github.com/{{ site.github_repo }}) 31 | 32 | ## Features 33 | 34 | {% assign feature_pages = site.pages | where_exp: "p", "p.url contains '/features/'" | sort: "title" %} 35 | {% for p in feature_pages %} 36 | {% if p.url contains '.html' %} 37 | - [{{ p.title | escape }}]({{ effective_baseurl }}{{ p.url }}) 38 | {% endif %} 39 | {% endfor %} 40 | 41 | ## ADR 42 | 43 | {% assign adr_pages = site.pages | where_exp: "p", "p.url contains '/adr/'" | sort: "title" %} 44 | {% for p in adr_pages %} 45 | {% if p.url contains '.html' %} 46 | - [{{ p.title | escape }}]({{ effective_baseurl }}{{ p.url }}) 47 | {% endif %} 48 | {% endfor %} 49 | 50 | ## API 51 | 52 | {% assign api_pages = site.pages | where_exp: "p", "p.url contains '/api/'" | sort: "title" %} 53 | {% for p in api_pages %} 54 | {% if p.url contains '.html' %} 55 | - [{{ p.title | escape }}]({{ effective_baseurl }}{{ p.url }}) 56 | {% endif %} 57 | {% endfor %} 58 | 59 | ## Machine sitemap 60 | 61 | - XML sitemap: [sitemap.xml]({{ effective_baseurl }}/sitemap.xml) 62 | -------------------------------------------------------------------------------- /docs/Features/provider-filesystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "file system storage, local development, ManagedCode.Storage.FileSystem, IStorage, tests, .NET" 3 | --- 4 | 5 | # Feature: File System Provider (`ManagedCode.Storage.FileSystem`) 6 | 7 | ## Purpose 8 | 9 | Implement `IStorage` on top of the local file system so you can use the same abstraction in production code, local development, and tests: 10 | 11 | - local development 12 | - on-prem/hybrid deployments 13 | - tests and demos 14 | 15 | ## Main Flows 16 | 17 | ```mermaid 18 | flowchart LR 19 | App --> FS[FileSystemStorage : IFileSystemStorage] 20 | FS --> IO[System.IO] 21 | IO --> Disk[(Disk)] 22 | ``` 23 | 24 | ## Components 25 | 26 | - `Storages/ManagedCode.Storage.FileSystem/FileSystemStorage.cs` 27 | - `Storages/ManagedCode.Storage.FileSystem/FileSystemStorageProvider.cs` 28 | - DI: 29 | - `Storages/ManagedCode.Storage.FileSystem/Extensions/ServiceCollectionExtensions.cs` 30 | - `Storages/ManagedCode.Storage.FileSystem/Extensions/StorageFactoryExtensions.cs` 31 | - Options: 32 | - `Storages/ManagedCode.Storage.FileSystem/Options/FileSystemStorageOptions.cs` 33 | 34 | ## DI Wiring 35 | 36 | ```bash 37 | dotnet add package ManagedCode.Storage.FileSystem 38 | ``` 39 | 40 | ```csharp 41 | using ManagedCode.Storage.FileSystem.Extensions; 42 | 43 | builder.Services.AddFileSystemStorageAsDefault(options => 44 | { 45 | options.BaseFolder = Path.Combine(builder.Environment.ContentRootPath, "storage"); 46 | }); 47 | ``` 48 | 49 | ## Current Behavior 50 | 51 | - `BaseFolder` acts as the container root. 52 | - Supports directory creation when `CreateContainerIfNotExists = true`. 53 | 54 | ## Tests 55 | 56 | - `Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemUploadTests.cs` 57 | - `Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemDownloadTests.cs` 58 | - `Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemBlobTests.cs` 59 | - `Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemContainerTests.cs` 60 | - `Tests/ManagedCode.Storage.Tests/Storages/FileSystem/FileSystemSecurityTests.cs` 61 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/Fixtures/FileSystemVirtualFileSystemFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Storage.FileSystem; 5 | using ManagedCode.Storage.FileSystem.Options; 6 | using Xunit; 7 | 8 | namespace ManagedCode.Storage.Tests.VirtualFileSystem.Fixtures; 9 | 10 | public sealed class FileSystemVirtualFileSystemFixture : IVirtualFileSystemFixture, IAsyncLifetime 11 | { 12 | private readonly string _rootPath = Path.Combine(Directory.GetCurrentDirectory(), "managedcode-vfs-matrix", Guid.NewGuid().ToString("N")); 13 | 14 | public VirtualFileSystemCapabilities Capabilities { get; } = new(); 15 | 16 | public Task InitializeAsync() 17 | { 18 | Directory.CreateDirectory(_rootPath); 19 | return Task.CompletedTask; 20 | } 21 | 22 | public Task DisposeAsync() 23 | { 24 | if (Directory.Exists(_rootPath)) 25 | { 26 | Directory.Delete(_rootPath, recursive: true); 27 | } 28 | 29 | return Task.CompletedTask; 30 | } 31 | 32 | public async Task CreateContextAsync() 33 | { 34 | var baseFolder = Path.Combine(_rootPath, Guid.NewGuid().ToString("N")); 35 | Directory.CreateDirectory(baseFolder); 36 | 37 | var options = new FileSystemStorageOptions 38 | { 39 | BaseFolder = baseFolder, 40 | CreateContainerIfNotExists = true 41 | }; 42 | 43 | var storage = new FileSystemStorage(options); 44 | var cleanup = new Func(async () => 45 | { 46 | await storage.RemoveContainerAsync(); 47 | if (Directory.Exists(baseFolder)) 48 | { 49 | Directory.Delete(baseFolder, recursive: true); 50 | } 51 | }); 52 | 53 | return await VirtualFileSystemTestContext.CreateAsync( 54 | storage, 55 | containerName: string.Empty, 56 | ownsStorage: true, 57 | serviceProvider: null, 58 | cleanup: cleanup); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.Google/GCPStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.Core; 3 | using ManagedCode.Storage.Core.Extensions; 4 | using ManagedCode.Storage.Core.Providers; 5 | using ManagedCode.Storage.Google.Options; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace ManagedCode.Storage.Google 10 | { 11 | public class GCPStorageProvider(IServiceProvider serviceProvider, GCPStorageOptions defaultOptions) : IStorageProvider 12 | { 13 | public Type StorageOptionsType => typeof(GCPStorageOptions); 14 | 15 | public TStorage CreateStorage(TOptions options) 16 | where TStorage : class, IStorage 17 | where TOptions : class, IStorageOptions 18 | { 19 | if (options is not GCPStorageOptions azureOptions) 20 | { 21 | throw new ArgumentException($"Options must be of type {typeof(GCPStorageOptions)}", nameof(options)); 22 | } 23 | 24 | var logger = serviceProvider.GetService>(); 25 | var storage = new GCPStorage(azureOptions, logger); 26 | 27 | return storage as TStorage 28 | ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 29 | } 30 | 31 | public IStorageOptions GetDefaultOptions() 32 | { 33 | return new GCPStorageOptions() 34 | { 35 | AuthFileName = defaultOptions.AuthFileName, 36 | BucketOptions = new BucketOptions 37 | { 38 | Bucket = defaultOptions.BucketOptions.Bucket, 39 | ProjectId = defaultOptions.BucketOptions.ProjectId 40 | }, 41 | GoogleCredential = defaultOptions.GoogleCredential, 42 | OriginalOptions = defaultOptions.OriginalOptions, 43 | StorageClientBuilder = defaultOptions.StorageClientBuilder, 44 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 45 | }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/Features/provider-aws-s3.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "Amazon S3, AWS S3, ManagedCode.Storage.Aws, IStorage, bucket, streaming upload, Object Lock, legal hold, .NET" 3 | --- 4 | 5 | # Feature: Amazon S3 Provider (`ManagedCode.Storage.Aws`) 6 | 7 | ## Purpose 8 | 9 | Implement `IStorage` on top of **Amazon S3**, including streaming and container (bucket) management semantics where applicable. 10 | 11 | ## Main Flows 12 | 13 | ```mermaid 14 | flowchart LR 15 | App --> S3[AWSStorage : IAWSStorage] 16 | S3 --> SDK[Amazon.S3 IAmazonS3] 17 | SDK --> S3Cloud[(Amazon S3)] 18 | ``` 19 | 20 | ## Components 21 | 22 | - `Storages/ManagedCode.Storage.Aws/AWSStorage.cs` 23 | - `Storages/ManagedCode.Storage.Aws/AWSStorageProvider.cs` 24 | - `Storages/ManagedCode.Storage.Aws/BlobStream.cs` 25 | - DI: 26 | - `Storages/ManagedCode.Storage.Aws/Extensions/ServiceCollectionExtensions.cs` 27 | - `Storages/ManagedCode.Storage.Aws/Extensions/StorageFactoryExtensions.cs` 28 | - Options: 29 | - `Storages/ManagedCode.Storage.Aws/Options/AWSStorageOptions.cs` 30 | 31 | ## DI Wiring 32 | 33 | ```bash 34 | dotnet add package ManagedCode.Storage.Aws 35 | ``` 36 | 37 | ```csharp 38 | using Amazon.S3; 39 | using ManagedCode.Storage.Aws.Extensions; 40 | 41 | builder.Services.AddAWSStorageAsDefault(options => 42 | { 43 | options.Bucket = "my-bucket"; 44 | options.PublicKey = configuration["Aws:AccessKeyId"]; 45 | options.SecretKey = configuration["Aws:SecretAccessKey"]; 46 | options.OriginalOptions = new AmazonS3Config { RegionEndpoint = Amazon.RegionEndpoint.USEast1 }; 47 | }); 48 | ``` 49 | 50 | ## Current Behavior 51 | 52 | - Supports multiple auth modes (access keys / role / instance profile) via `AWSStorageOptions`. 53 | - Can create the bucket when `CreateContainerIfNotExists = true`. 54 | 55 | ## Tests 56 | 57 | - `Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSUploadTests.cs` 58 | - `Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSDownloadTests.cs` 59 | - `Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSBlobTests.cs` 60 | - `Tests/ManagedCode.Storage.Tests/Storages/AWS/AWSContainerTests.cs` 61 | - `Tests/ManagedCode.Storage.Tests/Storages/AWS/AwsConfigTests.cs` 62 | -------------------------------------------------------------------------------- /docs/Features/provider-google-cloud-storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "Google Cloud Storage, GCS, ManagedCode.Storage.Gcp, StorageClient, IStorage, bucket, streaming, .NET" 3 | --- 4 | 5 | # Feature: Google Cloud Storage Provider (`ManagedCode.Storage.Gcp`) 6 | 7 | ## Purpose 8 | 9 | Implement `IStorage` on top of **Google Cloud Storage (GCS)** using `Google.Cloud.Storage.V1`. 10 | 11 | ## Main Flows 12 | 13 | ```mermaid 14 | flowchart LR 15 | App --> GCS[GCPStorage : IGCPStorage] 16 | GCS --> SDK[Google.Cloud.Storage.V1 StorageClient] 17 | SDK --> Cloud[(GCS)] 18 | ``` 19 | 20 | ## Components 21 | 22 | - `Storages/ManagedCode.Storage.Google/GCPStorage.cs` 23 | - `Storages/ManagedCode.Storage.Google/GCPStorageProvider.cs` 24 | - DI: 25 | - `Storages/ManagedCode.Storage.Google/Extensions/ServiceCollectionExtensions.cs` 26 | - `Storages/ManagedCode.Storage.Google/Extensions/StorageFactoryExtensions.cs` 27 | - Options: 28 | - `Storages/ManagedCode.Storage.Google/Options/GCPStorageOptions.cs` 29 | - `Storages/ManagedCode.Storage.Google/Options/BucketOptions.cs` 30 | 31 | ## DI Wiring 32 | 33 | ```bash 34 | dotnet add package ManagedCode.Storage.Gcp 35 | ``` 36 | 37 | ```csharp 38 | using Google.Apis.Auth.OAuth2; 39 | using ManagedCode.Storage.Google.Extensions; 40 | using ManagedCode.Storage.Google.Options; 41 | 42 | builder.Services.AddGCPStorageAsDefault(options => 43 | { 44 | options.GoogleCredential = GoogleCredential.FromFile("service-account.json"); 45 | options.BucketOptions = new BucketOptions 46 | { 47 | ProjectId = "my-project-id", 48 | Bucket = "my-bucket" 49 | }; 50 | }); 51 | ``` 52 | 53 | ## Current Behavior 54 | 55 | - Supports file-based auth (`AuthFileName`) and direct `GoogleCredential`. 56 | - Bucket creation can be enabled via `CreateContainerIfNotExists`. 57 | 58 | ## Tests 59 | 60 | - `Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSUploadTests.cs` 61 | - `Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSDownloadTests.cs` 62 | - `Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSBlobTests.cs` 63 | - `Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSContainerTests.cs` 64 | - `Tests/ManagedCode.Storage.Tests/Storages/GCS/GCSConfigTests.cs` 65 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/Fixtures/SftpVirtualFileSystemFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using ManagedCode.Storage.Core; 4 | using ManagedCode.Storage.Sftp; 5 | using ManagedCode.Storage.Tests.Storages.Sftp; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Testcontainers.Sftp; 8 | using Xunit; 9 | 10 | namespace ManagedCode.Storage.Tests.VirtualFileSystem.Fixtures; 11 | 12 | public sealed class SftpVirtualFileSystemFixture : IVirtualFileSystemFixture, IAsyncLifetime 13 | { 14 | private SftpContainer _container = null!; 15 | 16 | public VirtualFileSystemCapabilities Capabilities { get; } = new( 17 | Enabled: false, 18 | SupportsListing: false, 19 | SupportsDirectoryDelete: false, 20 | SupportsDirectoryCopy: false, 21 | SupportsMove: false, 22 | SupportsDirectoryStats: false); 23 | 24 | public async Task InitializeAsync() 25 | { 26 | _container = SftpContainerFactory.Create(); 27 | await _container.StartAsync(); 28 | } 29 | 30 | public async Task DisposeAsync() 31 | { 32 | if (_container is not null) 33 | { 34 | await _container.DisposeAsync(); 35 | } 36 | } 37 | 38 | public async Task CreateContextAsync() 39 | { 40 | var host = _container.GetHost(); 41 | var port = _container.GetPort(); 42 | var username = SftpContainerFactory.Username; 43 | var password = SftpContainerFactory.Password; 44 | var remoteDirectory = $"{SftpContainerFactory.RemoteDirectory}/vfs-{Guid.NewGuid():N}"; 45 | 46 | var provider = SftpConfigurator.ConfigureServices(host, port, username, password, remoteDirectory); 47 | var storage = provider.GetRequiredService(); 48 | 49 | async ValueTask Cleanup() 50 | { 51 | await storage.RemoveContainerAsync(); 52 | } 53 | 54 | return await VirtualFileSystemTestContext.CreateAsync( 55 | storage, 56 | remoteDirectory, 57 | ownsStorage: false, 58 | serviceProvider: provider, 59 | cleanup: Cleanup); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/CloudKit/CloudKitStorageTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using ManagedCode.Storage.CloudKit; 6 | using ManagedCode.Storage.CloudKit.Options; 7 | using Shouldly; 8 | using Xunit; 9 | 10 | namespace ManagedCode.Storage.Tests.Storages.CloudKit; 11 | 12 | public class CloudKitStorageTests 13 | { 14 | [Fact] 15 | public async Task CloudKitStorage_RoundTrip_WithHttpHandler() 16 | { 17 | var handler = new FakeCloudKitHttpHandler(); 18 | var httpClient = new HttpClient(handler); 19 | 20 | var storage = new CloudKitStorage(new CloudKitStorageOptions 21 | { 22 | ContainerId = "iCloud.com.example.app", 23 | Environment = CloudKitEnvironment.Development, 24 | Database = CloudKitDatabase.Public, 25 | ApiToken = "test-token", 26 | RootPath = "app-data", 27 | HttpClient = httpClient 28 | }); 29 | 30 | var upload = await storage.UploadAsync("storage payload", options => 31 | { 32 | options.Directory = "dir"; 33 | options.FileName = "file.txt"; 34 | }); 35 | 36 | upload.IsSuccess.ShouldBeTrue(); 37 | upload.Value.FullName.ShouldBe("dir/file.txt"); 38 | upload.Value.Container.ShouldBe("iCloud.com.example.app"); 39 | 40 | var download = await storage.DownloadAsync("dir/file.txt"); 41 | download.IsSuccess.ShouldBeTrue(); 42 | using (var reader = new StreamReader(download.Value.FileStream, Encoding.UTF8)) 43 | { 44 | (await reader.ReadToEndAsync()).ShouldBe("storage payload"); 45 | } 46 | 47 | var existsBeforeDelete = await storage.ExistsAsync("dir/file.txt"); 48 | existsBeforeDelete.IsSuccess.ShouldBeTrue(); 49 | existsBeforeDelete.Value.ShouldBeTrue(); 50 | 51 | var deleteDir = await storage.DeleteDirectoryAsync("dir"); 52 | deleteDir.IsSuccess.ShouldBeTrue(); 53 | 54 | var existsAfterDelete = await storage.ExistsAsync("dir/file.txt"); 55 | existsAfterDelete.IsSuccess.ShouldBeTrue(); 56 | existsAfterDelete.Value.ShouldBeFalse(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/Storages/Abstracts/ContainerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DotNet.Testcontainers.Containers; 5 | using Shouldly; 6 | using ManagedCode.Storage.Tests.Common; 7 | using Xunit; 8 | 9 | namespace ManagedCode.Storage.Tests.Storages.Abstracts; 10 | 11 | public abstract class ContainerTests : BaseContainer where T : IContainer 12 | { 13 | [Fact] 14 | public async Task CreateContainer_ShouldBeSuccess() 15 | { 16 | var container = await Storage.CreateContainerAsync(); 17 | container.IsSuccess 18 | .ShouldBeTrue(); 19 | } 20 | 21 | [Fact] 22 | public async Task CreateContainerAsync() 23 | { 24 | await Should.NotThrowAsync(() => Storage.CreateContainerAsync()); 25 | } 26 | 27 | [Fact] 28 | public async Task RemoveContainer_ShouldBeSuccess() 29 | { 30 | var createResult = await Storage.CreateContainerAsync(); 31 | createResult.IsSuccess 32 | .ShouldBeTrue(); 33 | 34 | var result = await Storage.RemoveContainerAsync(); 35 | 36 | result.IsSuccess 37 | .ShouldBeTrue(result.Problem?.Detail ?? "Failed without details"); 38 | } 39 | 40 | [Fact] 41 | public async Task GetFileListAsyncTest() 42 | { 43 | await UploadTestFileAsync(); 44 | await UploadTestFileAsync(); 45 | await UploadTestFileAsync(); 46 | 47 | var files = await Storage.GetBlobMetadataListAsync() 48 | .ToListAsync(); 49 | files.Count 50 | .ShouldBeGreaterThanOrEqualTo(3); 51 | } 52 | 53 | [Fact] 54 | public async Task DeleteDirectory_ShouldBeSuccess() 55 | { 56 | // Arrange 57 | var directory = "test-directory"; 58 | await UploadTestFileListAsync(directory, 3); 59 | 60 | // Act 61 | var result = await Storage.DeleteDirectoryAsync(directory); 62 | var blobs = await Storage.GetBlobMetadataListAsync(directory) 63 | .ToListAsync(); 64 | 65 | // Assert 66 | result.IsSuccess 67 | .ShouldBeTrue(result.Problem?.Detail ?? "Failed without details"); 68 | 69 | blobs.Count 70 | .ShouldBe(0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/CloudKitStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ManagedCode.Storage.CloudKit.Options; 3 | using ManagedCode.Storage.Core; 4 | using ManagedCode.Storage.Core.Providers; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace ManagedCode.Storage.CloudKit; 8 | 9 | public class CloudKitStorageProvider(IServiceProvider serviceProvider, CloudKitStorageOptions defaultOptions) : IStorageProvider 10 | { 11 | public Type StorageOptionsType => typeof(CloudKitStorageOptions); 12 | 13 | public TStorage CreateStorage(TOptions options) 14 | where TStorage : class, IStorage 15 | where TOptions : class, IStorageOptions 16 | { 17 | if (options is not CloudKitStorageOptions cloudKitOptions) 18 | { 19 | throw new ArgumentException($"Options must be of type {typeof(CloudKitStorageOptions)}", nameof(options)); 20 | } 21 | 22 | var logger = serviceProvider.GetService(typeof(ILogger)) as ILogger; 23 | var storage = new CloudKitStorage(cloudKitOptions, logger); 24 | return storage as TStorage ?? throw new InvalidOperationException($"Cannot create storage of type {typeof(TStorage)}"); 25 | } 26 | 27 | public IStorageOptions GetDefaultOptions() 28 | { 29 | return new CloudKitStorageOptions 30 | { 31 | ContainerId = defaultOptions.ContainerId, 32 | Environment = defaultOptions.Environment, 33 | Database = defaultOptions.Database, 34 | RootPath = defaultOptions.RootPath, 35 | RecordType = defaultOptions.RecordType, 36 | PathFieldName = defaultOptions.PathFieldName, 37 | AssetFieldName = defaultOptions.AssetFieldName, 38 | ContentTypeFieldName = defaultOptions.ContentTypeFieldName, 39 | ApiToken = defaultOptions.ApiToken, 40 | WebAuthToken = defaultOptions.WebAuthToken, 41 | ServerToServerKeyId = defaultOptions.ServerToServerKeyId, 42 | ServerToServerPrivateKeyPem = defaultOptions.ServerToServerPrivateKeyPem, 43 | HttpClient = defaultOptions.HttpClient, 44 | Client = defaultOptions.Client, 45 | CreateContainerIfNotExists = defaultOptions.CreateContainerIfNotExists 46 | }; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14 6 | true 7 | embedded 8 | enable 9 | true 10 | 11 | 12 | 13 | 14 | ManagedCode 15 | Copyright © 2021-$([System.DateTime]::Now.ToString(`yyyy`)) ManagedCode SAS 16 | true 17 | true 18 | true 19 | snupkg 20 | Github 21 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 22 | logo.png 23 | MIT 24 | true 25 | README.md 26 | 27 | https://github.com/managedcode/Storage 28 | https://github.com/managedcode/Storage 29 | Managed Code - Storage 30 | 10.0.1 31 | 10.0.1 32 | 33 | 34 | 35 | 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | all 45 | runtime; build; native; contentfiles; analyzers; buildtransitive 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/Storage/StorageFromFileExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using ManagedCode.Communication; 7 | using ManagedCode.Storage.Core; 8 | using ManagedCode.Storage.Core.Models; 9 | using ManagedCode.Storage.Server.Extensions.File; 10 | using Microsoft.AspNetCore.Http; 11 | 12 | namespace ManagedCode.Storage.Server.Extensions.Storage; 13 | 14 | public static class StorageFromFileExtensions 15 | { 16 | public static async Task> UploadToStorageAsync(this IStorage storage, IFormFile formFile, UploadOptions? options = null, 17 | CancellationToken cancellationToken = default) 18 | { 19 | options ??= new UploadOptions(formFile.FileName, mimeType: formFile.ContentType); 20 | 21 | await using var stream = formFile.OpenReadStream(); 22 | return await storage.UploadAsync(stream, options, cancellationToken); 23 | } 24 | 25 | public static async Task> UploadToStorageAsync(this IStorage storage, IFormFile formFile, Action options, 26 | CancellationToken cancellationToken = default) 27 | { 28 | var newOptions = new UploadOptions(formFile.FileName, mimeType: formFile.ContentType); 29 | options.Invoke(newOptions); 30 | 31 | await using var stream = formFile.OpenReadStream(); 32 | return await storage.UploadAsync(stream, newOptions, cancellationToken); 33 | } 34 | 35 | public static async IAsyncEnumerable> UploadToStorageAsync(this IStorage storage, IFormFileCollection formFiles, 36 | UploadOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) 37 | { 38 | foreach (var formFile in formFiles) 39 | yield return await storage.UploadToStorageAsync(formFile, options, cancellationToken); 40 | } 41 | 42 | public static async IAsyncEnumerable> UploadToStorageAsync(this IStorage storage, IFormFileCollection formFiles, 43 | Action options, [EnumeratorCancellation] CancellationToken cancellationToken = default) 44 | { 45 | foreach (var formFile in formFiles) 46 | yield return await storage.UploadToStorageAsync(formFile, options, cancellationToken); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/Extensions/Storage/StorageBrowserFileExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using ManagedCode.Communication; 5 | using ManagedCode.Storage.Core; 6 | using ManagedCode.Storage.Core.Models; 7 | using ManagedCode.Storage.Server.Controllers; 8 | using ManagedCode.Storage.Server.Extensions.File; 9 | using Microsoft.AspNetCore.Components.Forms; 10 | 11 | namespace ManagedCode.Storage.Server.Extensions.Storage; 12 | 13 | public static class StorageBrowserFileExtensions 14 | { 15 | public static async Task> UploadToStorageAsync(this IStorage storage, IBrowserFile formFile, UploadOptions? options = null, 16 | CancellationToken cancellationToken = default, StorageServerOptions? serverOptions = null) 17 | { 18 | options ??= new UploadOptions(formFile.Name, mimeType: formFile.ContentType); 19 | 20 | var threshold = (serverOptions ?? new StorageServerOptions()).InMemoryUploadThresholdBytes; 21 | 22 | if (formFile.Size > threshold) 23 | { 24 | var localFile = await formFile.ToLocalFileAsync(cancellationToken); 25 | return await storage.UploadAsync(localFile.FileInfo, options, cancellationToken); 26 | } 27 | 28 | await using (var stream = formFile.OpenReadStream()) 29 | { 30 | return await storage.UploadAsync(stream, options, cancellationToken); 31 | } 32 | } 33 | 34 | public static async Task> UploadToStorageAsync(this IStorage storage, IBrowserFile formFile, Action options, 35 | CancellationToken cancellationToken = default, StorageServerOptions? serverOptions = null) 36 | { 37 | var newOptions = new UploadOptions(formFile.Name, mimeType: formFile.ContentType); 38 | options.Invoke(newOptions); 39 | 40 | var threshold = (serverOptions ?? new StorageServerOptions()).InMemoryUploadThresholdBytes; 41 | 42 | if (formFile.Size > threshold) 43 | { 44 | var localFile = await formFile.ToLocalFileAsync(cancellationToken); 45 | return await storage.UploadAsync(localFile.FileInfo, newOptions, cancellationToken); 46 | } 47 | 48 | await using (var stream = formFile.OpenReadStream()) 49 | { 50 | return await storage.UploadAsync(stream, newOptions, cancellationToken); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/Features/provider-azure-blob.md: -------------------------------------------------------------------------------- 1 | --- 2 | keywords: "Azure Blob Storage, ManagedCode.Storage.Azure, IStorage, BlobClient, container, streaming upload, download, .NET" 3 | --- 4 | 5 | # Feature: Azure Blob Storage Provider (`ManagedCode.Storage.Azure`) 6 | 7 | ## Purpose 8 | 9 | Implement `IStorage` on top of **Azure Blob Storage** using the Azure SDK, including streaming and metadata operations. 10 | 11 | ## Main Flows 12 | 13 | ```mermaid 14 | flowchart LR 15 | App --> AzureStorage[AzureStorage : IAzureStorage] 16 | AzureStorage --> BlobClient[Azure SDK BlobContainerClient/BlobClient] 17 | BlobClient --> Azure[(Azure Blob Storage)] 18 | ``` 19 | 20 | ## Components 21 | 22 | - Core types: 23 | - `Storages/ManagedCode.Storage.Azure/AzureStorage.cs` 24 | - `Storages/ManagedCode.Storage.Azure/AzureStorageProvider.cs` 25 | - `Storages/ManagedCode.Storage.Azure/BlobStream.cs` (stream helpers) 26 | - DI: 27 | - `Storages/ManagedCode.Storage.Azure/Extensions/ServiceCollectionExtensions.cs` 28 | - `Storages/ManagedCode.Storage.Azure/Extensions/StorageFactoryExtensions.cs` 29 | - Options: 30 | - `Storages/ManagedCode.Storage.Azure/Options/AzureStorageOptions.cs` (connection string) 31 | - `Storages/ManagedCode.Storage.Azure/Options/AzureStorageCredentialsOptions.cs` (token credential) 32 | 33 | ## DI Wiring 34 | 35 | ```bash 36 | dotnet add package ManagedCode.Storage.Azure 37 | ``` 38 | 39 | ```csharp 40 | using ManagedCode.Storage.Azure.Extensions; 41 | 42 | builder.Services.AddAzureStorageAsDefault(options => 43 | { 44 | options.Container = "my-container"; 45 | options.ConnectionString = configuration["Azure:ConnectionString"]; 46 | }); 47 | ``` 48 | 49 | ## Current Behavior 50 | 51 | - Supports container creation when `CreateContainerIfNotExists = true`. 52 | - Uses Azure SDK transfer options when configured (`UploadTransferOptions`). 53 | 54 | ## Tests 55 | 56 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureUploadTests.cs` 57 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureDownloadTests.cs` 58 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureBlobTests.cs` 59 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureBlobStreamTests.cs` 60 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureContainerTests.cs` 61 | - `Tests/ManagedCode.Storage.Tests/Storages/Azure/AzureConfigTests.cs` 62 | 63 | ## References 64 | 65 | - `README.md` (package list + general usage) 66 | - Azure SDK docs (Blob Storage) 67 | -------------------------------------------------------------------------------- /Integraions/ManagedCode.Storage.Server/ChunkUpload/ChunkUploadSession.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace ManagedCode.Storage.Server.ChunkUpload; 7 | 8 | internal sealed class ChunkUploadSession 9 | { 10 | private readonly ConcurrentDictionary _chunkFiles = new(); 11 | 12 | public ChunkUploadSession(string uploadId, string fileName, string? contentType, int totalChunks, int chunkSize, long? fileSize, string workingDirectory) 13 | { 14 | UploadId = uploadId; 15 | FileName = fileName; 16 | ContentType = contentType; 17 | TotalChunks = totalChunks; 18 | ChunkSize = chunkSize; 19 | FileSize = fileSize; 20 | WorkingDirectory = workingDirectory; 21 | LastTouchedUtc = DateTimeOffset.UtcNow; 22 | } 23 | 24 | public string UploadId { get; } 25 | 26 | public string FileName { get; } 27 | 28 | public string? ContentType { get; } 29 | 30 | public int TotalChunks { get; } 31 | 32 | public int ChunkSize { get; } 33 | 34 | public long? FileSize { get; } 35 | 36 | public string WorkingDirectory { get; } 37 | 38 | public DateTimeOffset LastTouchedUtc { get; private set; } 39 | 40 | public IReadOnlyDictionary ChunkFiles => _chunkFiles; 41 | 42 | public void Touch() 43 | { 44 | LastTouchedUtc = DateTimeOffset.UtcNow; 45 | } 46 | 47 | public string RegisterChunk(int index, string path) 48 | { 49 | _chunkFiles[index] = path; 50 | Touch(); 51 | return path; 52 | } 53 | 54 | public void EnsureAllChunksPresent() 55 | { 56 | if (TotalChunks <= 0) 57 | { 58 | return; 59 | } 60 | 61 | for (var i = 1; i <= TotalChunks; i++) 62 | { 63 | if (!_chunkFiles.ContainsKey(i)) 64 | { 65 | throw new InvalidOperationException($"Missing chunk {i} for upload {UploadId}"); 66 | } 67 | } 68 | } 69 | 70 | public void Cleanup() 71 | { 72 | foreach (var (_, path) in _chunkFiles) 73 | { 74 | if (File.Exists(path)) 75 | { 76 | File.Delete(path); 77 | } 78 | } 79 | 80 | if (Directory.Exists(WorkingDirectory)) 81 | { 82 | Directory.Delete(WorkingDirectory, recursive: true); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/ManagedCode.Storage.Tests/VirtualFileSystem/Fixtures/AzureVirtualFileSystemFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using ManagedCode.Storage.Azure.Extensions; 4 | using ManagedCode.Storage.Azure.Options; 5 | using ManagedCode.Storage.Core; 6 | using ManagedCode.Storage.Tests.Common; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Testcontainers.Azurite; 9 | using Xunit; 10 | 11 | namespace ManagedCode.Storage.Tests.VirtualFileSystem.Fixtures; 12 | 13 | public sealed class AzureVirtualFileSystemFixture : IVirtualFileSystemFixture, IAsyncLifetime 14 | { 15 | private AzuriteContainer _container = null!; 16 | 17 | public VirtualFileSystemCapabilities Capabilities { get; } = new(); 18 | 19 | public async Task InitializeAsync() 20 | { 21 | _container = new AzuriteBuilder() 22 | .WithImage(ContainerImages.Azurite) 23 | .WithCommand("--skipApiVersionCheck") 24 | .Build(); 25 | 26 | await _container.StartAsync(); 27 | } 28 | 29 | public async Task DisposeAsync() 30 | { 31 | if (_container is not null) 32 | { 33 | await _container.DisposeAsync(); 34 | } 35 | } 36 | 37 | public async Task CreateContextAsync() 38 | { 39 | var containerName = $"vfs-{Guid.NewGuid():N}"; 40 | var connectionString = _container.GetConnectionString(); 41 | 42 | var services = new ServiceCollection(); 43 | 44 | services.AddLogging(); 45 | 46 | services.AddAzureStorageAsDefault(options => 47 | { 48 | options.ConnectionString = connectionString; 49 | options.Container = containerName; 50 | options.CreateContainerIfNotExists = true; 51 | }); 52 | 53 | services.AddAzureStorage(new AzureStorageOptions 54 | { 55 | ConnectionString = connectionString, 56 | Container = containerName, 57 | CreateContainerIfNotExists = true 58 | }); 59 | 60 | var provider = services.BuildServiceProvider(); 61 | var storage = provider.GetRequiredService(); 62 | 63 | async ValueTask Cleanup() 64 | { 65 | await storage.RemoveContainerAsync(); 66 | } 67 | 68 | return await VirtualFileSystemTestContext.CreateAsync( 69 | storage, 70 | containerName, 71 | ownsStorage: false, 72 | serviceProvider: provider, 73 | cleanup: Cleanup); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Storages/ManagedCode.Storage.CloudKit/Options/CloudKitStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using ManagedCode.Storage.CloudKit.Clients; 3 | using ManagedCode.Storage.Core; 4 | 5 | namespace ManagedCode.Storage.CloudKit.Options; 6 | 7 | public class CloudKitStorageOptions : IStorageOptions 8 | { 9 | public bool CreateContainerIfNotExists { get; set; } 10 | 11 | /// 12 | /// CloudKit container identifier, e.g. iCloud.com.company.app. 13 | /// 14 | public string ContainerId { get; set; } = string.Empty; 15 | 16 | public CloudKitEnvironment Environment { get; set; } = CloudKitEnvironment.Development; 17 | 18 | public CloudKitDatabase Database { get; set; } = CloudKitDatabase.Public; 19 | 20 | /// 21 | /// Optional prefix applied to all blob paths (like a virtual folder). 22 | /// 23 | public string RootPath { get; set; } = string.Empty; 24 | 25 | /// 26 | /// CloudKit record type that stores files. 27 | /// 28 | public string RecordType { get; set; } = "MCStorageFile"; 29 | 30 | public string PathFieldName { get; set; } = "path"; 31 | 32 | public string AssetFieldName { get; set; } = "file"; 33 | 34 | public string ContentTypeFieldName { get; set; } = "contentType"; 35 | 36 | /// 37 | /// API token authentication (ckAPIToken) for CloudKit Web Services. 38 | /// 39 | public string? ApiToken { get; set; } 40 | 41 | /// 42 | /// Optional user authentication token (ckWebAuthToken) for user-scoped CloudKit requests. 43 | /// Note: CloudKit rotates this token on each request; callers should treat it as single-use and persist the rotated value. 44 | /// 45 | public string? WebAuthToken { get; set; } 46 | 47 | /// 48 | /// Server-to-server key id for signed requests (X-Apple-CloudKit-Request-KeyID). 49 | /// 50 | public string? ServerToServerKeyId { get; set; } 51 | 52 | /// 53 | /// Server-to-server private key in PEM (PKCS8) format. 54 | /// 55 | public string? ServerToServerPrivateKeyPem { get; set; } 56 | 57 | /// 58 | /// Optional custom HttpClient used for CloudKit Web Services requests. 59 | /// 60 | public HttpClient? HttpClient { get; set; } 61 | 62 | /// 63 | /// Optional custom CloudKit client (useful for tests). 64 | /// 65 | public ICloudKitClient? Client { get; set; } 66 | } 67 | --------------------------------------------------------------------------------