diff --git a/GeekLearning.Storage.sln b/GeekLearning.Storage.sln index 1c39f88..aa1ace3 100644 --- a/GeekLearning.Storage.sln +++ b/GeekLearning.Storage.sln @@ -1,18 +1,10 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26228.10 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage", "src\GeekLearning.Storage\GeekLearning.Storage.xproj", "{1F419C53-73C6-4460-B284-6A49AE41C596}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{FBAC4C17-D755-49A9-959D-18FD6B95B543}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage.Azure", "src\GeekLearning.Storage.Azure\GeekLearning.Storage.Azure.xproj", "{FD8BB8F9-9AF5-4C12-B962-9E08C30B01E2}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage.FileSystem", "src\GeekLearning.Storage.FileSystem\GeekLearning.Storage.FileSystem.xproj", "{4A12B042-76B3-471B-9235-F653E1ABE3C0}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage.BasicSample", "samples\GeekLearning.Storage.BasicSample\GeekLearning.Storage.BasicSample.xproj", "{63416AEA-DA51-4D62-B566-DB7D9BC800DC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2DAF5EF9-8F8E-4C51-BE2D-8D63CA143360}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{6BEB33C6-FA17-4F58-ACC3-83C1EB28B604}" @@ -20,18 +12,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{6BEB33C6 .gitattributes = .gitattributes .gitignore = .gitignore GitVersion.yml = GitVersion.yml - global.json = global.json LICENSE.md = LICENSE.md README.md = README.md EndProjectSection EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage.FileSystem.Server", "src\GeekLearning.Storage.FileSystem.Server\GeekLearning.Storage.FileSystem.Server.xproj", "{9D94CD6C-9451-449A-BED2-1C07D624A8E0}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{520AB1D3-C501-40FD-ACEB-7CC0D1F00B90}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage.Integration.Test", "tests\GeekLearning.Storage.Integration.Test\GeekLearning.Storage.Integration.Test.xproj", "{590B21B0-2AFA-4329-82AD-EF180C50EB5C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage", "src\GeekLearning.Storage\GeekLearning.Storage.csproj", "{1F419C53-73C6-4460-B284-6A49AE41C596}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage.Azure", "src\GeekLearning.Storage.Azure\GeekLearning.Storage.Azure.csproj", "{FD8BB8F9-9AF5-4C12-B962-9E08C30B01E2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage.FileSystem", "src\GeekLearning.Storage.FileSystem\GeekLearning.Storage.FileSystem.csproj", "{4A12B042-76B3-471B-9235-F653E1ABE3C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage.BasicSample", "samples\GeekLearning.Storage.BasicSample\GeekLearning.Storage.BasicSample.csproj", "{63416AEA-DA51-4D62-B566-DB7D9BC800DC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage.FileSystem.Server", "src\GeekLearning.Storage.FileSystem.Server\GeekLearning.Storage.FileSystem.Server.csproj", "{9D94CD6C-9451-449A-BED2-1C07D624A8E0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage.Integration.Test", "tests\GeekLearning.Storage.Integration.Test\GeekLearning.Storage.Integration.Test.csproj", "{590B21B0-2AFA-4329-82AD-EF180C50EB5C}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem", "src\GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem\GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.xproj", "{8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem", "src\GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem\GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.csproj", "{8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index 920c59c..bd1d1f9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -[![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)](https://www.nuget.org/packages/GeekLearning.Storage/) [![Build Status](https://geeklearning.visualstudio.com/_apis/public/build/definitions/f841b266-7595-4d01-9ee1-4864cf65aa73/27/badge)](#) # Geek Learning Cloud Storage Abstraction - + This library abstracts physical data storage in a way which allows you to transparently switch the underlying provider by configuration. @@ -26,3 +24,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/global.json b/global.json deleted file mode 100644 index 65e97c5..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "projects": [ "src", "tests", "samples" ], - "sdk": { - "version": "1.0.0-preview2-1-003177" - } -} \ No newline at end of file diff --git a/samples/GeekLearning.Storage.BasicSample/Controllers/SampleController.cs b/samples/GeekLearning.Storage.BasicSample/Controllers/SampleController.cs index 433bb5a..58c60fa 100644 --- a/samples/GeekLearning.Storage.BasicSample/Controllers/SampleController.cs +++ b/samples/GeekLearning.Storage.BasicSample/Controllers/SampleController.cs @@ -17,14 +17,14 @@ public SampleController(IStorageFactory storageFactory) } [HttpGet] - public async Task> Get() + public async ValueTask> Get() { var summaries = await this.sharedAssets.ListAsync("summaries", "*.txt", recursive: true, withMetadata: false); return summaries.Select(x => x.Path); } [HttpGet] - public async Task Get(string path) + public async ValueTask Get(string path) { var summary = await this.sharedAssets.GetAsync(path); return await summary.ReadAllTextAsync(); diff --git a/samples/GeekLearning.Storage.BasicSample/Controllers/ValuesController.cs b/samples/GeekLearning.Storage.BasicSample/Controllers/ValuesController.cs index affe249..1550312 100644 --- a/samples/GeekLearning.Storage.BasicSample/Controllers/ValuesController.cs +++ b/samples/GeekLearning.Storage.BasicSample/Controllers/ValuesController.cs @@ -16,13 +16,13 @@ public ValuesController(TemplatesStore templates) } [HttpGet] - public async Task> Get() + public async ValueTask> Get() { return new string[] { await templates.Store.ReadAllTextAsync("json.json"), "value2" }; } [HttpGet("files")] - public async Task> Get(int id) + public async ValueTask> Get(int id) { var files = await templates.Store.ListAsync(""); return files.Select(x => x.PublicUrl); diff --git a/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj b/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj new file mode 100644 index 0000000..ceb2ee2 --- /dev/null +++ b/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.csproj @@ -0,0 +1,40 @@ + + + + netcoreapp1.1 + true + GeekLearning.Storage.BasicSample + Exe + GeekLearning.Storage.BasicSample + 1.1.2 + $(PackageTargetFallback);portable-net45+win8 + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.xproj b/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.xproj deleted file mode 100644 index 1a43cd3..0000000 --- a/samples/GeekLearning.Storage.BasicSample/GeekLearning.Storage.BasicSample.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 63416aea-da51-4d62-b566-db7d9bc800dc - GeekLearning.Storage.BasicSample - .\obj - .\bin\ - v4.5.2 - - - 2.0 - - - 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/samples/GeekLearning.Storage.BasicSample/project.json b/samples/GeekLearning.Storage.BasicSample/project.json deleted file mode 100644 index cb6d25b..0000000 --- a/samples/GeekLearning.Storage.BasicSample/project.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.1.0", - "type": "platform" - }, - "Microsoft.AspNetCore.Mvc": "1.1.0", - "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", - "Microsoft.Extensions.Configuration.Json": "1.1.0", - "Microsoft.Extensions.Logging": "1.1.0", - "Microsoft.Extensions.Logging.Console": "1.1.0", - "Microsoft.Extensions.Logging.Debug": "1.1.0", - "Microsoft.Extensions.Options": "1.1.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", - - "GeekLearning.Storage": "*", - "GeekLearning.Storage.Azure": "*", - "GeekLearning.Storage.FileSystem": "*", - "GeekLearning.Storage.FileSystem.Server": "*" - }, - - "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" - }, - - "frameworks": { - "netcoreapp1.1": { - "imports": [ - "portable-net45+win8" - ] - } - }, - - "buildOptions": { - "emitEntryPoint": true, - "preserveCompilationContext": true - }, - - "runtimeOptions": { - "configProperties": { - "System.GC.Server": true - } - }, - - "publishOptions": { - "include": [ - "wwwroot", - "Views", - "appsettings.json", - "web.config", - "Templates" - ] - }, - - "scripts": { - "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] - } -} 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..e2d911a 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; @@ -7,34 +8,47 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using System.Security.Cryptography; using System.Threading.Tasks; 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) + public async ValueTask ListAsync(string path, bool recursive, bool withMetadata) { if (string.IsNullOrWhiteSpace(path)) { @@ -62,7 +76,7 @@ public async Task ListAsync(string path, bool recursive, bool return results.OfType().Select(blob => new Internal.AzureFileReference(blob, withMetadata: withMetadata)).ToArray(); } - public async Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) + public async ValueTask ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) { if (string.IsNullOrWhiteSpace(path)) { @@ -108,12 +122,12 @@ public async Task ListAsync(string path, string searchPattern, return filteredResults.Files.Select(x => pathMap[path + x.Path]).ToArray(); } - public async Task GetAsync(IPrivateFileReference file, bool withMetadata) + public async ValueTask GetAsync(IPrivateFileReference file, bool withMetadata) { return await this.InternalGetAsync(file, withMetadata); } - public async Task GetAsync(Uri uri, bool withMetadata) + public async ValueTask GetAsync(Uri uri, bool withMetadata) { return await this.InternalGetAsync(uri, withMetadata); } @@ -124,63 +138,130 @@ public async Task DeleteAsync(IPrivateFileReference file) await fileReference.DeleteAsync(); } - public async Task ReadAsync(IPrivateFileReference file) + public async ValueTask ReadAsync(IPrivateFileReference file) { var fileReference = await this.InternalGetAsync(file); return await fileReference.ReadInMemoryAsync(); } - public async Task ReadAllBytesAsync(IPrivateFileReference file) + public async ValueTask ReadAllBytesAsync(IPrivateFileReference file) { var fileReference = await this.InternalGetAsync(file); return await fileReference.ReadAllBytesAsync(); } - public async Task ReadAllTextAsync(IPrivateFileReference file) + public async ValueTask ReadAllTextAsync(IPrivateFileReference file) { var fileReference = await this.InternalGetAsync(file); return await fileReference.ReadAllTextAsync(); } - public async Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) + public async ValueTask SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) { using (var stream = new SyncMemoryStream(data, 0, data.Length)) { - return await this.SaveAsync(stream, file, contentType); + return await this.SaveAsync(stream, file, contentType, overwritePolicy); } } - public async Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) + public async ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) { + var uploadBlob = true; var blockBlob = this.container.Value.GetBlockBlobReference(file.Path); + var blobExists = await blockBlob.ExistsAsync(); - if (await blockBlob.ExistsAsync()) + if (blobExists) { + if (overwritePolicy == OverwritePolicy.Never) + { + throw new Exceptions.FileAlreadyExistsException(this.Name, file.Path); + } + await blockBlob.FetchAttributesAsync(); + + if (overwritePolicy == OverwritePolicy.IfContentModified) + { + using (var md5 = MD5.Create()) + { + data.Seek(0, SeekOrigin.Begin); + var contentMD5 = Convert.ToBase64String(md5.ComputeHash(data)); + data.Seek(0, SeekOrigin.Begin); + uploadBlob = (contentMD5 != blockBlob.Properties.ContentMD5); + } + } } - await blockBlob.UploadFromStreamAsync(data); + if (uploadBlob) + { + await blockBlob.UploadFromStreamAsync(data); + } var reference = new Internal.AzureFileReference(blockBlob, withMetadata: true); - reference.Properties.ContentType = contentType; - await reference.SavePropertiesAsync(); + if (reference.Properties.ContentType != contentType) + { + reference.Properties.ContentType = contentType; + await reference.SavePropertiesAsync(); + } return reference; } - private async Task InternalGetAsync(IPrivateFileReference file, bool withMetadata = false) + public ValueTask GetSharedAccessSignatureAsync(ISharedAccessPolicy policy) + { + var adHocPolicy = new SharedAccessBlobPolicy() + { + SharedAccessStartTime = policy.StartTime, + SharedAccessExpiryTime = policy.ExpiryTime, + Permissions = FromGenericToAzure(policy.Permissions), + }; + + return new ValueTask(this.container.Value.GetSharedAccessSignature(adHocPolicy)); + } + + internal static SharedAccessBlobPermissions FromGenericToAzure(SharedAccessPermissions permissions) { - var azureFile = file as Internal.AzureFileReference; - if (azureFile != null) + var result = SharedAccessBlobPermissions.None; + + if (permissions.HasFlag(SharedAccessPermissions.Add)) + { + result |= SharedAccessBlobPermissions.Add; + } + + if (permissions.HasFlag(SharedAccessPermissions.Create)) { - return azureFile; + result |= SharedAccessBlobPermissions.Create; } - return await this.InternalGetAsync(new Uri(file.Path, UriKind.Relative), withMetadata); + if (permissions.HasFlag(SharedAccessPermissions.Delete)) + { + result |= SharedAccessBlobPermissions.Delete; + } + + if (permissions.HasFlag(SharedAccessPermissions.List)) + { + result |= SharedAccessBlobPermissions.List; + } + + if (permissions.HasFlag(SharedAccessPermissions.Read)) + { + result |= SharedAccessBlobPermissions.Read; + } + + if (permissions.HasFlag(SharedAccessPermissions.Write)) + { + result |= SharedAccessBlobPermissions.Write; + } + + return result; + } + + private ValueTask InternalGetAsync(IPrivateFileReference file, bool withMetadata = false) + { + return this.InternalGetAsync(new Uri(file.Path, UriKind.Relative), withMetadata); } - private async Task InternalGetAsync(Uri uri, bool withMetadata) + private async ValueTask InternalGetAsync(Uri uri, bool withMetadata) { try { 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 new file mode 100644 index 0000000..3bb06fd --- /dev/null +++ b/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.csproj @@ -0,0 +1,34 @@ + + + + Azure Storage Provider for Geek Learning Storage Abstractions. + 0.0.1 + Geek Learning;Cyprien Autexier;Adrien Siffermann + net45;netstandard1.3 + GeekLearning.Storage.Azure + GeekLearning.Storage.Azure + 1.6.1 + $(PackageTargetFallback);portable-net45+win8 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.xproj b/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.xproj deleted file mode 100644 index 7a8c151..0000000 --- a/src/GeekLearning.Storage.Azure/GeekLearning.Storage.Azure.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - fd8bb8f9-9af5-4c12-b962-9e08c30b01e2 - GeekLearning.Storage.Azure - .\obj - .\bin\ - v4.5.2 - - - - 2.0 - - - diff --git a/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs b/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs index 47a1614..013efe1 100644 --- a/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs +++ b/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs @@ -3,11 +3,15 @@ using Microsoft.WindowsAzure.Storage.Blob; using System; using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Threading.Tasks; public class AzureFileProperties : IFileProperties { private const string DefaultCacheControl = "max-age=300, must-revalidate"; private readonly ICloudBlob cloudBlob; + private readonly Dictionary decodedMetadata; public AzureFileProperties(ICloudBlob cloudBlob) { @@ -16,6 +20,15 @@ public AzureFileProperties(ICloudBlob cloudBlob) { this.cloudBlob.Properties.CacheControl = DefaultCacheControl; } + + if (this.cloudBlob.Metadata != null) + { + decodedMetadata = this.cloudBlob.Metadata.ToDictionary(m => m.Key, m => WebUtility.HtmlDecode(m.Value)); + } + else + { + decodedMetadata = new Dictionary(); + } } public DateTimeOffset? LastModified => this.cloudBlob.Properties.LastModified; @@ -36,6 +49,20 @@ public string CacheControl set { this.cloudBlob.Properties.CacheControl = value; } } - public IDictionary Metadata => this.cloudBlob.Metadata; + public string ContentMD5 => this.cloudBlob.Properties.ContentMD5; + + public IDictionary Metadata => this.decodedMetadata; + + internal async Task SaveAsync() + { + await this.cloudBlob.SetPropertiesAsync(); + + foreach (var meta in this.decodedMetadata) + { + this.cloudBlob.Metadata[meta.Key] = WebUtility.HtmlEncode(meta.Value); + } + + await this.cloudBlob.SetMetadataAsync(); + } } } diff --git a/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs b/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs index 65f6024..789dfc2 100644 --- a/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs +++ b/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs @@ -8,13 +8,15 @@ public class AzureFileReference : IFileReference { - private Lazy propertiesLazy; + private Lazy propertiesLazy; + private bool withMetadata; public AzureFileReference(string path, ICloudBlob cloudBlob, bool withMetadata) { this.Path = path; this.CloudBlob = cloudBlob; - this.propertiesLazy = new Lazy(() => + this.withMetadata = withMetadata; + this.propertiesLazy = new Lazy(() => { if (withMetadata && cloudBlob.Metadata != null && cloudBlob.Properties != null) { @@ -43,12 +45,12 @@ public Task DeleteAsync() return this.CloudBlob.DeleteAsync(); } - public async Task ReadAsync() + public async ValueTask ReadAsync() { return await this.ReadInMemoryAsync(); } - public async Task ReadInMemoryAsync() + public async ValueTask ReadInMemoryAsync() { var memoryStream = new MemoryStream(); await this.CloudBlob.DownloadRangeToStreamAsync(memoryStream, null, null); @@ -61,17 +63,12 @@ public Task UpdateAsync(Stream stream) return this.CloudBlob.UploadFromStreamAsync(stream); } - public Task GetExpirableUriAsync() - { - throw new NotImplementedException(); - } - public async Task ReadToStreamAsync(Stream targetStream) { await this.CloudBlob.DownloadRangeToStreamAsync(targetStream, null, null); } - public async Task ReadAllTextAsync() + public async ValueTask ReadAllTextAsync() { using (var reader = new StreamReader(await this.CloudBlob.OpenReadAsync(AccessCondition.GenerateEmptyCondition(), new BlobRequestOptions(), new OperationContext()))) { @@ -79,15 +76,39 @@ public async Task ReadAllTextAsync() } } - public async Task ReadAllBytesAsync() + public async ValueTask ReadAllBytesAsync() { return (await this.ReadInMemoryAsync()).ToArray(); } - public async Task SavePropertiesAsync() + public Task SavePropertiesAsync() + { + return this.propertiesLazy.Value.SaveAsync(); + } + + public ValueTask GetSharedAccessSignature(ISharedAccessPolicy policy) { - await this.CloudBlob.SetPropertiesAsync(); - await this.CloudBlob.SetMetadataAsync(); + var adHocPolicy = new SharedAccessBlobPolicy() + { + SharedAccessStartTime = policy.StartTime, + SharedAccessExpiryTime = policy.ExpiryTime, + Permissions = AzureStore.FromGenericToAzure(policy.Permissions), + }; + + return new ValueTask(this.CloudBlob.GetSharedAccessSignature(adHocPolicy)); + } + + public async Task FetchProperties() + { + if (this.withMetadata) + { + return; + } + + await this.CloudBlob.FetchAttributesAsync(); + + this.propertiesLazy = new Lazy(() => new AzureFileProperties(this.CloudBlob)); + this.withMetadata = true; } } } diff --git a/src/GeekLearning.Storage.Azure/Properties/AssemblyInfo.cs b/src/GeekLearning.Storage.Azure/Properties/AssemblyInfo.cs deleted file mode 100644 index 0b37cfb..0000000 --- a/src/GeekLearning.Storage.Azure/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GeekLearning.Storage.Azure")] -[assembly: AssemblyDescription("Azure Storage Provider for Geek Learning Storage Abstractions.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Geek Learning")] -[assembly: AssemblyProduct("GeekLearning.Storage")] -[assembly: AssemblyCopyright("Copyright © Geek Learning 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("fd8bb8f9-9af5-4c12-b962-9e08c30b01e2")] diff --git a/src/GeekLearning.Storage.Azure/project.json b/src/GeekLearning.Storage.Azure/project.json deleted file mode 100644 index 9fc3c0a..0000000 --- a/src/GeekLearning.Storage.Azure/project.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "version": "0.0.1-*", - "description": "Azure Storage Provider for Geek Learning Storage Abstractions.", - "authors": [ "Geek Learning", "Cyprien Autexier", "Adrien Siffermann" ], - "packOptions": { - "tags": [], - "projectUrl": "", - "licenseUrl": "" - }, - - "dependencies": { - "NETStandard.Library": "1.6.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.1.0", - "Microsoft.Extensions.Options": "1.1.0", - "Microsoft.Extensions.FileSystemGlobbing": "1.1.0", - - "WindowsAzure.Storage": "8.0.0", - - "GeekLearning.Storage": "*" - }, - - "frameworks": { - "net45": {}, - "netstandard1.3": { - "imports": [ - "portable-net45+win8" - ] - } - } -} diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs index 5d489bd..a124b75 100644 --- a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs @@ -8,9 +8,14 @@ public static class FileSystemExtendedPropertiesExtensions { - public static IServiceCollection AddFileSystemExtendedProperties(this IServiceCollection services, Action configure) + public static IServiceCollection AddFileSystemExtendedProperties(this IServiceCollection services, Action configure = null) { - services.Configure(configure); + if (configure == null) + { + configure = o => { }; + } + + services.Configure(configure); services.AddTransient(); return services; } 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 new file mode 100644 index 0000000..a49d417 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.csproj @@ -0,0 +1,33 @@ + + + + File System based extended properties storage provider for the FileSystem provider. + 0.0.1 + Geek Learning;Adrien Siffermann;Cyprien Autexier + net45;netstandard1.3 + GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem + GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem + 1.6.1 + false + false + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.xproj b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.xproj deleted file mode 100644 index 7354067..0000000 --- a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - 8c02ebbe-9ec8-4f47-9464-5a94bde25a8f - GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem - .\obj - .\bin\ - v4.6.2 - - - - 2.0 - - - diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs index afdf947..01e7a14 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) @@ -17,16 +16,16 @@ public ExtendedPropertiesProvider( this.options = options.Value; } - public Task GetExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file) + public ValueTask GetExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file) { var extendedPropertiesPath = this.GetExtendedPropertiesPath(storeAbsolutePath, file); if (!File.Exists(extendedPropertiesPath)) { - return Task.FromResult(new FileExtendedProperties()); + return new ValueTask(new FileExtendedProperties()); } var content = File.ReadAllText(extendedPropertiesPath); - return Task.FromResult(JsonConvert.DeserializeObject(content)); + return new ValueTask(JsonConvert.DeserializeObject(content)); } public Task SaveExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file, FileExtendedProperties extendedProperties) diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Properties/AssemblyInfo.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Properties/AssemblyInfo.cs deleted file mode 100644 index 6c872e6..0000000 --- a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8c02ebbe-9ec8-4f47-9464-5a94bde25a8f")] diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/project.json b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/project.json deleted file mode 100644 index af100df..0000000 --- a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/project.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "version": "0.0.1-*", - "description": "File System based extended properties storage provider for the FileSystem provider.", - "authors": [ "Geek Learning", "Adrien Siffermann", "Cyprien Autexier" ], - "packOptions": { - "tags": [], - "projectUrl": "", - "licenseUrl": "" - }, - - "dependencies": { - "NETStandard.Library": "1.6.1", - "Newtonsoft.Json": "9.0.1", - - "GeekLearning.Storage.FileSystem": "*" - }, - - "frameworks": { - "net45": {}, - "netstandard1.3": {} - } -} 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 new file mode 100644 index 0000000..01b1a2a --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.csproj @@ -0,0 +1,32 @@ + + + + Geek Learning File Server based on FileSystem Storage Provider. + 0.0.1 + Geek Learning;Cyprien Autexier;Adrien Siffermann + net451;netstandard1.4 + GeekLearning.Storage.FileSystem.Server + GeekLearning.Storage.FileSystem.Server + 1.6.1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.xproj b/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.xproj deleted file mode 100644 index c90d136..0000000 --- a/src/GeekLearning.Storage.FileSystem.Server/GeekLearning.Storage.FileSystem.Server.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - 9d94cd6c-9451-449a-bed2-1c07d624a8e0 - GeekLearning.Storage.FileSystem.Server - .\obj - .\bin\ - v4.5.2 - - - - 2.0 - - - diff --git a/src/GeekLearning.Storage.FileSystem.Server/Properties/AssemblyInfo.cs b/src/GeekLearning.Storage.FileSystem.Server/Properties/AssemblyInfo.cs deleted file mode 100644 index f86cb40..0000000 --- a/src/GeekLearning.Storage.FileSystem.Server/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GeekLearning.Storage.FileSystem.Server")] -[assembly: AssemblyDescription("Geek Learning File Server based on FileSystem Storage Provider.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Geek Learning")] -[assembly: AssemblyProduct("GeekLearning.Storage")] -[assembly: AssemblyCopyright("Copyright © Geek Learning 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9d94cd6c-9451-449a-bed2-1c07d624a8e0")] diff --git a/src/GeekLearning.Storage.FileSystem.Server/project.json b/src/GeekLearning.Storage.FileSystem.Server/project.json deleted file mode 100644 index a809676..0000000 --- a/src/GeekLearning.Storage.FileSystem.Server/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": "0.0.1-*", - "description": "Geek Learning File Server based on FileSystem Storage Provider.", - "authors": [ "Geek Learning", "Cyprien Autexier", "Adrien Siffermann" ], - "packOptions": { - "tags": [], - "projectUrl": "", - "licenseUrl": "" - }, - - "dependencies": { - "NETStandard.Library": "1.6.1", - "Microsoft.AspNetCore.Http.Abstractions": "1.1.0", - "Microsoft.Extensions.Logging.Abstractions": "1.1.0", - "Microsoft.IdentityModel.Tokens": "5.1.0", - - "GeekLearning.Storage.FileSystem": "*" - }, - - "frameworks": { - "net451": {}, - "netstandard1.4": {} - } -} 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..5d8b5af 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,35 +10,34 @@ 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) + public async ValueTask ListAsync(string path, bool recursive, bool withMetadata) { var directoryPath = (string.IsNullOrEmpty(path) || path == "/" || path == "\\") ? this.AbsolutePath : Path.Combine(this.AbsolutePath, path); @@ -57,7 +57,7 @@ public async Task ListAsync(string path, bool recursive, bool return result.ToArray(); } - public async Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) + public async ValueTask ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) { var directoryPath = (string.IsNullOrEmpty(path) || path == "/" || path == "\\") ? this.AbsolutePath : Path.Combine(this.AbsolutePath, path); @@ -81,12 +81,12 @@ public async Task ListAsync(string path, string searchPattern, return result.ToArray(); } - public async Task GetAsync(IPrivateFileReference file, bool withMetadata) + public async ValueTask GetAsync(IPrivateFileReference file, bool withMetadata) { return await this.InternalGetAsync(file, withMetadata); } - public async Task GetAsync(Uri uri, bool withMetadata) + public async ValueTask GetAsync(Uri uri, bool withMetadata) { if (uri.IsAbsoluteUri) { @@ -102,64 +102,80 @@ public async Task DeleteAsync(IPrivateFileReference file) await fileReference.DeleteAsync(); } - public async Task ReadAsync(IPrivateFileReference file) + public async ValueTask ReadAsync(IPrivateFileReference file) { var fileReference = await this.InternalGetAsync(file); return await fileReference.ReadAsync(); } - public async Task ReadAllBytesAsync(IPrivateFileReference file) + public async ValueTask ReadAllBytesAsync(IPrivateFileReference file) { var fileReference = await this.InternalGetAsync(file); return await fileReference.ReadAllBytesAsync(); } - public async Task ReadAllTextAsync(IPrivateFileReference file) + public async ValueTask ReadAllTextAsync(IPrivateFileReference file) { var fileReference = await this.InternalGetAsync(file); return await fileReference.ReadAllTextAsync(); } - public async Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) + public async ValueTask SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) { using (var stream = new MemoryStream(data, 0, data.Length)) { - return await this.SaveAsync(stream, file, contentType); + return await this.SaveAsync(stream, file, contentType, overwritePolicy); } } - public async Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) + public async ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) { var fileReference = await this.InternalGetAsync(file, withMetadata: true, checkIfExists: false); - this.EnsurePathExists(fileReference.FileSystemPath); + var fileExists = File.Exists(fileReference.FileSystemPath); - using (var fileStream = File.Open(fileReference.FileSystemPath, FileMode.Create, FileAccess.Write)) + if (fileExists) { - await data.CopyToAsync(fileStream); + if (overwritePolicy == OverwritePolicy.Never) + { + throw new Exceptions.FileAlreadyExistsException(this.Name, file.Path); + } } var properties = fileReference.Properties as Internal.FileSystemFileProperties; + var hashes = ComputeHashes(data); + + if (!fileExists + || overwritePolicy == OverwritePolicy.Always + || (overwritePolicy == OverwritePolicy.IfContentModified && properties.ContentMD5 != hashes.ContentMD5)) + { + this.EnsurePathExists(fileReference.FileSystemPath); + + using (var fileStream = File.Open(fileReference.FileSystemPath, FileMode.Create, FileAccess.Write)) + { + await data.CopyToAsync(fileStream); + } + } properties.ContentType = contentType; - properties.ExtendedProperties.ETag = GenerateEtag(fileReference.FileSystemPath); + properties.ExtendedProperties.ETag = hashes.ETag; + properties.ExtendedProperties.ContentMD5 = hashes.ContentMD5; await fileReference.SavePropertiesAsync(); return fileReference; } - private async Task InternalGetAsync(IPrivateFileReference file, bool withMetadata = false, bool checkIfExists = true) + public ValueTask GetSharedAccessSignatureAsync(ISharedAccessPolicy policy) { - var fileSystemFile = file as Internal.FileSystemFileReference; - if (fileSystemFile != null) - { - return fileSystemFile; - } + throw new NotSupportedException(); + } - return await this.InternalGetAsync(file.Path, withMetadata, checkIfExists); + private ValueTask InternalGetAsync(IPrivateFileReference file, bool withMetadata = false, bool checkIfExists = true) + { + return this.InternalGetAsync(file.Path, withMetadata, checkIfExists); } - private async Task InternalGetAsync(string path, bool withMetadata, bool checkIfExists = true) + private async ValueTask InternalGetAsync(string path, bool withMetadata, bool checkIfExists = true) { var fullPath = Path.Combine(this.AbsolutePath, path); if (checkIfExists && !File.Exists(fullPath)) @@ -199,18 +215,23 @@ private void EnsurePathExists(string path) } } - private static string GenerateEtag(string fileSystemPath) + private static (string ETag, string ContentMD5) ComputeHashes(Stream stream) { - var etag = string.Empty; - using (var stream = File.Open(fileSystemPath, FileMode.Open, FileAccess.Read)) + var eTag = string.Empty; + var contentMD5 = string.Empty; + + stream.Seek(0, SeekOrigin.Begin); using (var md5 = MD5.Create()) { + stream.Seek(0, SeekOrigin.Begin); var hash = md5.ComputeHash(stream); + stream.Seek(0, SeekOrigin.Begin); + contentMD5 = Convert.ToBase64String(hash); string hex = BitConverter.ToString(hash); - etag = hex.Replace("-", ""); + eTag = $"\"{hex.Replace("-", "")}\""; } - return $"\"{etag}\""; + return (eTag, contentMD5); } } } diff --git a/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj b/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj new file mode 100644 index 0000000..283f2c0 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj @@ -0,0 +1,33 @@ + + + + FileSystem Provider for Geek Learning Storage Abstractions. + 0.0.1 + Geek Learning;Cyprien Autexier;Adrien Siffermann + net45;netstandard1.3 + GeekLearning.Storage.FileSystem + GeekLearning.Storage.FileSystem + 1.6.1 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.xproj b/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.xproj deleted file mode 100644 index 97fe046..0000000 --- a/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - 4a12b042-76b3-471b-9235-f653e1abe3c0 - GeekLearning.Storage.FileSystem - .\obj - .\bin\ - v4.5.2 - - - - 2.0 - - - diff --git a/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs b/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs index 843de53..5785103 100644 --- a/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs +++ b/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs @@ -4,7 +4,7 @@ public interface IExtendedPropertiesProvider { - Task GetExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file); + ValueTask GetExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file); Task SaveExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file, Internal.FileExtendedProperties extendedProperties); } diff --git a/src/GeekLearning.Storage.FileSystem/Internal/FileExtendedProperties.cs b/src/GeekLearning.Storage.FileSystem/Internal/FileExtendedProperties.cs index b644a6f..975429c 100644 --- a/src/GeekLearning.Storage.FileSystem/Internal/FileExtendedProperties.cs +++ b/src/GeekLearning.Storage.FileSystem/Internal/FileExtendedProperties.cs @@ -15,6 +15,8 @@ public FileExtendedProperties() public string CacheControl { get; set; } + public string ContentMD5 { get; set; } + public IDictionary Metadata { get; set; } } } diff --git a/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileProperties.cs b/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileProperties.cs index 4f8288a..1d9e89e 100644 --- a/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileProperties.cs +++ b/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileProperties.cs @@ -33,6 +33,8 @@ public string CacheControl set { this.extendedProperties.CacheControl = value; } } + public string ContentMD5 => this.extendedProperties.ContentMD5; + public IDictionary Metadata => this.extendedProperties.Metadata; internal FileExtendedProperties ExtendedProperties => this.extendedProperties; diff --git a/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileReference.cs b/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileReference.cs index 6ed1d9a..d038472 100644 --- a/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileReference.cs +++ b/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileReference.cs @@ -7,9 +7,10 @@ public class FileSystemFileReference : IFileReference { private readonly FileSystemStore store; - private readonly Lazy propertiesLazy; private readonly Lazy publicUrlLazy; private readonly IExtendedPropertiesProvider extendedPropertiesProvider; + private bool withMetadata; + private Lazy propertiesLazy; public FileSystemFileReference( string filePath, @@ -24,6 +25,7 @@ public FileSystemFileReference( this.Path = path.Replace('\\', '/'); this.store = store; this.extendedPropertiesProvider = extendedPropertiesProvider; + this.withMetadata = withMetadata; this.propertiesLazy = new Lazy(() => { @@ -60,25 +62,19 @@ public Task DeleteAsync() return Task.FromResult(true); } - public Task GetExpirableUriAsync() + public ValueTask ReadAllBytesAsync() { - throw new NotImplementedException(); + return new ValueTask(File.ReadAllBytes(this.FileSystemPath)); } - public Task ReadAllBytesAsync() + public ValueTask ReadAllTextAsync() { - return Task.FromResult(File.ReadAllBytes(this.FileSystemPath)); + return new ValueTask(File.ReadAllText(this.FileSystemPath)); } - public Task ReadAllTextAsync() + public ValueTask ReadAsync() { - return Task.FromResult(File.ReadAllText(this.FileSystemPath)); - } - - public Task ReadAsync() - { - Stream stream = File.OpenRead(this.FileSystemPath); - return Task.FromResult(stream); + return new ValueTask(File.OpenRead(this.FileSystemPath)); } public async Task ReadToStreamAsync(Stream targetStream) @@ -109,5 +105,30 @@ public Task SavePropertiesAsync() this, (this.Properties as FileSystemFileProperties).ExtendedProperties); } + + public ValueTask GetSharedAccessSignature(ISharedAccessPolicy policy) + { + throw new NotSupportedException(); + } + + public async Task FetchProperties() + { + if (this.withMetadata) + { + return; + } + + if (this.extendedPropertiesProvider == null) + { + throw new InvalidOperationException("There is no FileSystem extended properties provider."); + } + + var extendedProperties = await this.extendedPropertiesProvider.GetExtendedPropertiesAsync( + this.store.AbsolutePath, + this); + + this.propertiesLazy = new Lazy(() => new FileSystemFileProperties(this.FileSystemPath, extendedProperties)); + this.withMetadata = true; + } } } diff --git a/src/GeekLearning.Storage.FileSystem/Properties/AssemblyInfo.cs b/src/GeekLearning.Storage.FileSystem/Properties/AssemblyInfo.cs deleted file mode 100644 index 4d9786a..0000000 --- a/src/GeekLearning.Storage.FileSystem/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GeekLearning.Storage.FileSystem")] -[assembly: AssemblyDescription("Geek Learning FileSystem Storage Provider")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Geek Learning")] -[assembly: AssemblyProduct("GeekLearning.Storage")] -[assembly: AssemblyCopyright("Copyright © Geek Learning 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4a12b042-76b3-471b-9235-f653e1abe3c0")] diff --git a/src/GeekLearning.Storage.FileSystem/project.json b/src/GeekLearning.Storage.FileSystem/project.json deleted file mode 100644 index e708da4..0000000 --- a/src/GeekLearning.Storage.FileSystem/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": "0.0.1-*", - "description": "FileSystem Provider for Geek Learning Storage Abstractions.", - "authors": [ "Geek Learning", "Cyprien Autexier", "Adrien Siffermann" ], - "packOptions": { - "tags": [], - "projectUrl": "", - "licenseUrl": "" - }, - - "dependencies": { - "NETStandard.Library": "1.6.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.1.0", - "Microsoft.Extensions.Options": "1.1.0", - "Microsoft.Extensions.FileSystemGlobbing": "1.1.0", - - "GeekLearning.Storage": "*" - }, - - "frameworks": { - "net45": {}, - "netstandard1.3": {} - } -} 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/FileAlreadyExistsException.cs b/src/GeekLearning.Storage/Exceptions/FileAlreadyExistsException.cs new file mode 100644 index 0000000..e6b0204 --- /dev/null +++ b/src/GeekLearning.Storage/Exceptions/FileAlreadyExistsException.cs @@ -0,0 +1,12 @@ +namespace GeekLearning.Storage.Exceptions +{ + using System; + + public class FileAlreadyExistsException : Exception + { + public FileAlreadyExistsException(string storeName, string filePath) + : base($"The file {filePath} already exists in Store {storeName}.") + { + } + } +} 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 new file mode 100644 index 0000000..906ec1e --- /dev/null +++ b/src/GeekLearning.Storage/GeekLearning.Storage.csproj @@ -0,0 +1,30 @@ + + + + File Storage abstractions with providers. + 0.0.1 + Geek Learning;Cyprien Autexier;Adrien Siffermann + netstandard1.1 + GeekLearning.Storage + GeekLearning.Storage + 1.6.1 + + + + + + + + + + + + + + + + + + + + diff --git a/src/GeekLearning.Storage/GeekLearning.Storage.xproj b/src/GeekLearning.Storage/GeekLearning.Storage.xproj deleted file mode 100644 index a5570c8..0000000 --- a/src/GeekLearning.Storage/GeekLearning.Storage.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - 1f419c53-73c6-4460-b284-6a49ae41c596 - GeekLearning.Storage - .\obj - .\bin\ - v4.5.2 - - - - 2.0 - - - diff --git a/src/GeekLearning.Storage/IFileProperties.cs b/src/GeekLearning.Storage/IFileProperties.cs index a786d1b..fd7dea9 100644 --- a/src/GeekLearning.Storage/IFileProperties.cs +++ b/src/GeekLearning.Storage/IFileProperties.cs @@ -15,6 +15,8 @@ public interface IFileProperties string CacheControl { get; set; } + string ContentMD5 { get; } + IDictionary Metadata { get; } } } diff --git a/src/GeekLearning.Storage/IFileReference.cs b/src/GeekLearning.Storage/IFileReference.cs index b89f712..115101e 100644 --- a/src/GeekLearning.Storage/IFileReference.cs +++ b/src/GeekLearning.Storage/IFileReference.cs @@ -11,18 +11,20 @@ public interface IFileReference : IPrivateFileReference Task ReadToStreamAsync(Stream targetStream); - Task ReadAsync(); + ValueTask ReadAsync(); - Task ReadAllTextAsync(); + ValueTask ReadAllTextAsync(); - Task ReadAllBytesAsync(); + ValueTask ReadAllBytesAsync(); Task DeleteAsync(); Task UpdateAsync(Stream stream); - Task GetExpirableUriAsync(); - Task SavePropertiesAsync(); + + ValueTask GetSharedAccessSignature(ISharedAccessPolicy policy); + + Task FetchProperties(); } } diff --git a/src/GeekLearning.Storage/ISharedAccessPolicy.cs b/src/GeekLearning.Storage/ISharedAccessPolicy.cs new file mode 100644 index 0000000..c701baf --- /dev/null +++ b/src/GeekLearning.Storage/ISharedAccessPolicy.cs @@ -0,0 +1,13 @@ +namespace GeekLearning.Storage +{ + using System; + + public interface ISharedAccessPolicy + { + DateTimeOffset? StartTime { get; } + + DateTimeOffset? ExpiryTime { get; } + + SharedAccessPermissions Permissions { get; } + } +} 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..f82c7e8 100644 --- a/src/GeekLearning.Storage/IStore.cs +++ b/src/GeekLearning.Storage/IStore.cs @@ -8,24 +8,28 @@ public interface IStore { string Name { get; } - Task ListAsync(string path, bool recursive, bool withMetadata); + Task InitAsync(); - Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata); + ValueTask ListAsync(string path, bool recursive, bool withMetadata); - Task GetAsync(IPrivateFileReference file, bool withMetadata); + ValueTask ListAsync(string path, string searchPattern, bool recursive, bool withMetadata); - Task GetAsync(Uri file, bool withMetadata); + ValueTask GetAsync(IPrivateFileReference file, bool withMetadata); + + ValueTask GetAsync(Uri file, bool withMetadata); Task DeleteAsync(IPrivateFileReference file); - Task ReadAsync(IPrivateFileReference file); + ValueTask ReadAsync(IPrivateFileReference file); + + ValueTask ReadAllBytesAsync(IPrivateFileReference file); - Task ReadAllBytesAsync(IPrivateFileReference file); + ValueTask ReadAllTextAsync(IPrivateFileReference file); - Task ReadAllTextAsync(IPrivateFileReference file); + ValueTask SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always); - Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType); + ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always); - Task SaveAsync(Stream data, IPrivateFileReference file, string contentType); + ValueTask GetSharedAccessSignatureAsync(ISharedAccessPolicy policy); } } diff --git a/src/GeekLearning.Storage/IStoreExtensions.cs b/src/GeekLearning.Storage/IStoreExtensions.cs index 36fa987..e063ad0 100644 --- a/src/GeekLearning.Storage/IStoreExtensions.cs +++ b/src/GeekLearning.Storage/IStoreExtensions.cs @@ -5,49 +5,31 @@ 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); - } + public static ValueTask ListAsync(this IStore store, string path, bool recursive = false, bool withMetadata = false) + => 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); - } + public static ValueTask ListAsync(this IStore store, string path, string searchPattern, bool recursive = false, bool withMetadata = false) + => store.ListAsync(path, searchPattern, recursive: recursive, withMetadata: withMetadata); public static Task DeleteAsync(this IStore store, string path) - { - return 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); - } - - public static Task ReadAsync(this IStore store, string path) - { - return store.ReadAsync(new Internal.PrivateFileReference(path)); - } - - public static Task ReadAllBytesAsync(this IStore store, string path) - { - return store.ReadAllBytesAsync(new Internal.PrivateFileReference(path)); - } - - public static Task ReadAllTextAsync(this IStore store, string path) - { - return 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); - } - - public static Task SaveAsync(this IStore store, Stream data, string path, string contentType) - { - return store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); - } + => store.DeleteAsync(new Internal.PrivateFileReference(path)); + + public static ValueTask GetAsync(this IStore store, string path, bool withMetadata = false) + => store.GetAsync(new Internal.PrivateFileReference(path), withMetadata: withMetadata); + + public static ValueTask ReadAsync(this IStore store, string path) + => store.ReadAsync(new Internal.PrivateFileReference(path)); + + public static ValueTask ReadAllBytesAsync(this IStore store, string path) + => store.ReadAllBytesAsync(new Internal.PrivateFileReference(path)); + + public static ValueTask ReadAllTextAsync(this IStore store, string path) + => store.ReadAllTextAsync(new Internal.PrivateFileReference(path)); + + public static ValueTask SaveAsync(this IStore store, byte[] data, string path, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) + => store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType, overwritePolicy); + + public static ValueTask SaveAsync(this IStore store, Stream data, string path, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) + => store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType, overwritePolicy); } } 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..8d4f4d3 100644 --- a/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs +++ b/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs @@ -1,76 +1,50 @@ 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 ValueTask GetAsync(Uri file, bool withMetadata) => this.innerStore.GetAsync(file, withMetadata); - public Task ReadAllBytesAsync(IPrivateFileReference file) - { - return innerStore.ReadAllBytesAsync(file); - } + public ValueTask GetAsync(IPrivateFileReference file, bool withMetadata) => this.innerStore.GetAsync(file, withMetadata); - public Task ReadAllTextAsync(IPrivateFileReference file) - { - return innerStore.ReadAllTextAsync(file); - } + public ValueTask ListAsync(string path, bool recursive, bool withMetadata) => this.innerStore.ListAsync(path, recursive, withMetadata); - public Task ReadAsync(IPrivateFileReference file) - { - return innerStore.ReadAsync(file); - } + public ValueTask 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 ValueTask ReadAllBytesAsync(IPrivateFileReference file) => this.innerStore.ReadAllBytesAsync(file); - public Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) - { - return innerStore.SaveAsync(data, file, contentType); - } + public ValueTask ReadAllTextAsync(IPrivateFileReference file) => this.innerStore.ReadAllTextAsync(file); + + public ValueTask ReadAsync(IPrivateFileReference file) => this.innerStore.ReadAsync(file); + + public ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) => this.innerStore.SaveAsync(data, file, contentType, overwritePolicy); + + public ValueTask SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) => this.innerStore.SaveAsync(data, file, contentType, overwritePolicy); + + public ValueTask GetSharedAccessSignatureAsync(ISharedAccessPolicy policy) => this.innerStore.GetSharedAccessSignatureAsync(policy); } } 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/OverwritePolicy.cs b/src/GeekLearning.Storage/OverwritePolicy.cs new file mode 100644 index 0000000..394c81e --- /dev/null +++ b/src/GeekLearning.Storage/OverwritePolicy.cs @@ -0,0 +1,9 @@ +namespace GeekLearning.Storage +{ + public enum OverwritePolicy + { + Always = 0, + IfContentModified = 1, + Never = 2, + } +} \ No newline at end of file diff --git a/src/GeekLearning.Storage/Properties/AssemblyInfo.cs b/src/GeekLearning.Storage/Properties/AssemblyInfo.cs deleted file mode 100644 index 82a6b4c..0000000 --- a/src/GeekLearning.Storage/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GeekLearning.Storage")] -[assembly: AssemblyDescription("File Storage abstractions with providers.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Geek Learning")] -[assembly: AssemblyProduct("GeekLearning.Storage")] -[assembly: AssemblyCopyright("Copyright © Geek Learning 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1f419c53-73c6-4460-b284-6a49ae41c596")] diff --git a/src/GeekLearning.Storage/SharedAccessPermissions.cs b/src/GeekLearning.Storage/SharedAccessPermissions.cs new file mode 100644 index 0000000..e06d14f --- /dev/null +++ b/src/GeekLearning.Storage/SharedAccessPermissions.cs @@ -0,0 +1,16 @@ +namespace GeekLearning.Storage +{ + using System; + + [Flags] + public enum SharedAccessPermissions + { + None = 0, + Read = 1, + Write = 2, + Delete = 4, + List = 8, + Add = 16, + Create = 32 + } +} diff --git a/src/GeekLearning.Storage/SharedAccessPolicy.cs b/src/GeekLearning.Storage/SharedAccessPolicy.cs new file mode 100644 index 0000000..5ad55b9 --- /dev/null +++ b/src/GeekLearning.Storage/SharedAccessPolicy.cs @@ -0,0 +1,13 @@ +namespace GeekLearning.Storage +{ + using System; + + public class SharedAccessPolicy : ISharedAccessPolicy + { + public DateTimeOffset? StartTime { get; set; } + + public DateTimeOffset? ExpiryTime { get; set; } + + public SharedAccessPermissions Permissions { get; set; } + } +} 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/src/GeekLearning.Storage/project.json b/src/GeekLearning.Storage/project.json deleted file mode 100644 index e8a24b8..0000000 --- a/src/GeekLearning.Storage/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "0.0.1-*", - "description": "File Storage abstractions with providers.", - "authors": [ "Geek Learning", "Cyprien Autexier", "Adrien Siffermann" ], - "packOptions": { - "tags": [], - "projectUrl": "", - "licenseUrl": "" - }, - - "dependencies": { - "NETStandard.Library": "1.6.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.1.0", - "Microsoft.Extensions.Options": "1.1.0" - }, - - "frameworks": { - "netstandard1.1": {} - } -} 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 new file mode 100644 index 0000000..2a5b84e --- /dev/null +++ b/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.csproj @@ -0,0 +1,53 @@ + + + + netcoreapp1.0;net451 + GeekLearning.Storage.Integration.Test + GeekLearning.Storage.Integration.Test + true + $(PackageTargetFallback);dotnet;portable-net45+win8 + 1.1.2 + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.xproj b/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.xproj deleted file mode 100644 index bb63879..0000000 --- a/tests/GeekLearning.Storage.Integration.Test/GeekLearning.Storage.Integration.Test.xproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 590b21b0-2afa-4329-82ad-ef180c50eb5c - GeekLearning.Storage.Integration.Test - .\obj - .\bin\ - v4.5.2 - - - 2.0 - - - - - - \ No newline at end of file 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/Properties/AssemblyInfo.cs b/tests/GeekLearning.Storage.Integration.Test/Properties/AssemblyInfo.cs deleted file mode 100644 index 07776c7..0000000 --- a/tests/GeekLearning.Storage.Integration.Test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("GeekLearning.Storage.Integration.Test")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("590b21b0-2afa-4329-82ad-ef180c50eb5c")] 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/SharedAccessTests.cs b/tests/GeekLearning.Storage.Integration.Test/SharedAccessTests.cs new file mode 100644 index 0000000..5a43bc2 --- /dev/null +++ b/tests/GeekLearning.Storage.Integration.Test/SharedAccessTests.cs @@ -0,0 +1,68 @@ +namespace GeekLearning.Storage.Integration.Test +{ + using Storage; + using Microsoft.Extensions.DependencyInjection; + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using System.Threading.Tasks; + using Xunit; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Auth; + using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.Extensions.Options; + using GeekLearning.Storage.Azure.Configuration; + using System.Linq; + + [Collection(nameof(IntegrationCollection))] + [Trait("Operation", "SharedAccess"), Trait("Kind", "Integration")] + public class SharedAccessTests + { + private readonly StoresFixture storeFixture; + + public SharedAccessTests(StoresFixture fixture) + { + this.storeFixture = fixture; + } + + [Theory(DisplayName = nameof(StoreSharedAccess)), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] + public async Task StoreSharedAccess(string storeName) + { + var storageFactory = this.storeFixture.Services.GetRequiredService(); + var options = this.storeFixture.Services.GetRequiredService>(); + + var store = storageFactory.GetStore(storeName); + + options.Value.ParsedStores.TryGetValue(storeName, out var storeOptions); + + var sharedAccessSignature = await store.GetSharedAccessSignatureAsync(new SharedAccessPolicy + { + ExpiryTime = DateTime.UtcNow.AddHours(24), + Permissions = SharedAccessPermissions.List, + }); + + var account = CloudStorageAccount.Parse(storeOptions.ConnectionString); + + var accountSAS = new StorageCredentials(sharedAccessSignature); + var accountWithSAS = new CloudStorageAccount(accountSAS, account.Credentials.AccountName, endpointSuffix: null, useHttps: true); + var blobClientWithSAS = accountWithSAS.CreateCloudBlobClient(); + var containerWithSAS = blobClientWithSAS.GetContainerReference(storeOptions.FolderName); + + BlobContinuationToken continuationToken = null; + List results = new List(); + + do + { + var response = await containerWithSAS.ListBlobsSegmentedAsync(continuationToken); + continuationToken = response.ContinuationToken; + results.AddRange(response.Results); + } + while (continuationToken != null); + + var filesFromStore = await store.ListAsync(null, false, false); + + Assert.Equal(filesFromStore.Length, results.OfType().Count()); + } + } +} diff --git a/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs b/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs index 19e3e75..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,89 +40,133 @@ public StoresFixture() services.AddOptions(); - services.AddStorage() + services.AddStorage(Configuration) .AddAzureStorage() - .AddFileSystemStorage(BasePath) - .AddFileSystemExtendedProperties(o => { }); + .AddFileSystemStorage(this.FileSystemRootPath) + .AddFileSystemExtendedProperties(); services.Configure(Configuration.GetSection("Storage")); 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(); } - private void ResetStores() + public IConfigurationRoot Configuration { get; } + + public IServiceProvider Services { get; } + + public string BasePath { get; } + + 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() { - ResetAzureStore(); - ResetFileSystemStore(); + this.DeleteRootResources(); } - private void ResetFileSystemStore() + private void DeleteRootResources() { - var directoryName = Configuration["Storage:Stores:filesystem:Parameters:Path"]; - var process = Process.Start(new ProcessStartInfo("robocopy.exe") + foreach (var parsedStoreKvp in this.AzureParsedOptions.ParsedStores) { - Arguments = $"\"{Path.Combine(BasePath, "SampleDirectory")}\" \"{Path.Combine(BasePath, directoryName)}\" /MIR" - }); + var cloudStorageAccount = CloudStorageAccount.Parse(parsedStoreKvp.Value.ConnectionString); + var client = cloudStorageAccount.CreateCloudBlobClient(); + var container = client.GetContainerReference(parsedStoreKvp.Value.FolderName); - if (!process.WaitForExit(30000)) + container.DeleteIfExistsAsync().Wait(); + } + + if (Directory.Exists(this.FileSystemRootPath)) + { + Directory.Delete(this.FileSystemRootPath, true); + } + + if (Directory.Exists(this.FileSystemSecondaryRootPath)) { - throw new TimeoutException("File system store was not reset properly"); + Directory.Delete(this.FileSystemSecondaryRootPath, true); } } - private void ResetAzureStore() + private void ResetStores() { - var azCopy = System.IO.Path.Combine( - Environment.ExpandEnvironmentVariables(Configuration["AzCopyPath"]), - "AzCopy.exe"); + this.DeleteRootResources(); + this.ResetAzureStores(); + this.ResetFileSystemStores(); + } - 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; + private void ResetFileSystemStores() + { + if (!Directory.Exists(this.FileSystemRootPath)) + { + Directory.CreateDirectory(this.FileSystemRootPath); + } - var client = cloudStorageAccount.CreateCloudBlobClient(); + foreach (var parsedStoreKvp in this.FileSystemParsedOptions.ParsedStores) + { + ResetFileSystemStore(parsedStoreKvp.Key, parsedStoreKvp.Value.AbsolutePath); + } - container = client.GetContainerReference(containerName); - container.CreateAsync().Wait(); + ResetFileSystemStore(this.TestStoreOptions.Name, this.TestStoreOptions.AbsolutePath); + } - var process = Process.Start(new ProcessStartInfo(azCopy) + private void ResetFileSystemStore(string storeName, string absolutePath) + { + var process = Process.Start(new ProcessStartInfo("robocopy.exe") { - Arguments = $"/Source:\"{System.IO.Path.Combine(BasePath, "SampleDirectory")}\" /Dest:\"{dest}\" /DestKey:{key} /S" + Arguments = $"\"{Path.Combine(this.BasePath, "SampleDirectory")}\" \"{absolutePath}\" /MIR" }); if (!process.WaitForExit(30000)) { - throw new TimeoutException("Azure store was not reset properly"); + process.Kill(); + throw new TimeoutException($"FileSystem Store '{storeName}' was not reset properly."); } } - public IConfigurationRoot Configuration { get; } + private void ResetAzureStores() + { + var azCopy = Path.Combine( + Environment.ExpandEnvironmentVariables(Configuration["AzCopyPath"]), + "AzCopy.exe"); - public IServiceProvider Services { get; } + foreach (var parsedStoreKvp in this.AzureParsedOptions.ParsedStores) + { + var cloudStorageAccount = CloudStorageAccount.Parse(parsedStoreKvp.Value.ConnectionString); + var cloudStoragekey = cloudStorageAccount.Credentials.ExportBase64EncodedKey(); + var containerName = parsedStoreKvp.Value.FolderName; - public string BasePath { get; } + var dest = cloudStorageAccount.BlobStorageUri.PrimaryUri.ToString() + containerName; - public void Dispose() - { - container.DeleteIfExistsAsync().Wait(); + var client = cloudStorageAccount.CreateCloudBlobClient(); - var fileSystemPath = Configuration["Storage:Stores:filesystem:Parameters:Path"]; - var folderNameFormat = Configuration["Storage:ExtendedPropertiesFolderNameFormat"]; + var container = client.GetContainerReference(containerName); + container.CreateIfNotExistsAsync().Wait(); - var directoryName = Path.Combine(BasePath, fileSystemPath); - if (Directory.Exists(directoryName)) - { - Directory.Delete(directoryName, true); - } + var arguments = $"/Source:\"{Path.Combine(this.BasePath, "SampleDirectory")}\" /Dest:\"{dest}\" /DestKey:{cloudStoragekey} /S /y"; + var process = Process.Start(new ProcessStartInfo(azCopy) + { + Arguments = arguments + }); - directoryName = Path.Combine(BasePath, string.Format(folderNameFormat, fileSystemPath)); - if (Directory.Exists(directoryName)) - { - Directory.Delete(directoryName, true); + 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..c82e0dd 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,31 @@ public async Task SaveMetatadaRoundtrip(string storeName) Assert.Equal(id, actualId); } - [Theory(DisplayName = nameof(ListMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] + [Theory(DisplayName = nameof(SaveEncodedMetatadaRoundtrip)), InlineData("Store1"), InlineData("Store2"), InlineData("Store3"), InlineData("Store4"), InlineData("Store5"), InlineData("Store6")] + public async Task SaveEncodedMetatadaRoundtrip(string storeName) + { + var storageFactory = this.storeFixture.Services.GetRequiredService(); + + var store = storageFactory.GetStore(storeName); + + var testFile = "Metadata/TextFile.txt"; + + var file = await store.GetAsync(testFile, withMetadata: true); + + var name = "ï"; + + file.Properties.Metadata["name"] = name; + + await file.SavePropertiesAsync(); + + file = await store.GetAsync(testFile, withMetadata: true); + + var actualName = file.Properties.Metadata["name"]; + + Assert.Equal(name, actualName); + } + + [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 7b59261..3876bad 100644 --- a/tests/GeekLearning.Storage.Integration.Test/appsettings.json +++ b/tests/GeekLearning.Storage.Integration.Test/appsettings.json @@ -1,27 +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" } }, - "ExtendedPropertiesFolderNameFormat": ".{0}-extended-properties" - }, - "TestStore": { - "Provider": "FileSystem", - "Parameters": { - "Path": "Templates" + + "ScopedStores": { + "ScopedStore1": { + "ProviderName": "AnotherFileSystem", + "FolderNameFormat": "AnotherPath-{0}" + }, + "ScopedStore2": { + "ProviderName": "AnotherAzure", + "AccessLevel": "Confidential", + "FolderNameFormat": "AnotherPath-{0}" + } } } } diff --git a/tests/GeekLearning.Storage.Integration.Test/project.json b/tests/GeekLearning.Storage.Integration.Test/project.json deleted file mode 100644 index 56f3c44..0000000 --- a/tests/GeekLearning.Storage.Integration.Test/project.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "version": "1.0.0-*", - - "buildOptions": { - "copyToOutput": [ - "SampleDirectory/**/*.*", - "appsettings.json", - "appsettings.*.json" - ] - }, - - "testRunner": "xunit", - - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "dotnet-test-xunit": "2.2.0-preview2-build1029", - "xunit": "2.2.0-beta2-build3300", - "xunit.runner.visualstudio": "2.2.0-beta2-build1149", - "Microsoft.Extensions.DependencyInjection": "1.1.0", - "Microsoft.Extensions.PlatformAbstractions": "1.1.0", - "Microsoft.Extensions.Configuration": "1.1.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", - "Microsoft.Extensions.Configuration.Json": "1.1.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", - "Microsoft.Extensions.Options": "1.1.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", - - "GeekLearning.Storage": "*", - "GeekLearning.Storage.Azure": "*", - "GeekLearning.Storage.FileSystem": "*", - "GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem": "*" - }, - - "frameworks": { - "netcoreapp1.0": { - "imports": [ "dotnet", "portable-net45+win8" ], - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.1" - } - } - }, - "net451": { - "frameworkAssemblies": {}, - "dependencies": {} - } - } -}