From 58777bd6d7582bb4919334e77cdab67cd73e1e71 Mon Sep 17 00:00:00 2001 From: dscherdil Date: Fri, 4 Mar 2022 16:49:48 +0100 Subject: [PATCH] Add azure blob storage support --- .../FileSystemAzureBlobStorageOptions.cs | 27 ++ .../Configuration/FileSystemType.cs | 5 + .../TestFtpServer/Configuration/FtpOptions.cs | 11 +- .../ServiceCollectionExtensions.cs | 12 +- samples/TestFtpServer/TestFtpServer.csproj | 1 + samples/TestFtpServer/appsettings.json | 11 + .../AzureBlobStorageDirectoryEntry.cs | 33 ++ .../AzureBlobStorageFileEntry.cs | 28 ++ .../AzureBlobStorageFileSystem.cs | 304 ++++++++++++++++++ .../AzureBlobStorageFileSystemEntry.cs | 57 ++++ .../AzureBlobStorageFileSystemOptions.cs | 27 ++ .../AzureBlobStorageFileSystemProvider.cs | 47 +++ ...reBlobStorageFtpServerBuilderExtensions.cs | 31 ++ .../AzureBlobStoragePath.cs | 33 ++ ...pServer.FileSystem.AzureBlobStorage.csproj | 17 + 15 files changed, 642 insertions(+), 2 deletions(-) create mode 100644 samples/TestFtpServer/Configuration/FileSystemAzureBlobStorageOptions.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageDirectoryEntry.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileEntry.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystem.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemEntry.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemOptions.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemProvider.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFtpServerBuilderExtensions.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStoragePath.cs create mode 100644 src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/FubarDev.FtpServer.FileSystem.AzureBlobStorage.csproj diff --git a/samples/TestFtpServer/Configuration/FileSystemAzureBlobStorageOptions.cs b/samples/TestFtpServer/Configuration/FileSystemAzureBlobStorageOptions.cs new file mode 100644 index 00000000..15017b15 --- /dev/null +++ b/samples/TestFtpServer/Configuration/FileSystemAzureBlobStorageOptions.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +namespace TestFtpServer.Configuration +{ + /// + /// Options for the Azure blob storage file system. + /// + public class FileSystemAzureBlobStorageOptions + { + /// + /// Gets or sets the root name. + /// + public string? RootPath { get; set; } + + /// + /// Gets or sets the container name. + /// + public string? ContainerName { get; set; } + + /// + /// Gets or sets the connection string. + /// + public string? ConnectionString { get; set; } + } +} diff --git a/samples/TestFtpServer/Configuration/FileSystemType.cs b/samples/TestFtpServer/Configuration/FileSystemType.cs index 2fb04340..01b31b90 100644 --- a/samples/TestFtpServer/Configuration/FileSystemType.cs +++ b/samples/TestFtpServer/Configuration/FileSystemType.cs @@ -38,5 +38,10 @@ public enum FileSystemType /// Amazon S3 file system. /// AmazonS3, + + /// + /// Azure Blob Storage file system. + /// + AzureBlobStorage, } } diff --git a/samples/TestFtpServer/Configuration/FtpOptions.cs b/samples/TestFtpServer/Configuration/FtpOptions.cs index 1017a559..5d2a8e2d 100644 --- a/samples/TestFtpServer/Configuration/FtpOptions.cs +++ b/samples/TestFtpServer/Configuration/FtpOptions.cs @@ -58,6 +58,7 @@ public string Backend switch (BackendType) { case FileSystemType.AmazonS3: return "amazon-s3"; + case FileSystemType.AzureBlobStorage: return "azureblobstorage"; case FileSystemType.InMemory: return "in-memory"; case FileSystemType.SystemIO: return "system-io"; case FileSystemType.Unix: return "unix"; @@ -77,6 +78,9 @@ public string Backend case "amazon-s3": BackendType = FileSystemType.AmazonS3; break; + case "azureblobstorage": + BackendType = FileSystemType.AzureBlobStorage; + break; case "inMemory": case "in-memory": BackendType = FileSystemType.InMemory; @@ -98,7 +102,7 @@ public string Backend break; default: throw new ArgumentOutOfRangeException( - $"Value must be one of \"in-memory\", \"system-io\", \"unix\", \"google-drive:user\", \"google-drive:service\", \"amazon-s3\" but was , \"{value}\""); + $"Value must be one of \"in-memory\", \"system-io\", \"unix\", \"google-drive:user\", \"google-drive:service\", \"amazon-s3\", \"azureblobstorage\" but was , \"{value}\""); } } } @@ -170,6 +174,11 @@ public string Layout /// public FileSystemAmazonS3Options AmazonS3 { get; set; } = new FileSystemAmazonS3Options(); + /// + /// Gets or sets azure blob storage system options. + /// + public FileSystemAzureBlobStorageOptions AzureBlobStorage { get; set; } = new FileSystemAzureBlobStorageOptions(); + internal FileSystemLayoutType LayoutType { get => _layout ?? FileSystemLayoutType.SingleRoot; diff --git a/samples/TestFtpServer/ServiceCollectionExtensions.cs b/samples/TestFtpServer/ServiceCollectionExtensions.cs index e462b288..21fc8d7c 100644 --- a/samples/TestFtpServer/ServiceCollectionExtensions.cs +++ b/samples/TestFtpServer/ServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using FubarDev.FtpServer.CommandExtensions; using FubarDev.FtpServer.Commands; using FubarDev.FtpServer.FileSystem; +using FubarDev.FtpServer.FileSystem.AzureBlobStorage; using FubarDev.FtpServer.FileSystem.DotNet; using FubarDev.FtpServer.FileSystem.GoogleDrive; using FubarDev.FtpServer.FileSystem.InMemory; @@ -93,7 +94,12 @@ public static IServiceCollection AddFtpServices( opt.BucketRegion = options.AmazonS3.BucketRegion; opt.AwsAccessKeyId = options.AmazonS3.AwsAccessKeyId; opt.AwsSecretAccessKey = options.AmazonS3.AwsSecretAccessKey; - }); + }) + .Configure(opt => + { + opt.ContainerName = options.AzureBlobStorage.ContainerName; + opt.ConnectionString = options.AzureBlobStorage.ConnectionString; + }); #if NETCOREAPP services .Configure( @@ -184,6 +190,10 @@ public static IServiceCollection AddFtpServices( services = services .AddFtpServer(sb => sb.ConfigureAuthentication(options).UseS3FileSystem().ConfigureServer(options)); break; + case FileSystemType.AzureBlobStorage: + services = services + .AddFtpServer(sb => sb.ConfigureAuthentication(options).UseAzureBlobStorageFileSystem().ConfigureServer(options)); + break; default: throw new NotSupportedException( $"Backend of type {options.Backend} cannot be run from configuration file options."); diff --git a/samples/TestFtpServer/TestFtpServer.csproj b/samples/TestFtpServer/TestFtpServer.csproj index 9a5438f6..f640e3cb 100644 --- a/samples/TestFtpServer/TestFtpServer.csproj +++ b/samples/TestFtpServer/TestFtpServer.csproj @@ -22,6 +22,7 @@ + diff --git a/samples/TestFtpServer/appsettings.json b/samples/TestFtpServer/appsettings.json index 789dc0ed..451b8096 100644 --- a/samples/TestFtpServer/appsettings.json +++ b/samples/TestFtpServer/appsettings.json @@ -159,5 +159,16 @@ "awsAccessKeyId": null, /* The AWS secret key */ "awsSecretAccessKey": null + }, + + /* Use Azure Blob Storage as backend */ + "azureblobstorage": { + /* Name of the root path */ + "rootPath": null, + /* Name of the container */ + "containerName": null, + /* Connection string to the blob storage */ + "connectionString": null } } + diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageDirectoryEntry.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageDirectoryEntry.cs new file mode 100644 index 00000000..235c9f7c --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageDirectoryEntry.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using System.IO; + + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// The virtual directory entry for blob objects. + /// + internal class AzureBlobStorageDirectoryEntry : AzureBlobStorageFileSystemEntry, IUnixDirectoryEntry + { + /// + /// Initializes a new instance of the class. + /// + /// The path/prefix of the child entries. + /// Determines if this is the root directory. + public AzureBlobStorageDirectoryEntry(string key, bool isRoot = false) + : base(key.EndsWith("/") || isRoot ? key : key + "/", Path.GetFileName(key.TrimEnd('/'))) + { + IsRoot = isRoot; + } + + /// + public bool IsRoot { get; } + + + /// + public bool IsDeletable => !IsRoot; + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileEntry.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileEntry.cs new file mode 100644 index 00000000..cb09d335 --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileEntry.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using System.IO; + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// A file entry for an azure blob storage object. + /// + internal class AzureBlobStorageFileEntry : AzureBlobStorageFileSystemEntry, IUnixFileEntry + { + /// + /// Initializes a new instance of the class. + /// + /// The blob object key. + /// The object size. + public AzureBlobStorageFileEntry(string key, long size) + : base(key, Path.GetFileName(key)) + { + Size = size; + } + + /// + public long Size { get; } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystem.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystem.cs new file mode 100644 index 00000000..14d6ee7c --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystem.cs @@ -0,0 +1,304 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; + +using FubarDev.FtpServer.BackgroundTransfer; + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// The The Azure Blob Storage file system implementation. + /// + public sealed class AzureBlobStorageFileSystem : IUnixFileSystem + { + private readonly AzureBlobStorageFileSystemOptions _options; + private readonly BlobServiceClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The provider options. + /// The root container. + public AzureBlobStorageFileSystem(AzureBlobStorageFileSystemOptions options, string rootDirectory) + { + _options = options; + _client = new BlobServiceClient(options.ConnectionString); + Root = new AzureBlobStorageDirectoryEntry(rootDirectory, true); + } + + /// + public bool SupportsAppend => false; + + /// + public bool SupportsNonEmptyDirectoryDelete => true; + + /// + public StringComparer FileSystemEntryComparer => StringComparer.Ordinal; + + /// + public IUnixDirectoryEntry Root { get; } + + /// + public Task> GetEntriesAsync( + IUnixDirectoryEntry directoryEntry, + CancellationToken cancellationToken) + { + var prefix = ((AzureBlobStorageDirectoryEntry)directoryEntry).Key; + prefix = prefix.TrimStart('/'); + return ListObjectsAsync(prefix, false, cancellationToken); + } + + /// + public async Task GetEntryByNameAsync( + IUnixDirectoryEntry directoryEntry, + string name, + CancellationToken cancellationToken) + { + var key = AzureBlobStoragePath.Combine(((AzureBlobStorageDirectoryEntry)directoryEntry).Key, name); + + var entry = await GetObjectAsync(key, cancellationToken); + if (entry != null) + return entry; + + // not a file search for directory + key += '/'; + + var objects = await ListObjectsAsync(key, true, cancellationToken); + if (objects.Count > 0) + return new AzureBlobStorageDirectoryEntry(key); + + return null; + } + + /// + public async Task MoveAsync( + IUnixDirectoryEntry parent, + IUnixFileSystemEntry source, + IUnixDirectoryEntry target, + string fileName, + CancellationToken cancellationToken) + { + var sourceKey = ((AzureBlobStorageDirectoryEntry)source).Key; + var key = AzureBlobStoragePath.Combine(((AzureBlobStorageDirectoryEntry)target).Key, fileName); + + if (source is AzureBlobStorageFileEntry file) + { + await MoveFile(sourceKey, key, cancellationToken); + return new AzureBlobStorageFileEntry(key, file.Size) + { + LastWriteTime = file.LastWriteTime ?? DateTimeOffset.UtcNow, + }; + } + + if (source is AzureBlobStorageDirectoryEntry) + { + key += '/'; + var container = _client.GetBlobContainerClient(_options.ContainerName); + var response = container.GetBlobsAsync(prefix: key); + + var enumerator = response.AsPages().GetAsyncEnumerator(); + + while (await enumerator.MoveNextAsync()) + { + var blob = enumerator.Current; + if (blob == null) continue; + foreach (var item in blob.Values) + { + await MoveFile(item.Name, key + item.Name.Substring(sourceKey.Length), cancellationToken); + } + } + + return new AzureBlobStorageDirectoryEntry(key); + } + + throw new InvalidOperationException(); + } + + /// + public Task UnlinkAsync(IUnixFileSystemEntry entry, CancellationToken cancellationToken) + { + var blobClient = GetBlobClient(((AzureBlobStorageFileSystemEntry)entry).Key); + return blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.None, null, cancellationToken); + } + + /// + public async Task CreateDirectoryAsync( + IUnixDirectoryEntry targetDirectory, + string directoryName, + CancellationToken cancellationToken) + { + var key = AzureBlobStoragePath.Combine(((AzureBlobStorageDirectoryEntry)targetDirectory).Key, directoryName + "/"); + + await using var memoryStream = new MemoryStream(); + + memoryStream.Write(Encoding.UTF8.GetBytes("")); + memoryStream.Seek(0, SeekOrigin.Begin); + + // Directories are virtual in azure blob storage if empty, upload an empty file as a workaround + await UploadFile(memoryStream, key + ".azuredir", cancellationToken); + + return new AzureBlobStorageDirectoryEntry(key); + } + + /// + public async Task OpenReadAsync( + IUnixFileEntry fileEntry, + long startPosition, + CancellationToken cancellationToken) + { + var blobClient = GetBlobClient(((AzureBlobStorageFileSystemEntry)fileEntry).Key); + + if (!await blobClient.ExistsAsync()) return Stream.Null; + + var stream = await blobClient.OpenReadAsync(startPosition); + + if (startPosition != 0) + { + stream.Seek(startPosition, SeekOrigin.Begin); + } + + return stream; + } + + /// + public Task AppendAsync( + IUnixFileEntry fileEntry, + long? startPosition, + Stream data, + CancellationToken cancellationToken) + { + throw new InvalidOperationException(); + } + + /// + public async Task CreateAsync( + IUnixDirectoryEntry targetDirectory, + string fileName, + Stream data, + CancellationToken cancellationToken) + { + var key = AzureBlobStoragePath.Combine(((AzureBlobStorageDirectoryEntry)targetDirectory).Key, fileName); + await UploadFile(data, key, cancellationToken); + return default; + } + + /// + public async Task ReplaceAsync( + IUnixFileEntry fileEntry, + Stream data, + CancellationToken cancellationToken) + { + await UploadFile(data, ((AzureBlobStorageFileEntry)fileEntry).Key, cancellationToken); + return default; + } + + /// + public Task SetMacTimeAsync( + IUnixFileSystemEntry entry, + DateTimeOffset? modify, + DateTimeOffset? access, + DateTimeOffset? create, + CancellationToken cancellationToken) + { + return Task.FromResult(entry); + } + + private async Task GetObjectAsync(string key, CancellationToken cancellationToken) + { + try + { + var container = _client.GetBlobContainerClient(_options.ContainerName); + + var response = container.GetBlobsAsync(prefix: key); + var enumerator = response.AsPages().GetAsyncEnumerator(); + await enumerator.MoveNextAsync(); + + if (enumerator.Current.Values.Count < 1) return null; + + if (key.EndsWith("/")) + return new AzureBlobStorageDirectoryEntry(key); + + var item = enumerator.Current.Values.Where(x => x.Name == key).FirstOrDefault(); + + if (item == null) return null; + + return new AzureBlobStorageFileEntry(key, item.Properties.ContentLength.GetValueOrDefault()) + { + LastWriteTime = item.Properties.LastModified, + }; + } + catch (Exception) { } + + return null; + } + + private async Task> ListObjectsAsync( + string prefix, + bool includeSelf, + CancellationToken cancellationToken) + { + var objects = new List(); + + var container = _client.GetBlobContainerClient(_options.ContainerName); + + var response = container.GetBlobsByHierarchyAsync(delimiter: "/", prefix: prefix); + var enumerator = response.AsPages().GetAsyncEnumerator(); + + while (await enumerator.MoveNextAsync()) + { + var blob = enumerator.Current; + if (blob == null) continue; + foreach (var item in blob.Values) + { + if (item.IsPrefix) + { + objects.Add(new AzureBlobStorageDirectoryEntry(item.Prefix)); + } + else if (item.IsBlob) + { + objects.Add(new AzureBlobStorageFileEntry(item.Blob.Name, item.Blob.Properties.ContentLength.GetValueOrDefault())); + } + } + } + + return objects; + } + + private async Task MoveFile(string sourceKey, string key, CancellationToken cancellationToken) + { + var blobClient = GetBlobClient(sourceKey); + + var tempDownload = await blobClient.DownloadStreamingAsync(); + await UploadFile(tempDownload.Value.Content, key, cancellationToken); + + await blobClient.DeleteAsync(); + } + + private async Task UploadFile(Stream data, string key, CancellationToken cancellationToken) + { + var blobClient = GetBlobClient(key); + try + { + var response = await blobClient.UploadAsync(data, true, cancellationToken); + } + catch (Exception) { } + } + + private BlobClient GetBlobClient(string key) + { + var container = _client.GetBlobContainerClient(_options.ContainerName); + return container.GetBlobClient(key); + } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemEntry.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemEntry.cs new file mode 100644 index 00000000..c159d54f --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemEntry.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using System; + +using FubarDev.FtpServer.FileSystem.Generic; + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// The basic file system entry for an azure blob storage file or directory. + /// + internal class AzureBlobStorageFileSystemEntry : IUnixFileSystemEntry + { + /// + /// Initializes a new instance of the class. + /// + /// The S3-specific key. + /// The name of the entry. + public AzureBlobStorageFileSystemEntry(string key, string name) + { + Key = key; + Name = name; + Permissions = new GenericUnixPermissions( + new GenericAccessMode(true, true, false), + new GenericAccessMode(true, true, false), + new GenericAccessMode(true, true, false)); + } + + /// + /// Gets the S3-specific key. + /// + public string Key { get; } + + /// + public string Owner => "owner"; + + /// + public string Group => "group"; + + /// + public string Name { get; } + + /// + public IUnixPermissions Permissions { get; } + + /// + public DateTimeOffset? LastWriteTime { get; set; } + + /// + public DateTimeOffset? CreatedTime => null; + + /// + public long NumberOfLinks => 1; + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemOptions.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemOptions.cs new file mode 100644 index 00000000..6b8ab944 --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemOptions.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// Options for the azure blob storage file system. + /// + public class AzureBlobStorageFileSystemOptions + { + /// + /// Gets or sets the root path. + /// + public string? RootPath { get; set; } + + /// + /// Gets or sets the container name. + /// + public string? ContainerName { get; set; } + + /// + /// Gets or sets the connection string. + /// + public string? ConnectionString { set; get; } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemProvider.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemProvider.cs new file mode 100644 index 00000000..9a3d5da5 --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFileSystemProvider.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using System; +using System.Threading.Tasks; + +using Microsoft.Extensions.Options; + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// The file system factory for a S3-based file system. + /// + internal class AzureBlobStorageFileSystemProvider : IFileSystemClassFactory + { + private readonly AzureBlobStorageFileSystemOptions _options; + private readonly IAccountDirectoryQuery _accountDirectoryQuery; + + /// + /// Initializes a new instance of the class. + /// + /// The provider options. + /// Interface to query account directories. + /// Gets thrown when the azure blob storage credentials weren't set. + public AzureBlobStorageFileSystemProvider(IOptions options, IAccountDirectoryQuery accountDirectoryQuery) + { + _options = options.Value; + _accountDirectoryQuery = accountDirectoryQuery; + + if (string.IsNullOrEmpty(_options.ContainerName) + || string.IsNullOrEmpty(_options.ConnectionString)) + { + throw new ArgumentException("Azure blob storage Credentials have not been set correctly"); + } + } + + /// + public Task Create(IAccountInformation accountInformation) + { + var directories = _accountDirectoryQuery.GetDirectories(accountInformation); + + return Task.FromResult( + new AzureBlobStorageFileSystem(_options, AzureBlobStoragePath.Combine(_options.RootPath ?? "", directories.RootPath))); + } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFtpServerBuilderExtensions.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFtpServerBuilderExtensions.cs new file mode 100644 index 00000000..a349b760 --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStorageFtpServerBuilderExtensions.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using FubarDev.FtpServer.FileSystem; +using FubarDev.FtpServer.FileSystem.AzureBlobStorage; + +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace FubarDev.FtpServer +{ + /// + /// Extension methods for . + /// + public static class AzureBlobStorageFtpServerBuilderExtensions + { + /// + /// Uses Azure blob storage as file system. + /// + /// The server builder used to configure the FTP server. + /// the server builder used to configure the FTP server. + public static IFtpServerBuilder UseAzureBlobStorageFileSystem(this IFtpServerBuilder builder) + { + builder.Services + .AddSingleton(); + + return builder; + } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStoragePath.cs b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStoragePath.cs new file mode 100644 index 00000000..cb0caa4b --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/AzureBlobStoragePath.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +namespace FubarDev.FtpServer.FileSystem.AzureBlobStorage +{ + /// + /// Helper functions for S3 paths. + /// + internal static class AzureBlobStoragePath + { + /// + /// Combine two paths. + /// + /// The first part of the resulting path. + /// The second part of the resulting path. + /// The combination of and with a / in between. + public static string Combine(string? first, string? second) + { + if (string.IsNullOrEmpty(first)) + { + return second ?? string.Empty; + } + + if (string.IsNullOrEmpty(second)) + { + return first; + } + + return string.Join("/", first.TrimEnd('/'), second); + } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/FubarDev.FtpServer.FileSystem.AzureBlobStorage.csproj b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/FubarDev.FtpServer.FileSystem.AzureBlobStorage.csproj new file mode 100644 index 00000000..c57e07f5 --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.AzureBlobStorage/FubarDev.FtpServer.FileSystem.AzureBlobStorage.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + enable + + + + + + + + + + + +