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
+
+
+
+
+
+
+
+
+
+
+
+