diff --git a/README.md b/README.md index 920c59c..932882b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![NuGet Version](http://img.shields.io/nuget/v/GeekLearning.Storage.svg?style=flat-square&label=nuget:%20primitives)](https://www.nuget.org/packages/GeekLearning.Storage/) -[![NuGet Version](http://img.shields.io/nuget/v/GeekLearning.Storage.FileSystem.svg?style=flat-square&label=nuget:%20filesystem)](https://www.nuget.org/packages/GeekLearning.Storage.FileSystem/) -[![NuGet Version](http://img.shields.io/nuget/v/GeekLearning.Storage.Azure.svg?style=flat-square&label=nuget:%20azure%20 storage)](https://www.nuget.org/packages/GeekLearning.Storage.Azure/) +[![NuGet Version](http://img.shields.io/nuget/v/GeekLearning.Storage.svg?style=flat-square&label=NuGet:%20Abstractions)](https://www.nuget.org/packages/GeekLearning.Storage/) +[![NuGet Version](http://img.shields.io/nuget/v/GeekLearning.Storage.FileSystem.svg?style=flat-square&label=NuGet:%20FileSystem)](https://www.nuget.org/packages/GeekLearning.Storage.FileSystem/) +[![NuGet Version](http://img.shields.io/nuget/v/GeekLearning.Storage.Azure.svg?style=flat-square&label=NuGet:%20Azure%20Storage)](https://www.nuget.org/packages/GeekLearning.Storage.Azure/) [![Build Status](https://geeklearning.visualstudio.com/_apis/public/build/definitions/f841b266-7595-4d01-9ee1-4864cf65aa73/27/badge)](#) # Geek Learning Cloud Storage Abstraction @@ -26,3 +26,4 @@ We don't support for Amazon S3, but it is one of our high priority objective. You can head to our introduction [blog post](http://geeklearning.io/dotnet-core-storage-cloud-or-file-system-storage-made-easy/), or to the [wiki](https://github.com/geeklearningio/gl-dotnet-storage/wiki). + diff --git a/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj b/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj index d9d597a..ceb2ee2 100644 --- a/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj +++ b/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj @@ -1,4 +1,4 @@ - + netcoreapp1.1 @@ -6,7 +6,7 @@ GeekLearning.Storage.BasicSample Exe GeekLearning.Storage.BasicSample - 1.1.1 + 1.1.2 $(PackageTargetFallback);portable-net45+win8 @@ -24,17 +24,17 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/samples/GeekLearning.Storage.BasicSample/Startup.cs b/samples/GeekLearning.Storage.BasicSample/Startup.cs index e9fb65d..eb16301 100644 --- a/samples/GeekLearning.Storage.BasicSample/Startup.cs +++ b/samples/GeekLearning.Storage.BasicSample/Startup.cs @@ -33,15 +33,14 @@ public void ConfigureServices(IServiceCollection services) byte[] signingKey = new byte[512]; rng.GetBytes(signingKey); - services.AddStorage() + services.AddStorage(this.Configuration.GetSection("Storage")) .AddAzureStorage() .AddFileSystemStorage(HostingEnvironement.ContentRootPath) - .AddFileSystemStorageServer(options=> { + .AddFileSystemStorageServer(options => + { options.SigningKey = signingKey; options.BaseUri = new Uri("http://localhost:11149/"); }); - - services.Configure(Configuration.GetSection("Storage")); services.AddScoped(); } diff --git a/samples/GeekLearning.Storage.BasicSample/appsettings.json b/samples/GeekLearning.Storage.BasicSample/appsettings.json index 6d3d0ab..22ffc9d 100644 --- a/samples/GeekLearning.Storage.BasicSample/appsettings.json +++ b/samples/GeekLearning.Storage.BasicSample/appsettings.json @@ -10,11 +10,8 @@ "Storage": { "Stores": { "Templates": { - "Provider": "FileSystem", - "Parameters": { - "Path": "Templates", - "Access" : "Public" - } + "ProviderType": "FileSystem", + "AccessLevel": "Public" } } } diff --git a/src/GeekLearning.Storage.Azure/AzureStorageExtensions.cs b/src/GeekLearning.Storage.Azure/AzureStorageExtensions.cs index 45cae55..3381d6c 100644 --- a/src/GeekLearning.Storage.Azure/AzureStorageExtensions.cs +++ b/src/GeekLearning.Storage.Azure/AzureStorageExtensions.cs @@ -1,12 +1,22 @@ namespace GeekLearning.Storage { using Azure; + using GeekLearning.Storage.Azure.Configuration; + using GeekLearning.Storage.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Options; public static class AzureStorageExtensions { public static IServiceCollection AddAzureStorage(this IServiceCollection services) + { + return services + .AddSingleton, ConfigureProviderOptions>() + .AddAzureStorageServices(); + } + + private static IServiceCollection AddAzureStorageServices(this IServiceCollection services) { services.TryAddEnumerable(ServiceDescriptor.Transient()); return services; diff --git a/src/GeekLearning.Storage.Azure/AzureStorageManagerOptions.cs b/src/GeekLearning.Storage.Azure/AzureStorageManagerOptions.cs deleted file mode 100644 index 917a72d..0000000 --- a/src/GeekLearning.Storage.Azure/AzureStorageManagerOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace GeekLearning.Storage.Azure -{ - using System.Collections.Generic; - - public class AzureStorageManagerOptions - { - public Dictionary SubStores { get; set; } - - public class SubStore - { - public string Container { get; set; } - - public string ConnectionString { get; set; } - } - } -} diff --git a/src/GeekLearning.Storage.Azure/AzureStorageProvider.cs b/src/GeekLearning.Storage.Azure/AzureStorageProvider.cs index b0086bc..c04dc42 100644 --- a/src/GeekLearning.Storage.Azure/AzureStorageProvider.cs +++ b/src/GeekLearning.Storage.Azure/AzureStorageProvider.cs @@ -1,14 +1,24 @@ namespace GeekLearning.Storage.Azure { + using GeekLearning.Storage.Azure.Configuration; + using GeekLearning.Storage.Internal; + using Microsoft.Extensions.Options; using Storage; - public class AzureStorageProvider : IStorageProvider + public class AzureStorageProvider : StorageProviderBase { - public string Name => "Azure"; + public const string ProviderName = "Azure"; - public IStore BuildStore(string storeName, IStorageStoreOptions storeOptions) + public AzureStorageProvider(IOptions options) + : base(options) { - return new AzureStore(storeName, storeOptions.Parameters["ConnectionString"], storeOptions.Parameters["Container"]); + } + + public override string Name => ProviderName; + + protected override IStore BuildStoreInternal(string storeName, AzureStoreOptions storeOptions) + { + return new AzureStore(storeOptions); } } } diff --git a/src/GeekLearning.Storage.Azure/AzureStore.cs b/src/GeekLearning.Storage.Azure/AzureStore.cs index ff125a7..0151860 100644 --- a/src/GeekLearning.Storage.Azure/AzureStore.cs +++ b/src/GeekLearning.Storage.Azure/AzureStore.cs @@ -1,5 +1,6 @@ namespace GeekLearning.Storage.Azure { + using GeekLearning.Storage.Azure.Configuration; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.Core; @@ -11,29 +12,41 @@ public class AzureStore : IStore { - private Lazy client; - private Lazy container; + private readonly AzureStoreOptions storeOptions; + private readonly Lazy client; + private readonly Lazy container; - public AzureStore(string storeName, string connectionString, string containerName) + public AzureStore(AzureStoreOptions storeOptions) { - this.Name = storeName; + storeOptions.Validate(); - if (string.IsNullOrWhiteSpace(connectionString)) - { - throw new ArgumentNullException("connectionString"); - } + this.storeOptions = storeOptions; + this.client = new Lazy(() => CloudStorageAccount.Parse(storeOptions.ConnectionString).CreateCloudBlobClient()); + this.container = new Lazy(() => this.client.Value.GetContainerReference(storeOptions.FolderName)); + } - if (string.IsNullOrWhiteSpace(containerName)) + public string Name => this.storeOptions.Name; + + public Task InitAsync() + { + BlobContainerPublicAccessType accessType; + switch (this.storeOptions.AccessLevel) { - throw new ArgumentNullException("containerName"); + case Storage.Configuration.AccessLevel.Public: + accessType = BlobContainerPublicAccessType.Container; + break; + case Storage.Configuration.AccessLevel.Confidential: + accessType = BlobContainerPublicAccessType.Blob; + break; + case Storage.Configuration.AccessLevel.Private: + default: + accessType = BlobContainerPublicAccessType.Off; + break; } - this.client = new Lazy(() => CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient()); - this.container = new Lazy(() => this.client.Value.GetContainerReference(containerName)); + return this.container.Value.CreateIfNotExistsAsync(accessType, null, null); } - public string Name { get; } - public async Task ListAsync(string path, bool recursive, bool withMetadata) { if (string.IsNullOrWhiteSpace(path)) diff --git a/src/GeekLearning.Storage.Azure/Configuration/AzureParsedOptions.cs b/src/GeekLearning.Storage.Azure/Configuration/AzureParsedOptions.cs new file mode 100644 index 0000000..cd97a62 --- /dev/null +++ b/src/GeekLearning.Storage.Azure/Configuration/AzureParsedOptions.cs @@ -0,0 +1,63 @@ +namespace GeekLearning.Storage.Azure.Configuration +{ + using GeekLearning.Storage.Configuration; + using System.Collections.Generic; + + public class AzureParsedOptions : IParsedOptions + { + public string Name => AzureStorageProvider.ProviderName; + + public IReadOnlyDictionary ConnectionStrings { get; set; } + + public IReadOnlyDictionary ParsedProviderInstances { get; set; } + + public IReadOnlyDictionary ParsedStores { get; set; } + + public IReadOnlyDictionary ParsedScopedStores { get; set; } + + public void BindProviderInstanceOptions(AzureProviderInstanceOptions providerInstanceOptions) + { + if (!string.IsNullOrEmpty(providerInstanceOptions.ConnectionStringName) + && string.IsNullOrEmpty(providerInstanceOptions.ConnectionString)) + { + if (!this.ConnectionStrings.ContainsKey(providerInstanceOptions.ConnectionStringName)) + { + throw new Exceptions.BadProviderConfiguration( + providerInstanceOptions.Name, + $"The ConnectionString '{providerInstanceOptions.ConnectionStringName}' cannot be found. Did you call AddStorage with the ConfigurationRoot?"); + } + + providerInstanceOptions.ConnectionString = this.ConnectionStrings[providerInstanceOptions.ConnectionStringName]; + } + } + + public void BindStoreOptions(AzureStoreOptions storeOptions, AzureProviderInstanceOptions providerInstanceOptions = null) + { + storeOptions.FolderName = storeOptions.FolderName.ToLowerInvariant(); + + if (!string.IsNullOrEmpty(storeOptions.ConnectionStringName) + && string.IsNullOrEmpty(storeOptions.ConnectionString)) + { + if (!this.ConnectionStrings.ContainsKey(storeOptions.ConnectionStringName)) + { + throw new Exceptions.BadStoreConfiguration( + storeOptions.Name, + $"The ConnectionString '{storeOptions.ConnectionStringName}' cannot be found. Did you call AddStorage with the ConfigurationRoot?"); + } + + storeOptions.ConnectionString = this.ConnectionStrings[storeOptions.ConnectionStringName]; + } + + if (providerInstanceOptions == null + || storeOptions.ProviderName != providerInstanceOptions.Name) + { + return; + } + + if (string.IsNullOrEmpty(storeOptions.ConnectionString)) + { + storeOptions.ConnectionString = providerInstanceOptions.ConnectionString; + } + } + } +} diff --git a/src/GeekLearning.Storage.Azure/Configuration/AzureProviderInstanceOptions.cs b/src/GeekLearning.Storage.Azure/Configuration/AzureProviderInstanceOptions.cs new file mode 100644 index 0000000..b97f168 --- /dev/null +++ b/src/GeekLearning.Storage.Azure/Configuration/AzureProviderInstanceOptions.cs @@ -0,0 +1,11 @@ +namespace GeekLearning.Storage.Azure.Configuration +{ + using GeekLearning.Storage.Configuration; + + public class AzureProviderInstanceOptions : ProviderInstanceOptions + { + public string ConnectionString { get; set; } + + public string ConnectionStringName { get; set; } + } +} diff --git a/src/GeekLearning.Storage.Azure/Configuration/AzureScopedStoreOptions.cs b/src/GeekLearning.Storage.Azure/Configuration/AzureScopedStoreOptions.cs new file mode 100644 index 0000000..c43eded --- /dev/null +++ b/src/GeekLearning.Storage.Azure/Configuration/AzureScopedStoreOptions.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.Azure.Configuration +{ + using GeekLearning.Storage.Configuration; + + public class AzureScopedStoreOptions : AzureStoreOptions, IScopedStoreOptions + { + public string FolderNameFormat { get; set; } + } +} diff --git a/src/GeekLearning.Storage.Azure/Configuration/AzureStoreOptions.cs b/src/GeekLearning.Storage.Azure/Configuration/AzureStoreOptions.cs new file mode 100644 index 0000000..c841405 --- /dev/null +++ b/src/GeekLearning.Storage.Azure/Configuration/AzureStoreOptions.cs @@ -0,0 +1,32 @@ +namespace GeekLearning.Storage.Azure.Configuration +{ + using GeekLearning.Storage.Configuration; + using System.Collections.Generic; + using System.Linq; + + public class AzureStoreOptions : StoreOptions + { + public string ConnectionString { get; set; } + + public string ConnectionStringName { get; set; } + + public override IEnumerable Validate(bool throwOnError = true) + { + var baseErrors = base.Validate(throwOnError); + var optionErrors = new List(); + + if (string.IsNullOrEmpty(this.ConnectionString)) + { + this.PushMissingPropertyError(optionErrors, nameof(this.ConnectionString)); + } + + var finalErrors = baseErrors.Concat(optionErrors); + if (throwOnError && finalErrors.Any()) + { + throw new Exceptions.BadStoreConfiguration(this.Name, finalErrors); + } + + return finalErrors; + } + } +} diff --git a/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.csproj b/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.csproj index 9165231..05046db 100644 --- a/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.csproj +++ b/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.csproj @@ -16,10 +16,10 @@ - - - - + + + + diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.csproj b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.csproj index 4aac9ec..a49d417 100644 --- a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.csproj +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs index afdf947..b5a639c 100644 --- a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs @@ -9,7 +9,6 @@ public class ExtendedPropertiesProvider : IExtendedPropertiesProvider { private readonly FileSystemExtendedPropertiesOptions options; - private readonly IStorageFactory storageFactory; public ExtendedPropertiesProvider( IOptions options) diff --git a/src/GeekLearning.Storage.FileSystem.Server/FileSystemStorageServerMiddleware.cs b/src/GeekLearning.Storage.FileSystem.Server/FileSystemStorageServerMiddleware.cs index d9387f8..a2a515a 100644 --- a/src/GeekLearning.Storage.FileSystem.Server/FileSystemStorageServerMiddleware.cs +++ b/src/GeekLearning.Storage.FileSystem.Server/FileSystemStorageServerMiddleware.cs @@ -1,5 +1,6 @@ namespace GeekLearning.Storage.FileSystem.Server { + using GeekLearning.Storage.FileSystem.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,14 +13,14 @@ public class FileSystemStorageServerMiddleware private ILogger logger; private IOptions serverOptions; - private IOptions storageOptions; + private FileSystemParsedOptions fileSystemParsedOptions; public FileSystemStorageServerMiddleware(RequestDelegate next, IOptions serverOptions, ILogger logger, - IOptions storageOptions) + IOptions fileSystemParsedOptions) { - this.storageOptions = storageOptions; + this.fileSystemParsedOptions = fileSystemParsedOptions.Value; this.next = next; this.serverOptions = serverOptions; this.logger = logger; @@ -33,12 +34,10 @@ public async Task Invoke(HttpContext context) var storeName = context.Request.Path.Value.Substring(1, subPathStart - 1); var storageFactory = context.RequestServices.GetRequiredService(); - StorageOptions.StorageStoreOptions storeOptions; - if (this.storageOptions.Value.Stores.TryGetValue(storeName, out storeOptions) - && storeOptions.Provider == "FileSystem") + if (this.fileSystemParsedOptions.ParsedStores.TryGetValue(storeName, out var storeOptions) + && storeOptions.ProviderType == FileSystemStorageProvider.ProviderName) { - string access; - if (!storeOptions.Parameters.TryGetValue("Access", out access) && access != "Public") + if (storeOptions.AccessLevel != Storage.Configuration.AccessLevel.Public) { context.Response.StatusCode = StatusCodes.Status403Forbidden; return; diff --git a/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.csproj b/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.csproj index d82a11c..01b1a2a 100644 --- a/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.csproj +++ b/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemParsedOptions.cs b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemParsedOptions.cs new file mode 100644 index 0000000..7294bff --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemParsedOptions.cs @@ -0,0 +1,52 @@ +namespace GeekLearning.Storage.FileSystem.Configuration +{ + using GeekLearning.Storage.Configuration; + using System.Collections.Generic; + using System.IO; + + public class FileSystemParsedOptions : IParsedOptions + { + public string Name => FileSystemStorageProvider.ProviderName; + + public IReadOnlyDictionary ConnectionStrings { get; set; } + + public IReadOnlyDictionary ParsedProviderInstances { get; set; } + + public IReadOnlyDictionary ParsedStores { get; set; } + + public IReadOnlyDictionary ParsedScopedStores { get; set; } + + public string RootPath { get; set; } + + public void BindProviderInstanceOptions(FileSystemProviderInstanceOptions providerInstanceOptions) + { + if (string.IsNullOrEmpty(providerInstanceOptions.RootPath)) + { + providerInstanceOptions.RootPath = this.RootPath; + } + else + { + if (!Path.IsPathRooted(providerInstanceOptions.RootPath)) + { + providerInstanceOptions.RootPath = Path.Combine(this.RootPath, providerInstanceOptions.RootPath); + } + } + } + + public void BindStoreOptions(FileSystemStoreOptions storeOptions, FileSystemProviderInstanceOptions providerInstanceOptions = null) + { + if (string.IsNullOrEmpty(storeOptions.RootPath)) + { + if (providerInstanceOptions != null + && storeOptions.ProviderName == providerInstanceOptions.Name) + { + storeOptions.RootPath = providerInstanceOptions.RootPath; + } + else + { + storeOptions.RootPath = this.RootPath; + } + } + } + } +} diff --git a/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemProviderInstanceOptions.cs b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemProviderInstanceOptions.cs new file mode 100644 index 0000000..bcf5e3c --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemProviderInstanceOptions.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.FileSystem.Configuration +{ + using GeekLearning.Storage.Configuration; + + public class FileSystemProviderInstanceOptions : ProviderInstanceOptions + { + public string RootPath { get; set; } + } +} diff --git a/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemScopedStoreOptions.cs b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemScopedStoreOptions.cs new file mode 100644 index 0000000..c417df2 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemScopedStoreOptions.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.FileSystem.Configuration +{ + using GeekLearning.Storage.Configuration; + + public class FileSystemScopedStoreOptions : FileSystemStoreOptions, IScopedStoreOptions + { + public string FolderNameFormat { get; set; } + } +} diff --git a/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemStoreOptions.cs b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemStoreOptions.cs new file mode 100644 index 0000000..7b3ad91 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/Configuration/FileSystemStoreOptions.cs @@ -0,0 +1,49 @@ +namespace GeekLearning.Storage.FileSystem.Configuration +{ + using GeekLearning.Storage.Configuration; + using System.IO; + using System.Collections.Generic; + using System.Linq; + + public class FileSystemStoreOptions : StoreOptions + { + public string RootPath { get; set; } + + public string AbsolutePath + { + get + { + if (string.IsNullOrEmpty(this.RootPath)) + { + return this.FolderName; + } + + if (string.IsNullOrEmpty(this.FolderName)) + { + return this.RootPath; + } + + return Path.Combine(this.RootPath, this.FolderName); + } + } + + public override IEnumerable Validate(bool throwOnError = true) + { + var baseErrors = base.Validate(throwOnError); + var optionErrors = new List(); + + if (string.IsNullOrEmpty(this.AbsolutePath)) + { + this.PushMissingPropertyError(optionErrors, nameof(this.AbsolutePath)); + } + + var finalErrors = baseErrors.Concat(optionErrors); + if (throwOnError && finalErrors.Any()) + { + throw new Exceptions.BadStoreConfiguration(this.Name, finalErrors); + } + + return finalErrors; + } + } +} diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemOptions.cs b/src/GeekLearning.Storage.FileSystem/FileSystemOptions.cs deleted file mode 100644 index 5476e6a..0000000 --- a/src/GeekLearning.Storage.FileSystem/FileSystemOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace GeekLearning.Storage.FileSystem -{ - public class FileSystemOptions - { - public string RootPath { get; set; } - } -} diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemStorageExtensions.cs b/src/GeekLearning.Storage.FileSystem/FileSystemStorageExtensions.cs index 53d2646..73825ca 100644 --- a/src/GeekLearning.Storage.FileSystem/FileSystemStorageExtensions.cs +++ b/src/GeekLearning.Storage.FileSystem/FileSystemStorageExtensions.cs @@ -1,14 +1,31 @@ namespace GeekLearning.Storage { using FileSystem; + using GeekLearning.Storage.FileSystem.Configuration; + using GeekLearning.Storage.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Options; public static class FileSystemStorageExtensions { public static IServiceCollection AddFileSystemStorage(this IServiceCollection services, string rootPath) { - services.Configure(options => options.RootPath = rootPath); + return services + .Configure(options => options.RootPath = rootPath) + .AddFileSystemStorageServices(); + } + + public static IServiceCollection AddFileSystemStorage(this IServiceCollection services) + { + return services + .Configure(options => options.RootPath = System.IO.Directory.GetCurrentDirectory()) + .AddFileSystemStorageServices(); + } + + private static IServiceCollection AddFileSystemStorageServices(this IServiceCollection services) + { + services.AddSingleton, ConfigureProviderOptions>(); services.TryAddEnumerable(ServiceDescriptor.Transient()); return services; } diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs b/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs index 5b7cb9d..ae9916d 100644 --- a/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs +++ b/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs @@ -1,32 +1,29 @@ namespace GeekLearning.Storage.FileSystem { - using Microsoft.Extensions.DependencyInjection; + using GeekLearning.Storage.FileSystem.Configuration; + using GeekLearning.Storage.Internal; using Microsoft.Extensions.Options; using Storage; - using System; - public class FileSystemStorageProvider : IStorageProvider + public class FileSystemStorageProvider : StorageProviderBase { - private IOptions options; - private IServiceProvider serviceProvider; + public const string ProviderName = "FileSystem"; + private readonly IPublicUrlProvider publicUrlProvider; + private readonly IExtendedPropertiesProvider extendedPropertiesProvider; - public FileSystemStorageProvider(IOptions options, IServiceProvider serviceProvider) + public FileSystemStorageProvider(IOptions options, IPublicUrlProvider publicUrlProvider = null, IExtendedPropertiesProvider extendedPropertiesProvider = null) + : base(options) { - this.options = options; - this.serviceProvider = serviceProvider; + this.publicUrlProvider = publicUrlProvider; + this.extendedPropertiesProvider = extendedPropertiesProvider; } - public string Name => "FileSystem"; + public override string Name => ProviderName; - public IStore BuildStore(string storeName, IStorageStoreOptions storeOptions) + protected override IStore BuildStoreInternal(string storeName, FileSystemStoreOptions storeOptions) { - var publicUrlProvider = this.serviceProvider.GetService(); - var extendedPropertiesProvider = this.serviceProvider.GetService(); - return new FileSystemStore( - storeName, - storeOptions.Parameters["Path"], - this.options.Value.RootPath, + storeOptions, publicUrlProvider, extendedPropertiesProvider); } diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs b/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs index 32c0fd6..724cfd4 100644 --- a/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs +++ b/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs @@ -1,5 +1,6 @@ namespace GeekLearning.Storage.FileSystem { + using GeekLearning.Storage.FileSystem.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -9,33 +10,32 @@ public class FileSystemStore : IStore { + private readonly FileSystemStoreOptions storeOptions; private readonly IPublicUrlProvider publicUrlProvider; private readonly IExtendedPropertiesProvider extendedPropertiesProvider; - public FileSystemStore(string storeName, string path, string rootPath, IPublicUrlProvider publicUrlProvider, IExtendedPropertiesProvider extendedPropertiesProvider) + public FileSystemStore(FileSystemStoreOptions storeOptions, IPublicUrlProvider publicUrlProvider, IExtendedPropertiesProvider extendedPropertiesProvider) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - - if (Path.IsPathRooted(path)) - { - this.AbsolutePath = path; - } - else - { - this.AbsolutePath = Path.Combine(rootPath, path); - } + storeOptions.Validate(); - this.Name = storeName; + this.storeOptions = storeOptions; this.publicUrlProvider = publicUrlProvider; this.extendedPropertiesProvider = extendedPropertiesProvider; } - public string Name { get; } + public string Name => storeOptions.Name; - internal string AbsolutePath { get; } + internal string AbsolutePath => storeOptions.AbsolutePath; + + public Task InitAsync() + { + if (!Directory.Exists(this.AbsolutePath)) + { + Directory.CreateDirectory(this.AbsolutePath); + } + + return Task.FromResult(0); + } public async Task ListAsync(string path, bool recursive, bool withMetadata) { diff --git a/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj b/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj index 24c0baa..bae7d7f 100644 --- a/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj +++ b/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/src/GeekLearning.Storage/Configuration/AccessLevel.cs b/src/GeekLearning.Storage/Configuration/AccessLevel.cs new file mode 100644 index 0000000..3442ada --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/AccessLevel.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.Configuration +{ + public enum AccessLevel + { + Private = 0, + Confidential = 1, + Public = 2, + } +} diff --git a/src/GeekLearning.Storage/Configuration/ConfigurationExtensions.cs b/src/GeekLearning.Storage/Configuration/ConfigurationExtensions.cs new file mode 100644 index 0000000..79f487f --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/ConfigurationExtensions.cs @@ -0,0 +1,130 @@ +namespace GeekLearning.Storage.Configuration +{ + using Microsoft.Extensions.Configuration; + using System.Collections.Generic; + using System.Linq; + + public static class ConfigurationExtensions + { + public static IReadOnlyDictionary Parse(this IReadOnlyDictionary unparsedConfiguration) + where TOptions : class, INamedElementOptions, new() + { + if (unparsedConfiguration == null) + { + return new Dictionary(); + } + + return unparsedConfiguration + .ToDictionary( + kvp => kvp.Key, + kvp => BindOptions(kvp)); + } + + public static TStoreOptions GetStoreConfiguration(this IParsedOptions parsedOptions, string storeName, bool throwIfNotFound = true) + where TInstanceOptions : class, IProviderInstanceOptions + where TStoreOptions : class, IStoreOptions + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + parsedOptions.ParsedStores.TryGetValue(storeName, out var storeOptions); + if (storeOptions != null) + { + return storeOptions; + } + + if (throwIfNotFound) + { + throw new Exceptions.StoreNotFoundException(storeName); + } + + return null; + } + + public static TScopedStoreOptions GetScopedStoreConfiguration(this IParsedOptions parsedOptions, string storeName, bool throwIfNotFound = true) + where TInstanceOptions : class, IProviderInstanceOptions + where TStoreOptions : class, IStoreOptions + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + parsedOptions.ParsedScopedStores.TryGetValue(storeName, out var scopedStoreOptions); + if (scopedStoreOptions != null) + { + return scopedStoreOptions; + } + + if (throwIfNotFound) + { + throw new Exceptions.StoreNotFoundException(storeName); + } + + return null; + } + + public static void Compute(this TInstanceOptions parsedProviderInstance, TParsedOptions options) + where TParsedOptions : class, IParsedOptions + where TInstanceOptions : class, IProviderInstanceOptions, new() + where TStoreOptions : class, IStoreOptions, new() + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + options.BindProviderInstanceOptions(parsedProviderInstance); + } + + public static void Compute(this TStoreOptions parsedStore, TParsedOptions options) + where TParsedOptions : class, IParsedOptions + where TInstanceOptions : class, IProviderInstanceOptions, new() + where TStoreOptions : class, IStoreOptions, new() + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + if (string.IsNullOrEmpty(parsedStore.FolderName)) + { + parsedStore.FolderName = parsedStore.Name; + } + + TInstanceOptions instanceOptions = null; + if (!string.IsNullOrEmpty(parsedStore.ProviderName)) + { + options.ParsedProviderInstances.TryGetValue(parsedStore.ProviderName, out instanceOptions); + if (instanceOptions == null) + { + return; + } + + parsedStore.ProviderType = instanceOptions.Type; + } + + options.BindStoreOptions(parsedStore, instanceOptions); + } + + public static TStoreOptions ParseStoreOptions(this IStoreOptions storeOptions, TParsedOptions options) + where TParsedOptions : class, IParsedOptions, new() + where TInstanceOptions : class, IProviderInstanceOptions, new() + where TStoreOptions : class, IStoreOptions, new() + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + if (!(storeOptions is TStoreOptions parsedStoreOptions)) + { + parsedStoreOptions = new TStoreOptions + { + Name = storeOptions.Name, + ProviderName = storeOptions.ProviderName, + ProviderType = storeOptions.ProviderType, + AccessLevel = storeOptions.AccessLevel, + FolderName = storeOptions.FolderName, + }; + } + + parsedStoreOptions.Compute(options); + return parsedStoreOptions; + } + + private static TOptions BindOptions(KeyValuePair kvp) + where TOptions : class, INamedElementOptions, new() + { + var options = new TOptions + { + Name = kvp.Key, + }; + + ConfigurationBinder.Bind(kvp.Value, options); + return options; + } + } +} diff --git a/src/GeekLearning.Storage/Configuration/INamedElementOptions.cs b/src/GeekLearning.Storage/Configuration/INamedElementOptions.cs new file mode 100644 index 0000000..b262f4c --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/INamedElementOptions.cs @@ -0,0 +1,7 @@ +namespace GeekLearning.Storage.Configuration +{ + public interface INamedElementOptions + { + string Name { get; set; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/IOptionError.cs b/src/GeekLearning.Storage/Configuration/IOptionError.cs new file mode 100644 index 0000000..8647dc6 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/IOptionError.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.Configuration +{ + public interface IOptionError + { + string PropertyName { get; } + + string ErrorMessage { get; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/IParsedOptions.cs b/src/GeekLearning.Storage/Configuration/IParsedOptions.cs new file mode 100644 index 0000000..942f935 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/IParsedOptions.cs @@ -0,0 +1,24 @@ +namespace GeekLearning.Storage.Configuration +{ + using System.Collections.Generic; + + public interface IParsedOptions + where TInstanceOptions : class, IProviderInstanceOptions + where TStoreOptions : class, IStoreOptions + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + string Name { get; } + + IReadOnlyDictionary ConnectionStrings { get; set; } + + IReadOnlyDictionary ParsedProviderInstances { get; set; } + + IReadOnlyDictionary ParsedStores { get; set; } + + IReadOnlyDictionary ParsedScopedStores { get; set; } + + void BindProviderInstanceOptions(TInstanceOptions providerInstanceOptions); + + void BindStoreOptions(TStoreOptions storeOptions, TInstanceOptions providerInstanceOptions = null); + } +} diff --git a/src/GeekLearning.Storage/Configuration/IProviderInstanceOptions.cs b/src/GeekLearning.Storage/Configuration/IProviderInstanceOptions.cs new file mode 100644 index 0000000..1f8d240 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/IProviderInstanceOptions.cs @@ -0,0 +1,7 @@ +namespace GeekLearning.Storage.Configuration +{ + public interface IProviderInstanceOptions : INamedElementOptions + { + string Type { get; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/IScopedStoreOptions.cs b/src/GeekLearning.Storage/Configuration/IScopedStoreOptions.cs new file mode 100644 index 0000000..52aeed9 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/IScopedStoreOptions.cs @@ -0,0 +1,7 @@ +namespace GeekLearning.Storage.Configuration +{ + public interface IScopedStoreOptions : IStoreOptions + { + string FolderNameFormat { get; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/IStoreOptions.cs b/src/GeekLearning.Storage/Configuration/IStoreOptions.cs new file mode 100644 index 0000000..099b390 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/IStoreOptions.cs @@ -0,0 +1,17 @@ +namespace GeekLearning.Storage.Configuration +{ + using System.Collections.Generic; + + public interface IStoreOptions : INamedElementOptions + { + string ProviderName { get; set; } + + string ProviderType { get; set; } + + AccessLevel AccessLevel { get; set; } + + string FolderName { get; set; } + + IEnumerable Validate(bool throwOnError = true); + } +} diff --git a/src/GeekLearning.Storage/Configuration/OptionError.cs b/src/GeekLearning.Storage/Configuration/OptionError.cs new file mode 100644 index 0000000..b8447c3 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/OptionError.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.Configuration +{ + public class OptionError : IOptionError + { + public string PropertyName { get; set; } + + public string ErrorMessage { get; set; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/ProviderInstanceOptions.cs b/src/GeekLearning.Storage/Configuration/ProviderInstanceOptions.cs new file mode 100644 index 0000000..ae0e77c --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/ProviderInstanceOptions.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage.Configuration +{ + public class ProviderInstanceOptions : IProviderInstanceOptions + { + public string Name { get; set; } + + public string Type { get; set; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/ScopedStoreOptions.cs b/src/GeekLearning.Storage/Configuration/ScopedStoreOptions.cs new file mode 100644 index 0000000..8dd4fe2 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/ScopedStoreOptions.cs @@ -0,0 +1,7 @@ +namespace GeekLearning.Storage.Configuration +{ + public class ScopedStoreOptions : StoreOptions, IScopedStoreOptions + { + public string FolderNameFormat { get; set; } + } +} diff --git a/src/GeekLearning.Storage/Configuration/StorageOptions.cs b/src/GeekLearning.Storage/Configuration/StorageOptions.cs new file mode 100644 index 0000000..9d02e23 --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/StorageOptions.cs @@ -0,0 +1,49 @@ +namespace GeekLearning.Storage.Configuration +{ + using Microsoft.Extensions.Configuration; + using System; + using System.Collections.Generic; + + public class StorageOptions : IParsedOptions + { + public const string DefaultConfigurationSectionName = "Storage"; + + private readonly Lazy> parsedProviderInstances; + private readonly Lazy> parsedStores; + private readonly Lazy> parsedScopedStores; + + public StorageOptions() + { + this.parsedProviderInstances = new Lazy>( + () => this.Providers.Parse()); + this.parsedStores = new Lazy>( + () => this.Stores.Parse()); + this.parsedScopedStores = new Lazy>( + () => this.ScopedStores.Parse()); + } + + public string Name => DefaultConfigurationSectionName; + + public IReadOnlyDictionary Providers { get; set; } + + public IReadOnlyDictionary Stores { get; set; } + + public IReadOnlyDictionary ScopedStores { get; set; } + + public IReadOnlyDictionary ConnectionStrings { get; set; } + + public IReadOnlyDictionary ParsedProviderInstances { get => this.parsedProviderInstances.Value; set { } } + + public IReadOnlyDictionary ParsedStores { get => this.parsedStores.Value; set { } } + + public IReadOnlyDictionary ParsedScopedStores { get => this.parsedScopedStores.Value; set { } } + + public void BindProviderInstanceOptions(ProviderInstanceOptions providerInstanceOptions) + { + } + + public void BindStoreOptions(StoreOptions storeOptions, ProviderInstanceOptions providerInstanceOptions) + { + } + } +} diff --git a/src/GeekLearning.Storage/Configuration/StoreOptions.cs b/src/GeekLearning.Storage/Configuration/StoreOptions.cs new file mode 100644 index 0000000..66f292c --- /dev/null +++ b/src/GeekLearning.Storage/Configuration/StoreOptions.cs @@ -0,0 +1,61 @@ +namespace GeekLearning.Storage.Configuration +{ + using System; + using System.Collections.Generic; + using System.Linq; + + public class StoreOptions : IStoreOptions + { + private const string MissingPropertyErrorMessage = "{0} should be defined."; + + public string Name { get; set; } + + public string ProviderName { get; set; } + + public string ProviderType { get; set; } + + public AccessLevel AccessLevel { get; set; } + + public string FolderName { get; set; } + + public virtual IEnumerable Validate(bool throwOnError = true) + { + var optionErrors = new List(); + + if (string.IsNullOrEmpty(this.Name)) + { + this.PushMissingPropertyError(optionErrors, nameof(this.Name)); + } + + if (string.IsNullOrEmpty(this.ProviderName) && string.IsNullOrEmpty(this.ProviderType)) + { + optionErrors.Add(new OptionError + { + PropertyName = "Provider", + ErrorMessage = $"You should set either a {nameof(this.ProviderType)} or a {nameof(this.ProviderName)} for each Store." + }); + } + + if (string.IsNullOrEmpty(this.FolderName)) + { + this.PushMissingPropertyError(optionErrors, nameof(this.FolderName)); + } + + if (throwOnError && optionErrors.Any()) + { + throw new Exceptions.BadStoreConfiguration(this.Name, optionErrors); + } + + return optionErrors; + } + + protected void PushMissingPropertyError(List optionErrors, string propertyName) + { + optionErrors.Add(new OptionError + { + PropertyName = propertyName, + ErrorMessage = string.Format(MissingPropertyErrorMessage, propertyName) + }); + } + } +} diff --git a/src/GeekLearning.Storage/Exceptions/BadProviderConfiguration.cs b/src/GeekLearning.Storage/Exceptions/BadProviderConfiguration.cs new file mode 100644 index 0000000..95a38a8 --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/BadProviderConfiguration.cs @@ -0,0 +1,17 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + + public class BadProviderConfiguration : Exception + { + public BadProviderConfiguration(string providerName) + : base($"The provider '{providerName}' was not properly configured.") + { + } + + public BadProviderConfiguration(string providerName, string details) + : base($"The providerName '{providerName}' was not properly configured. {details}") + { + } + } +} diff --git a/src/GeekLearning.Storage/Exceptions/BadScopedStoreConfiguration.cs b/src/GeekLearning.Storage/Exceptions/BadScopedStoreConfiguration.cs new file mode 100644 index 0000000..508bf3c --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/BadScopedStoreConfiguration.cs @@ -0,0 +1,22 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + + public class BadScopedStoreConfiguration : Exception + { + public BadScopedStoreConfiguration(string storeName) + : base($"The scoped store '{storeName}' was not properly configured.") + { + } + + public BadScopedStoreConfiguration(string storeName, string details) + : base($"The scoped store '{storeName}' was not properly configured. {details}") + { + } + + public BadScopedStoreConfiguration(string storeName, string details, Exception innerException) + : base($"The scoped store '{storeName}' was not properly configured. {details}", innerException) + { + } + } +} diff --git a/src/GeekLearning.Storage/Exceptions/BadStoreConfiguration.cs b/src/GeekLearning.Storage/Exceptions/BadStoreConfiguration.cs new file mode 100644 index 0000000..caae3eb --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/BadStoreConfiguration.cs @@ -0,0 +1,27 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + using System.Collections.Generic; + using System.Linq; + + public class BadStoreConfiguration : Exception + { + public BadStoreConfiguration(string storeName) + : base($"The store '{storeName}' was not properly configured.") + { + } + + public BadStoreConfiguration(string storeName, string details) + : base($"The store '{storeName}' was not properly configured. {details}") + { + } + + public BadStoreConfiguration(string storeName, IEnumerable errors) + : this(storeName, string.Join(" | ", errors.Select(e => e.ErrorMessage))) + { + this.Errors = errors; + } + + public IEnumerable Errors { get; } + } +} diff --git a/src/GeekLearning.Storage/Exceptions/BadStoreProviderException.cs b/src/GeekLearning.Storage/Exceptions/BadStoreProviderException.cs new file mode 100644 index 0000000..a062288 --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/BadStoreProviderException.cs @@ -0,0 +1,12 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + + public class BadStoreProviderException : Exception + { + public BadStoreProviderException(string providerName, string storeName) + : base($"The store '{storeName}' was not configured with the provider '{providerName}'. Unable to build it.") + { + } + } +} diff --git a/src/GeekLearning.Storage/Exceptions/ProviderNotFoundException.cs b/src/GeekLearning.Storage/Exceptions/ProviderNotFoundException.cs new file mode 100644 index 0000000..af57f98 --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/ProviderNotFoundException.cs @@ -0,0 +1,12 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + + public class ProviderNotFoundException : Exception + { + public ProviderNotFoundException(string providerName) + : base($"The configured provider '{providerName}' was not found. Did you forget to register providers in your Startup.ConfigureServices?") + { + } + } +} diff --git a/src/GeekLearning.Storage/Exceptions/StoreNotFoundException.cs b/src/GeekLearning.Storage/Exceptions/StoreNotFoundException.cs new file mode 100644 index 0000000..b93f841 --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/StoreNotFoundException.cs @@ -0,0 +1,12 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + + public class StoreNotFoundException : Exception + { + public StoreNotFoundException(string storeName) + : base($"The configured store '{storeName}' was not found. Did you configure it properly in your appsettings.json?") + { + } + } +} diff --git a/src/GeekLearning.Storage/GeekLearning.Storage.csproj b/src/GeekLearning.Storage/GeekLearning.Storage.csproj index e61058f..2e70721 100644 --- a/src/GeekLearning.Storage/GeekLearning.Storage.csproj +++ b/src/GeekLearning.Storage/GeekLearning.Storage.csproj @@ -11,8 +11,10 @@ - - + + + + diff --git a/src/GeekLearning.Storage/IStorageFactory.cs b/src/GeekLearning.Storage/IStorageFactory.cs index 4bf8398..48ccad4 100644 --- a/src/GeekLearning.Storage/IStorageFactory.cs +++ b/src/GeekLearning.Storage/IStorageFactory.cs @@ -1,11 +1,15 @@ namespace GeekLearning.Storage { + using Configuration; + public interface IStorageFactory { - IStore GetStore(string storeName, IStorageStoreOptions configuration); + IStore GetStore(string storeName, IStoreOptions configuration); IStore GetStore(string storeName); + IStore GetScopedStore(string storeName, params object[] args); + bool TryGetStore(string storeName, out IStore store); bool TryGetStore(string storeName, out IStore store, string provider); diff --git a/src/GeekLearning.Storage/IStorageProvider.cs b/src/GeekLearning.Storage/IStorageProvider.cs index 5ac5670..353be9f 100644 --- a/src/GeekLearning.Storage/IStorageProvider.cs +++ b/src/GeekLearning.Storage/IStorageProvider.cs @@ -1,9 +1,15 @@ namespace GeekLearning.Storage { + using Configuration; + public interface IStorageProvider { string Name { get; } - IStore BuildStore(string storeName, IStorageStoreOptions storeOptions); + IStore BuildStore(string storeName); + + IStore BuildStore(string storeName, IStoreOptions storeOptions); + + IStore BuildScopedStore(string storeName, params object[] args); } } diff --git a/src/GeekLearning.Storage/IStorageStoreOptions.cs b/src/GeekLearning.Storage/IStorageStoreOptions.cs deleted file mode 100644 index 4807f7b..0000000 --- a/src/GeekLearning.Storage/IStorageStoreOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace GeekLearning.Storage -{ - using System.Collections.Generic; - - public interface IStorageStoreOptions - { - string Provider { get; } - - Dictionary Parameters { get; } - } -} diff --git a/src/GeekLearning.Storage/IStore.cs b/src/GeekLearning.Storage/IStore.cs index 889d16f..eb869de 100644 --- a/src/GeekLearning.Storage/IStore.cs +++ b/src/GeekLearning.Storage/IStore.cs @@ -8,6 +8,8 @@ public interface IStore { string Name { get; } + Task InitAsync(); + Task ListAsync(string path, bool recursive, bool withMetadata); Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata); diff --git a/src/GeekLearning.Storage/IStoreExtensions.cs b/src/GeekLearning.Storage/IStoreExtensions.cs index 36fa987..e6880c2 100644 --- a/src/GeekLearning.Storage/IStoreExtensions.cs +++ b/src/GeekLearning.Storage/IStoreExtensions.cs @@ -6,48 +6,30 @@ public static class IStoreExtensions { public static Task ListAsync(this IStore store, string path, bool recursive = false, bool withMetadata = false) - { - return store.ListAsync(path, recursive: recursive, withMetadata: withMetadata); - } + => store.ListAsync(path, recursive: recursive, withMetadata: withMetadata); public static Task ListAsync(this IStore store, string path, string searchPattern, bool recursive = false, bool withMetadata = false) - { - return store.ListAsync(path, searchPattern, recursive: recursive, withMetadata: withMetadata); - } + => store.ListAsync(path, searchPattern, recursive: recursive, withMetadata: withMetadata); public static Task DeleteAsync(this IStore store, string path) - { - return store.DeleteAsync(new Internal.PrivateFileReference(path)); - } + => store.DeleteAsync(new Internal.PrivateFileReference(path)); public static Task GetAsync(this IStore store, string path, bool withMetadata = false) - { - return store.GetAsync(new Internal.PrivateFileReference(path), withMetadata: withMetadata); - } + => store.GetAsync(new Internal.PrivateFileReference(path), withMetadata: withMetadata); public static Task ReadAsync(this IStore store, string path) - { - return store.ReadAsync(new Internal.PrivateFileReference(path)); - } + => store.ReadAsync(new Internal.PrivateFileReference(path)); public static Task ReadAllBytesAsync(this IStore store, string path) - { - return store.ReadAllBytesAsync(new Internal.PrivateFileReference(path)); - } + => store.ReadAllBytesAsync(new Internal.PrivateFileReference(path)); public static Task ReadAllTextAsync(this IStore store, string path) - { - return store.ReadAllTextAsync(new Internal.PrivateFileReference(path)); - } + => store.ReadAllTextAsync(new Internal.PrivateFileReference(path)); public static Task SaveAsync(this IStore store, byte[] data, string path, string contentType) - { - return store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); - } + => store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); public static Task SaveAsync(this IStore store, Stream data, string path, string contentType) - { - return store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); - } + => store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); } } diff --git a/src/GeekLearning.Storage/IStore{TOptions}.cs b/src/GeekLearning.Storage/IStore{TOptions}.cs index 709f787..c278242 100644 --- a/src/GeekLearning.Storage/IStore{TOptions}.cs +++ b/src/GeekLearning.Storage/IStore{TOptions}.cs @@ -1,7 +1,9 @@ namespace GeekLearning.Storage { + using Configuration; + public interface IStore : IStore - where TOptions : class, IStorageStoreOptions, new() + where TOptions : class, IStoreOptions, new() { } } diff --git a/src/GeekLearning.Storage/Internal/ConfigureProviderOptions.cs b/src/GeekLearning.Storage/Internal/ConfigureProviderOptions.cs new file mode 100644 index 0000000..556b92e --- /dev/null +++ b/src/GeekLearning.Storage/Internal/ConfigureProviderOptions.cs @@ -0,0 +1,59 @@ +namespace GeekLearning.Storage.Internal +{ + using GeekLearning.Storage.Configuration; + using Microsoft.Extensions.Options; + using System.Linq; + + public class ConfigureProviderOptions : IConfigureOptions + where TParsedOptions : class, IParsedOptions + where TInstanceOptions : class, IProviderInstanceOptions, new() + where TStoreOptions : class, IStoreOptions, new() + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions, new() + { + private readonly StorageOptions storageOptions; + + public ConfigureProviderOptions(IOptions storageOptions) + { + this.storageOptions = storageOptions.Value; + } + + public void Configure(TParsedOptions options) + { + if (this.storageOptions == null) + { + return; + } + + options.ConnectionStrings = this.storageOptions.ConnectionStrings; + + options.ParsedProviderInstances = this.storageOptions.Providers.Parse() + .Where(kvp => kvp.Value.Type == options.Name) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + foreach (var parsedProviderInstance in options.ParsedProviderInstances) + { + parsedProviderInstance.Value.Compute(options); + } + + var parsedStores = this.storageOptions.Stores.Parse(); + foreach (var parsedStore in parsedStores) + { + parsedStore.Value.Compute(options); + } + + options.ParsedStores = parsedStores + .Where(kvp => kvp.Value.ProviderType == options.Name) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + var parsedScopedStores = this.storageOptions.ScopedStores.Parse(); + foreach (var parsedScopedStore in parsedScopedStores) + { + parsedScopedStore.Value.Compute(options); + } + + options.ParsedScopedStores = parsedScopedStores + .Where(kvp => kvp.Value.ProviderType == options.Name) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + } +} diff --git a/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs b/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs index 228bea9..8dd2508 100644 --- a/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs +++ b/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs @@ -1,76 +1,48 @@ namespace GeekLearning.Storage.Internal { + using Configuration; using Microsoft.Extensions.Options; using System; using System.IO; using System.Threading.Tasks; public class GenericStoreProxy : IStore, IStore - where TOptions : class, IStorageStoreOptions, new() + where TOptions : class, IStoreOptions, new() { private IStore innerStore; public GenericStoreProxy(IStorageFactory factory, IOptions options) { - this.innerStore = factory.GetStore(nameof(TOptions), options.Value); - } - - public string Name - { - get + if (options == null) { - return innerStore.Name; + throw new ArgumentNullException("options", "Unable to build generic Store. Did you forget to configure your options?"); } - } - public Task DeleteAsync(IPrivateFileReference file) - { - return innerStore.DeleteAsync(file); + this.innerStore = factory.GetStore(nameof(TOptions), options.Value); } - public Task GetAsync(Uri file, bool withMetadata) - { - return innerStore.GetAsync(file, withMetadata); - } + public string Name => this.innerStore.Name; - public Task GetAsync(IPrivateFileReference file, bool withMetadata) - { - return innerStore.GetAsync(file, withMetadata); - } + public Task InitAsync() => this.innerStore.InitAsync(); - public Task ListAsync(string path, bool recursive, bool withMetadata) - { - return innerStore.ListAsync(path, recursive, withMetadata); - } + public Task DeleteAsync(IPrivateFileReference file) => this.innerStore.DeleteAsync(file); - public Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) - { - return innerStore.ListAsync(path, searchPattern, recursive, withMetadata); - } + public Task GetAsync(Uri file, bool withMetadata) => this.innerStore.GetAsync(file, withMetadata); - public Task ReadAllBytesAsync(IPrivateFileReference file) - { - return innerStore.ReadAllBytesAsync(file); - } + public Task GetAsync(IPrivateFileReference file, bool withMetadata) => this.innerStore.GetAsync(file, withMetadata); - public Task ReadAllTextAsync(IPrivateFileReference file) - { - return innerStore.ReadAllTextAsync(file); - } + public Task ListAsync(string path, bool recursive, bool withMetadata) => this.innerStore.ListAsync(path, recursive, withMetadata); - public Task ReadAsync(IPrivateFileReference file) - { - return innerStore.ReadAsync(file); - } + public Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) => this.innerStore.ListAsync(path, searchPattern, recursive, withMetadata); - public Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) - { - return innerStore.SaveAsync(data, file, contentType); - } + public Task ReadAllBytesAsync(IPrivateFileReference file) => this.innerStore.ReadAllBytesAsync(file); - public Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) - { - return innerStore.SaveAsync(data, file, contentType); - } + public Task ReadAllTextAsync(IPrivateFileReference file) => this.innerStore.ReadAllTextAsync(file); + + public Task ReadAsync(IPrivateFileReference file) => this.innerStore.ReadAsync(file); + + public Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) => this.innerStore.SaveAsync(data, file, contentType); + + public Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) => this.innerStore.SaveAsync(data, file, contentType); } } diff --git a/src/GeekLearning.Storage/Internal/StorageFactory.cs b/src/GeekLearning.Storage/Internal/StorageFactory.cs index 609e566..8ff8cc4 100644 --- a/src/GeekLearning.Storage/Internal/StorageFactory.cs +++ b/src/GeekLearning.Storage/Internal/StorageFactory.cs @@ -1,52 +1,62 @@ namespace GeekLearning.Storage.Internal { + using GeekLearning.Storage.Configuration; using Microsoft.Extensions.Options; using System.Collections.Generic; using System.Linq; public class StorageFactory : IStorageFactory { - private IOptions options; - private IEnumerable storageProviders; + private StorageOptions options; + private IReadOnlyDictionary storageProviders; public StorageFactory(IEnumerable storageProviders, IOptions options) { - this.storageProviders = storageProviders; - this.options = options; + this.storageProviders = storageProviders.ToDictionary(sp => sp.Name, sp => sp); + this.options = options.Value; } - public IStore GetStore(string storeName, IStorageStoreOptions configuration) + public IStore GetStore(string storeName, IStoreOptions configuration) { - return this.storageProviders.FirstOrDefault(x => x.Name == configuration.Provider).BuildStore(storeName, configuration); + return this.GetProvider(configuration).BuildStore(storeName, configuration); } public IStore GetStore(string storeName) { - var conf = this.options.Value.Stores[storeName]; - return this.storageProviders.FirstOrDefault(x => x.Name == conf.Provider).BuildStore(storeName, conf); + return this.GetProvider(this.options.GetStoreConfiguration(storeName)).BuildStore(storeName); + } + + public IStore GetScopedStore(string storeName, params object[] args) + { + return this.GetProvider(this.options.GetScopedStoreConfiguration(storeName)).BuildScopedStore(storeName, args); } public bool TryGetStore(string storeName, out IStore store) { - StorageOptions.StorageStoreOptions conf; - if (this.options.Value.Stores.TryGetValue(storeName, out conf)) + var configuration = this.options.GetStoreConfiguration(storeName, throwIfNotFound: false); + if (configuration != null) { - store = this.storageProviders.FirstOrDefault(x => x.Name == conf.Provider).BuildStore(storeName, conf); - return true; + var provider = this.GetProvider(configuration, throwIfNotFound: false); + if (provider != null) + { + store = provider.BuildStore(storeName); + return true; + } } store = null; return false; } - public bool TryGetStore(string storeName, out IStore store, string provider) + public bool TryGetStore(string storeName, out IStore store, string providerName) { - StorageOptions.StorageStoreOptions conf; - if (this.options.Value.Stores.TryGetValue(storeName, out conf)) + var configuration = this.options.GetStoreConfiguration(storeName, throwIfNotFound: false); + if (configuration != null) { - if (provider == conf.Provider) + var provider = this.GetProvider(configuration, throwIfNotFound: false); + if (provider != null && provider.Name == providerName) { - store = this.storageProviders.FirstOrDefault(x => x.Name == conf.Provider).BuildStore(storeName, conf); + store = provider.BuildStore(storeName); return true; } } @@ -54,5 +64,43 @@ public bool TryGetStore(string storeName, out IStore store, string provider) store = null; return false; } + + private IStorageProvider GetProvider(IStoreOptions configuration, bool throwIfNotFound = true) + { + string providerTypeName = null; + if (!string.IsNullOrEmpty(configuration.ProviderType)) + { + providerTypeName = configuration.ProviderType; + } + else if (!string.IsNullOrEmpty(configuration.ProviderName)) + { + this.options.ParsedProviderInstances.TryGetValue(configuration.ProviderName, out var providerInstanceOptions); + if (providerInstanceOptions != null) + { + providerTypeName = providerInstanceOptions.Type; + } + else if (throwIfNotFound) + { + throw new Exceptions.BadProviderConfiguration(configuration.ProviderName, "Unable to find it in the configuration."); + } + } + else if (throwIfNotFound) + { + throw new Exceptions.BadStoreConfiguration(configuration.Name, "You have to set either 'ProviderType' or 'ProviderName' on Store configuration."); + } + + if (string.IsNullOrEmpty(providerTypeName)) + { + return null; + } + + this.storageProviders.TryGetValue(providerTypeName, out var provider); + if (provider == null && throwIfNotFound) + { + throw new Exceptions.ProviderNotFoundException(providerTypeName); + } + + return provider; + } } } diff --git a/src/GeekLearning.Storage/Internal/StorageProviderBase.cs b/src/GeekLearning.Storage/Internal/StorageProviderBase.cs new file mode 100644 index 0000000..96acdaa --- /dev/null +++ b/src/GeekLearning.Storage/Internal/StorageProviderBase.cs @@ -0,0 +1,57 @@ +namespace GeekLearning.Storage.Internal +{ + using System; + using Configuration; + using Microsoft.Extensions.Options; + + public abstract class StorageProviderBase : IStorageProvider + where TParsedOptions : class, IParsedOptions, new() + where TInstanceOptions : class, IProviderInstanceOptions, new() + where TStoreOptions : class, IStoreOptions, new() + where TScopedStoreOptions : class, TStoreOptions, IScopedStoreOptions + { + protected readonly TParsedOptions options; + + public StorageProviderBase(IOptions options) + { + this.options = options.Value; + } + + public abstract string Name { get; } + + public IStore BuildStore(string storeName) + { + return this.BuildStoreInternal(storeName, this.options.GetStoreConfiguration(storeName)); + } + + public IStore BuildStore(string storeName, IStoreOptions storeOptions) + { + if (storeOptions.ProviderType != this.Name) + { + throw new Exceptions.BadStoreProviderException(this.Name, storeName); + } + + return this.BuildStoreInternal( + storeName, + storeOptions.ParseStoreOptions(options)); + } + + public IStore BuildScopedStore(string storeName, params object[] args) + { + var scopedStoreOptions = this.options.GetScopedStoreConfiguration(storeName); + + try + { + scopedStoreOptions.FolderName = string.Format(scopedStoreOptions.FolderNameFormat, args); + } + catch (Exception ex) + { + throw new Exceptions.BadScopedStoreConfiguration(storeName, "Cannot format folder name. See InnerException for details.", ex); + } + + return this.BuildStoreInternal(storeName, scopedStoreOptions.ParseStoreOptions(options)); + } + + protected abstract IStore BuildStoreInternal(string storeName, TStoreOptions storeOptions); + } +} diff --git a/src/GeekLearning.Storage/StorageExtensions.cs b/src/GeekLearning.Storage/StorageExtensions.cs deleted file mode 100644 index 7fabe92..0000000 --- a/src/GeekLearning.Storage/StorageExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace GeekLearning.Storage -{ - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - - public static class StorageExtensions - { - public static IServiceCollection AddStorage(this IServiceCollection services) - { - services.TryAddTransient(); - services.TryAdd(ServiceDescriptor.Transient(typeof(IStore<>), typeof(Internal.GenericStoreProxy<>))); - return services; - } - } -} diff --git a/src/GeekLearning.Storage/StorageOptions.cs b/src/GeekLearning.Storage/StorageOptions.cs deleted file mode 100644 index f26e4cf..0000000 --- a/src/GeekLearning.Storage/StorageOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace GeekLearning.Storage -{ - using System.Collections.Generic; - - public class StorageOptions - { - public Dictionary Stores { get; set; } - - public class StorageStoreOptions : IStorageStoreOptions - { - public string Provider { get; set; } - - public Dictionary Parameters { get; set; } - } - } -} diff --git a/src/GeekLearning.Storage/StorageServiceCollectionExtensions.cs b/src/GeekLearning.Storage/StorageServiceCollectionExtensions.cs new file mode 100644 index 0000000..073d149 --- /dev/null +++ b/src/GeekLearning.Storage/StorageServiceCollectionExtensions.cs @@ -0,0 +1,48 @@ +namespace GeekLearning.Storage +{ + using Configuration; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using System.Collections.Generic; + using System.Linq; + + public static class StorageServiceCollectionExtensions + { + public static IServiceCollection AddStorage(this IServiceCollection services) + { + services.TryAddTransient(); + services.TryAdd(ServiceDescriptor.Transient(typeof(IStore<>), typeof(Internal.GenericStoreProxy<>))); + return services; + } + + public static IServiceCollection AddStorage(this IServiceCollection services, IConfigurationSection configurationSection) + { + return services + .Configure(configurationSection) + .AddStorage(); + } + + public static IServiceCollection AddStorage(this IServiceCollection services, IConfigurationRoot configurationRoot) + { + return services + .Configure(configurationRoot.GetSection(StorageOptions.DefaultConfigurationSectionName)) + .Configure(storageOptions => + { + var connectionStrings = new Dictionary(); + ConfigurationBinder.Bind(configurationRoot.GetSection("ConnectionStrings"), connectionStrings); + + if (storageOptions.ConnectionStrings != null) + { + foreach (var existingConnectionString in storageOptions.ConnectionStrings) + { + connectionStrings[existingConnectionString.Key] = existingConnectionString.Value; + } + } + + storageOptions.ConnectionStrings = connectionStrings; + }) + .AddStorage(); + } + } +} diff --git a/src/GeekLearning.Storage/StoreBase.cs b/src/GeekLearning.Storage/StoreBase.cs index 86ea12f..23a38ed 100644 --- a/src/GeekLearning.Storage/StoreBase.cs +++ b/src/GeekLearning.Storage/StoreBase.cs @@ -9,6 +9,6 @@ public StoreBase(string storeName, IStorageFactory storageFactory) this.store = storageFactory.GetStore(storeName); } - public IStore Store { get { return this.store; } } + public IStore Store => this.store; } } diff --git a/tests/GeekLearning.Storage.Integration.Test/DeleteTests.cs b/tests/GeekLearning.Storage.Integration.Test/DeleteTests.cs index 2ddf857..da582bb 100644 --- a/tests/GeekLearning.Storage.Integration.Test/DeleteTests.cs +++ b/tests/GeekLearning.Storage.Integration.Test/DeleteTests.cs @@ -16,7 +16,7 @@ public DeleteTests(StoresFixture fixture) this.storeFixture = fixture; } - [Theory(DisplayName = nameof(Delete)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(Delete)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task Delete(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); diff --git a/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.csproj b/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.csproj index 9dd8e42..2a5b84e 100644 --- a/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.csproj +++ b/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.csproj @@ -6,7 +6,7 @@ GeekLearning.Storage.Integration.Test true $(PackageTargetFallback);dotnet;portable-net45+win8 - 1.0.4 + 1.1.2 @@ -23,18 +23,18 @@ - + - - - + + + - - - - - - + + + + + + diff --git a/tests/GeekLearning.Storage.Integration.Test/GenericIStoreTests.cs b/tests/GeekLearning.Storage.Integration.Test/GenericIStoreTests.cs index 94299db..5a813a3 100644 --- a/tests/GeekLearning.Storage.Integration.Test/GenericIStoreTests.cs +++ b/tests/GeekLearning.Storage.Integration.Test/GenericIStoreTests.cs @@ -7,7 +7,7 @@ using Xunit; [Collection(nameof(IntegrationCollection))] - [Trait("Kind", "Integration")] + [Trait("Operation", "GenericIStore"), Trait("Kind", "Integration")] public class GenericIStoreTests { private StoresFixture storeFixture; diff --git a/tests/GeekLearning.Storage.Integration.Test/ListTests.cs b/tests/GeekLearning.Storage.Integration.Test/ListTests.cs index 1c1c0d1..99b6615 100644 --- a/tests/GeekLearning.Storage.Integration.Test/ListTests.cs +++ b/tests/GeekLearning.Storage.Integration.Test/ListTests.cs @@ -17,7 +17,7 @@ public ListTests(StoresFixture fixture) this.storeFixture = fixture; } - [Theory(DisplayName = nameof(ListRootFiles)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ListRootFiles)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ListRootFiles(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -36,7 +36,7 @@ public async Task ListRootFiles(string storeName) Assert.Empty(unexpectedFiles); } - [Theory(DisplayName = nameof(ListEmptyPathFiles)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ListEmptyPathFiles)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ListEmptyPathFiles(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -55,7 +55,7 @@ public async Task ListEmptyPathFiles(string storeName) Assert.Empty(unexpectedFiles); } - [Theory(DisplayName = nameof(ListSubDirectoryFiles)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ListSubDirectoryFiles)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ListSubDirectoryFiles(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -74,7 +74,7 @@ public async Task ListSubDirectoryFiles(string storeName) Assert.Empty(unexpectedFiles); } - [Theory(DisplayName = nameof(ListSubDirectoryFilesWithTrailingSlash)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ListSubDirectoryFilesWithTrailingSlash)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ListSubDirectoryFilesWithTrailingSlash(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -93,7 +93,7 @@ public async Task ListSubDirectoryFilesWithTrailingSlash(string storeName) Assert.Empty(unexpectedFiles); } - [Theory(DisplayName = nameof(ExtensionGlobbing)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ExtensionGlobbing)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ExtensionGlobbing(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -112,7 +112,7 @@ public async Task ExtensionGlobbing(string storeName) Assert.Empty(unexpectedFiles); } - [Theory(DisplayName = nameof(FileNameGlobbing)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(FileNameGlobbing)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task FileNameGlobbing(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -131,7 +131,7 @@ public async Task FileNameGlobbing(string storeName) Assert.Empty(unexpectedFiles); } - [Theory(DisplayName = nameof(FileNameGlobbingAtRoot)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(FileNameGlobbingAtRoot)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task FileNameGlobbingAtRoot(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); diff --git a/tests/GeekLearning.Storage.Integration.Test/ReadTests.cs b/tests/GeekLearning.Storage.Integration.Test/ReadTests.cs index 9b45aa0..ed607fd 100644 --- a/tests/GeekLearning.Storage.Integration.Test/ReadTests.cs +++ b/tests/GeekLearning.Storage.Integration.Test/ReadTests.cs @@ -17,7 +17,7 @@ public ReadTests(StoresFixture fixture) this.storeFixture = fixture; } - [Theory(DisplayName = nameof(ReadAllTextFromRootFile)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ReadAllTextFromRootFile)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ReadAllTextFromRootFile(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -31,7 +31,7 @@ public async Task ReadAllTextFromRootFile(string storeName) Assert.Equal(expectedText, actualText); } - [Theory(DisplayName = nameof(ReadAllTextFromRootFile)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ReadAllTextFromRootFile)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ReadAllTextFromSubdirectoryFile(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -45,7 +45,7 @@ public async Task ReadAllTextFromSubdirectoryFile(string storeName) Assert.Equal(expectedText, actualText); } - [Theory(DisplayName = nameof(ReadAllBytesFromSubdirectoryFile)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ReadAllBytesFromSubdirectoryFile)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ReadAllBytesFromSubdirectoryFile(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -61,7 +61,7 @@ public async Task ReadAllBytesFromSubdirectoryFile(string storeName) } } - [Theory(DisplayName = nameof(ReadAllBytesFromSubdirectoryFileUsingFileReference)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ReadAllBytesFromSubdirectoryFileUsingFileReference)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ReadAllBytesFromSubdirectoryFileUsingFileReference(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -80,7 +80,7 @@ public async Task ReadAllBytesFromSubdirectoryFileUsingFileReference(string stor } - [Theory(DisplayName = nameof(ReadFileFromSubdirectoryFile)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ReadFileFromSubdirectoryFile)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ReadFileFromSubdirectoryFile(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -101,7 +101,7 @@ public async Task ReadFileFromSubdirectoryFile(string storeName) Assert.Equal(expectedText, actualText); } - [Theory(DisplayName = nameof(ReadAllTextFromSubdirectoryFileUsingFileReference)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ReadAllTextFromSubdirectoryFileUsingFileReference)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ReadAllTextFromSubdirectoryFileUsingFileReference(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -118,7 +118,7 @@ public async Task ReadAllTextFromSubdirectoryFileUsingFileReference(string store } - [Theory(DisplayName = nameof(ListThenReadAllTextFromSubdirectoryFile)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ListThenReadAllTextFromSubdirectoryFile)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ListThenReadAllTextFromSubdirectoryFile(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); diff --git a/tests/GeekLearning.Storage.Integration.Test/ScopedStoresTests.cs b/tests/GeekLearning.Storage.Integration.Test/ScopedStoresTests.cs new file mode 100644 index 0000000..ea01d2b --- /dev/null +++ b/tests/GeekLearning.Storage.Integration.Test/ScopedStoresTests.cs @@ -0,0 +1,41 @@ +namespace GeekLearning.Storage.Integration.Test +{ + using Microsoft.Extensions.DependencyInjection; + using Storage; + using System; + using System.Text; + using System.Threading.Tasks; + using Xunit; + + [Collection(nameof(IntegrationCollection))] + [Trait("Operation", "ScopedStores"), Trait("Kind", "Integration")] + public class ScopedStoresTests + { + private StoresFixture storeFixture; + + public ScopedStoresTests(StoresFixture fixture) + { + this.storeFixture = fixture; + } + + [Theory(DisplayName = nameof(ScopedStoreUpdate)), InlineData("ScopedStore1"), InlineData("ScopedStore2")] + public async Task ScopedStoreUpdate(string storeName) + { + var storageFactory = this.storeFixture.Services.GetRequiredService(); + + var formatArg = Guid.NewGuid(); + var store = storageFactory.GetScopedStore(storeName, formatArg); + + await store.InitAsync(); + + var textToWrite = "The answer is 42"; + var filePath = "Update/42.txt"; + + await store.SaveAsync(Encoding.UTF8.GetBytes(textToWrite), filePath, "text/plain"); + + var readFromWrittenFile = await store.ReadAllTextAsync(filePath); + + Assert.Equal(textToWrite, readFromWrittenFile); + } + } +} diff --git a/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs b/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs index 3d000ee..4017599 100644 --- a/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs +++ b/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs @@ -1,7 +1,9 @@ namespace GeekLearning.Storage.Integration.Test { + using GeekLearning.Storage.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; @@ -10,12 +12,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; + using GeekLearning.Storage.Azure.Configuration; + using GeekLearning.Storage.FileSystem.Configuration; public class StoresFixture : IDisposable { - private CloudStorageAccount cloudStorageAccount; - private CloudBlobContainer container; - public StoresFixture() { this.BasePath = PlatformServices.Default.Application.ApplicationBasePath; @@ -27,8 +28,10 @@ public StoresFixture() .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.development.json", optional: true) .AddInMemoryCollection(new KeyValuePair[] { - new KeyValuePair("Storage:Stores:azure:Parameters:Container", containerId), - new KeyValuePair("TestStore:Parameters:Container", containerId) + new KeyValuePair("Storage:Stores:Store3:FolderName", $"Store3-{containerId}"), + new KeyValuePair("Storage:Stores:Store4:FolderName", $"Store4-{containerId}"), + new KeyValuePair("Storage:Stores:Store5:FolderName", $"Store5-{containerId}"), + new KeyValuePair("Storage:Stores:Store6:FolderName", $"Store6-{containerId}"), }); this.Configuration = builder.Build(); @@ -37,7 +40,7 @@ public StoresFixture() services.AddOptions(); - services.AddStorage() + services.AddStorage(Configuration) .AddAzureStorage() .AddFileSystemStorage(this.FileSystemRootPath) .AddFileSystemExtendedProperties(); @@ -46,7 +49,10 @@ public StoresFixture() services.Configure(Configuration.GetSection("TestStore")); this.Services = services.BuildServiceProvider(); - + this.StorageOptions = this.Services.GetService>().Value; + this.AzureParsedOptions = this.Services.GetService>().Value; + this.FileSystemParsedOptions = this.Services.GetService>().Value; + this.TestStoreOptions = this.Services.GetService>().Value.ParseStoreOptions(this.FileSystemParsedOptions); ResetStores(); } @@ -58,6 +64,16 @@ public StoresFixture() public string FileSystemRootPath => Path.Combine(this.BasePath, "FileVault"); + public string FileSystemSecondaryRootPath => Path.Combine(this.BasePath, "FileVault2"); + + public StorageOptions StorageOptions { get; } + + public AzureParsedOptions AzureParsedOptions { get; } + + public FileSystemParsedOptions FileSystemParsedOptions { get; } + + public FileSystemStoreOptions TestStoreOptions { get; } + public void Dispose() { this.DeleteRootResources(); @@ -65,67 +81,92 @@ public void Dispose() private void DeleteRootResources() { - if (this.container != null) + foreach (var parsedStoreKvp in this.AzureParsedOptions.ParsedStores) { - this.container.DeleteIfExistsAsync().Wait(); + var cloudStorageAccount = CloudStorageAccount.Parse(parsedStoreKvp.Value.ConnectionString); + var client = cloudStorageAccount.CreateCloudBlobClient(); + var container = client.GetContainerReference(parsedStoreKvp.Value.FolderName); + + container.DeleteIfExistsAsync().Wait(); } if (Directory.Exists(this.FileSystemRootPath)) { Directory.Delete(this.FileSystemRootPath, true); } + + if (Directory.Exists(this.FileSystemSecondaryRootPath)) + { + Directory.Delete(this.FileSystemSecondaryRootPath, true); + } } private void ResetStores() { this.DeleteRootResources(); - this.ResetAzureStore(); - this.ResetFileSystemStore(); + this.ResetAzureStores(); + this.ResetFileSystemStores(); } - private void ResetFileSystemStore() + private void ResetFileSystemStores() { if (!Directory.Exists(this.FileSystemRootPath)) { Directory.CreateDirectory(this.FileSystemRootPath); } - var directoryName = Configuration["Storage:Stores:filesystem:Parameters:Path"]; + foreach (var parsedStoreKvp in this.FileSystemParsedOptions.ParsedStores) + { + ResetFileSystemStore(parsedStoreKvp.Key, parsedStoreKvp.Value.AbsolutePath); + } + + ResetFileSystemStore(this.TestStoreOptions.Name, this.TestStoreOptions.AbsolutePath); + } + + private void ResetFileSystemStore(string storeName, string absolutePath) + { var process = Process.Start(new ProcessStartInfo("robocopy.exe") { - Arguments = $"\"{Path.Combine(this.BasePath, "SampleDirectory")}\" \"{Path.Combine(this.FileSystemRootPath, directoryName)}\" /MIR" + Arguments = $"\"{Path.Combine(this.BasePath, "SampleDirectory")}\" \"{absolutePath}\" /MIR" }); if (!process.WaitForExit(30000)) { - throw new TimeoutException("File system store was not reset properly"); + process.Kill(); + throw new TimeoutException($"FileSystem Store '{storeName}' was not reset properly."); } } - private void ResetAzureStore() + private void ResetAzureStores() { var azCopy = Path.Combine( Environment.ExpandEnvironmentVariables(Configuration["AzCopyPath"]), "AzCopy.exe"); - cloudStorageAccount = CloudStorageAccount.Parse(Configuration["Storage:Stores:azure:Parameters:ConnectionString"]); - var key = cloudStorageAccount.Credentials.ExportBase64EncodedKey(); - var containerName = Configuration["Storage:Stores:azure:Parameters:Container"]; - var dest = cloudStorageAccount.BlobStorageUri.PrimaryUri.ToString() + containerName; + foreach (var parsedStoreKvp in this.AzureParsedOptions.ParsedStores) + { + var cloudStorageAccount = CloudStorageAccount.Parse(parsedStoreKvp.Value.ConnectionString); + var cloudStoragekey = cloudStorageAccount.Credentials.ExportBase64EncodedKey(); + var containerName = parsedStoreKvp.Value.FolderName; + + var dest = cloudStorageAccount.BlobStorageUri.PrimaryUri.ToString() + containerName; - var client = cloudStorageAccount.CreateCloudBlobClient(); + var client = cloudStorageAccount.CreateCloudBlobClient(); - this.container = client.GetContainerReference(containerName); - this.container.CreateAsync().Wait(); + var container = client.GetContainerReference(containerName); + container.CreateIfNotExistsAsync().Wait(); - var process = Process.Start(new ProcessStartInfo(azCopy) - { - Arguments = $"/Source:\"{Path.Combine(this.BasePath, "SampleDirectory")}\" /Dest:\"{dest}\" /DestKey:{key} /S" - }); + var arguments = $"/Source:\"{Path.Combine(this.BasePath, "SampleDirectory")}\" /Dest:\"{dest}\" /DestKey:{cloudStoragekey} /S /y"; + var process = Process.Start(new ProcessStartInfo(azCopy) + { + Arguments = arguments + }); - if (!process.WaitForExit(30000)) - { - throw new TimeoutException("Azure store was not reset properly"); + if (!process.WaitForExit(30000)) + { + process.Kill(); + throw new TimeoutException($"Azure Store '{parsedStoreKvp.Key}' was not reset properly."); + } } } } diff --git a/tests/GeekLearning.Storage.Integration.Test/TestStore.cs b/tests/GeekLearning.Storage.Integration.Test/TestStore.cs index 1df5a25..ab5c465 100644 --- a/tests/GeekLearning.Storage.Integration.Test/TestStore.cs +++ b/tests/GeekLearning.Storage.Integration.Test/TestStore.cs @@ -1,11 +1,30 @@ namespace GeekLearning.Storage.Integration.Test { + using GeekLearning.Storage.Configuration; using System.Collections.Generic; + using System.Linq; - public class TestStore : IStorageStoreOptions + public class TestStore : IStoreOptions { - public string Provider { get; set; } + public TestStore() + { + this.Name = "TestStore"; + this.ProviderType = "FileSystem"; + } - public Dictionary Parameters { get; set; } + public string ProviderName { get; set; } + + public string ProviderType { get; set; } + + public AccessLevel AccessLevel { get; set; } + + public string FolderName { get; set; } + + public string Name { get; set; } + + public IEnumerable Validate(bool throwOnError = true) + { + return Enumerable.Empty(); + } } } diff --git a/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs b/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs index 4d5124e..50958cb 100644 --- a/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs +++ b/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs @@ -20,7 +20,7 @@ public UpdateTests(StoresFixture fixture) this.storeFixture = fixture; } - [Theory(DisplayName = nameof(WriteAllText)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(WriteAllText)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task WriteAllText(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -36,7 +36,7 @@ public async Task WriteAllText(string storeName) Assert.Equal(textToWrite, readFromWrittenFile); } - [Theory(DisplayName = nameof(ETagShouldBeTheSameWithSameContent)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ETagShouldBeTheSameWithSameContent)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ETagShouldBeTheSameWithSameContent(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -51,7 +51,7 @@ public async Task ETagShouldBeTheSameWithSameContent(string storeName) Assert.Equal(savedReference.Properties.ETag, readReference.Properties.ETag); } - [Theory(DisplayName = nameof(ETagShouldBeDifferentWithDifferentContent)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ETagShouldBeDifferentWithDifferentContent)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ETagShouldBeDifferentWithDifferentContent(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -67,7 +67,7 @@ public async Task ETagShouldBeDifferentWithDifferentContent(string storeName) Assert.NotEqual(savedReference.Properties.ETag, updatedReference.Properties.ETag); } - [Theory(DisplayName = nameof(SaveStream)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(SaveStream)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task SaveStream(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -83,7 +83,7 @@ public async Task SaveStream(string storeName) Assert.Equal(textToWrite, readFromWrittenFile); } - [Theory(DisplayName = nameof(AddMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(AddMetatadaRoundtrip)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task AddMetatadaRoundtrip(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -107,7 +107,7 @@ public async Task AddMetatadaRoundtrip(string storeName) Assert.Equal(id, actualId); } - [Theory(DisplayName = nameof(SaveMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(SaveMetatadaRoundtrip)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task SaveMetatadaRoundtrip(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -131,7 +131,7 @@ public async Task SaveMetatadaRoundtrip(string storeName) Assert.Equal(id, actualId); } - [Theory(DisplayName = nameof(ListMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(ListMetatadaRoundtrip)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] public async Task ListMetatadaRoundtrip(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); diff --git a/tests/GeekLearning.Storage.Integration.Test/appsettings.json b/tests/GeekLearning.Storage.Integration.Test/appsettings.json index 2c8e21c..3876bad 100644 --- a/tests/GeekLearning.Storage.Integration.Test/appsettings.json +++ b/tests/GeekLearning.Storage.Integration.Test/appsettings.json @@ -1,26 +1,66 @@ { + "ConnectionStrings": { + "ConnectionStringFromAppSettings": "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;EndpointSuffix=core.windows.net" + }, + "AzCopyPath": "%ProgramFiles(x86)%\\Microsoft SDKs\\Azure\\AzCopy", + "Storage": { + + "Providers": { + "FirstAzure": { + "Type": "Azure", + "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;EndpointSuffix=core.windows.net" + }, + "AnotherAzure": { + "Type": "Azure", + "ConnectionStringName": "ConnectionStringFromAppSettings" + }, + "FirstFileSystem": { + "Type": "FileSystem" + }, + "AnotherFileSystem": { + "Type": "FileSystem", + "RootPath": "../FileVault2" + } + }, + "Stores": { - "filesystem": { - "Provider": "FileSystem", - "Parameters": { - "Path": "Templates" - } - }, - "azure": { - "Provider": "Azure", - "Parameters": { - "ConnectionString": "YourConnectionString", - "Container": "templates" - } + "Store1": { + "ProviderName": "FirstFileSystem" + }, + "Store2": { + "ProviderName": "FirstFileSystem", + "AccessLevel": "Public", + "FolderName": "AnotherPath" + }, + "Store3": { + "ProviderName": "FirstAzure", + "AccessLevel": "Private" + }, + "Store4": { + "ProviderType": "Azure", + "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;EndpointSuffix=core.windows.net" + }, + "Store5": { + "ProviderName": "AnotherAzure" + }, + "Store6": { + "ProviderType": "Azure", + "ConnectionStringName": "ConnectionStringFromAppSettings" + } + }, + + "ScopedStores": { + "ScopedStore1": { + "ProviderName": "AnotherFileSystem", + "FolderNameFormat": "AnotherPath-{0}" + }, + "ScopedStore2": { + "ProviderName": "AnotherAzure", + "AccessLevel": "Confidential", + "FolderNameFormat": "AnotherPath-{0}" } - } - }, - "TestStore": { - "Provider": "FileSystem", - "Parameters": { - "Path": "Templates" } } }