diff --git a/GeekLearning.Storage.sln b/GeekLearning.Storage.sln index bc2f558..1c39f88 100644 --- a/GeekLearning.Storage.sln +++ b/GeekLearning.Storage.sln @@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{520AB1D3-C50 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}" 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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +63,10 @@ Global {590B21B0-2AFA-4329-82AD-EF180C50EB5C}.Debug|Any CPU.Build.0 = Debug|Any CPU {590B21B0-2AFA-4329-82AD-EF180C50EB5C}.Release|Any CPU.ActiveCfg = Release|Any CPU {590B21B0-2AFA-4329-82AD-EF180C50EB5C}.Release|Any CPU.Build.0 = Release|Any CPU + {8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -72,5 +78,6 @@ Global {63416AEA-DA51-4D62-B566-DB7D9BC800DC} = {FBAC4C17-D755-49A9-959D-18FD6B95B543} {9D94CD6C-9451-449A-BED2-1C07D624A8E0} = {520AB1D3-C501-40FD-ACEB-7CC0D1F00B90} {590B21B0-2AFA-4329-82AD-EF180C50EB5C} = {2DAF5EF9-8F8E-4C51-BE2D-8D63CA143360} + {8C02EBBE-9EC8-4F47-9464-5A94BDE25A8F} = {520AB1D3-C501-40FD-ACEB-7CC0D1F00B90} EndGlobalSection EndGlobal diff --git a/src/GeekLearning.Storage.Azure/AzureStore.cs b/src/GeekLearning.Storage.Azure/AzureStore.cs index f6f67a6..ff125a7 100644 --- a/src/GeekLearning.Storage.Azure/AzureStore.cs +++ b/src/GeekLearning.Storage.Azure/AzureStore.cs @@ -2,6 +2,7 @@ { using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; + using Microsoft.WindowsAzure.Storage.Core; using System; using System.Collections.Generic; using System.IO; @@ -10,10 +11,8 @@ public class AzureStore : IStore { - private string connectionString; - private Lazy container; private Lazy client; - private string containerName; + private Lazy container; public AzureStore(string storeName, string connectionString, string containerName) { @@ -29,93 +28,12 @@ public AzureStore(string storeName, string connectionString, string containerNam throw new ArgumentNullException("containerName"); } - this.connectionString = connectionString; - this.containerName = containerName; - - client = new Lazy(() => CloudStorageAccount.Parse(this.connectionString).CreateCloudBlobClient()); - container = new Lazy(() => this.client.Value.GetContainerReference(this.containerName)); + this.client = new Lazy(() => CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient()); + this.container = new Lazy(() => this.client.Value.GetContainerReference(containerName)); } public string Name { get; } - private async Task InternalGetAsync(IPrivateFileReference file, bool withMetadata) - { - var azureFile = file as Internal.AzureFileReference; - if (azureFile != null) - { - return azureFile; - } - - try - { - var blob = await this.container.Value.GetBlobReferenceFromServerAsync(file.Path); - return new Internal.AzureFileReference(file.Path, blob); - } - catch (StorageException storageException) - { - if (storageException.RequestInformation.HttpStatusCode == 404) - { - return null; - } - throw; - } - } - - public async Task GetAsync(IPrivateFileReference file, bool withMetadata) - { - return await InternalGetAsync(file, withMetadata); - } - - public async Task GetAsync(Uri uri, bool withMetadata) - { - if (uri.IsAbsoluteUri) - { - return new Internal.AzureFileReference(await this.client.Value.GetBlobReferenceFromServerAsync(uri)); - } - else - { - return new Internal.AzureFileReference(await this.container.Value.GetBlobReferenceFromServerAsync(uri.ToString())); - } - } - - public async Task ReadAsync(IPrivateFileReference file) - { - var fileReference = await InternalGetAsync(file, false); - return await fileReference.ReadInMemoryAsync(); - } - - public async Task ReadAllBytesAsync(IPrivateFileReference file) - { - var fileReference = await InternalGetAsync(file, false); - return await fileReference.ReadAllBytesAsync(); - } - - public async Task ReadAllTextAsync(IPrivateFileReference file) - { - var fileReference = await InternalGetAsync(file, false); - return await fileReference.ReadAllTextAsync(); - } - - public async Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) - { - var blockBlob = this.container.Value.GetBlockBlobReference(file.Path); - await blockBlob.UploadFromStreamAsync(data); - blockBlob.Properties.ContentType = contentType; - blockBlob.Properties.CacheControl = "max-age=300, must-revalidate"; - await blockBlob.SetPropertiesAsync(); - return new Internal.AzureFileReference(blockBlob); - } - - public async Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) - { - var blockBlob = this.container.Value.GetBlockBlobReference(file.Path); - await blockBlob.UploadFromByteArrayAsync(data, 0, data.Length); - blockBlob.Properties.ContentType = contentType; - blockBlob.Properties.CacheControl = "max-age=300, must-revalidate"; - await blockBlob.SetPropertiesAsync(); - return new Internal.AzureFileReference(blockBlob); - } - public async Task ListAsync(string path, bool recursive, bool withMetadata) { if (string.IsNullOrWhiteSpace(path)) @@ -141,7 +59,7 @@ public async Task ListAsync(string path, bool recursive, bool } while (continuationToken != null); - return results.OfType().Select(blob => new Internal.AzureFileReference(blob)).ToArray(); + return results.OfType().Select(blob => new Internal.AzureFileReference(blob, withMetadata: withMetadata)).ToArray(); } public async Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) @@ -181,7 +99,7 @@ public async Task ListAsync(string path, string searchPattern, } while (continuationToken != null); - var pathMap = results.OfType().Select(blob => new Internal.AzureFileReference(blob)).ToDictionary(x => x.Path); + var pathMap = results.OfType().Select(blob => new Internal.AzureFileReference(blob, withMetadata: withMetadata)).ToDictionary(x => x.Path); var filteredResults = matcher.Execute( new Internal.AzureListDirectoryWrapper(path, @@ -190,19 +108,119 @@ 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) + { + return await this.InternalGetAsync(file, withMetadata); + } + + public async Task GetAsync(Uri uri, bool withMetadata) + { + return await this.InternalGetAsync(uri, withMetadata); + } + public async Task DeleteAsync(IPrivateFileReference file) { - var fileReference = await InternalGetAsync(file, false); + var fileReference = await this.InternalGetAsync(file); await fileReference.DeleteAsync(); } - public async Task AddMetadataAsync(IPrivateFileReference file, IDictionary metadata) + public async Task ReadAsync(IPrivateFileReference file) { - var fileReference = await InternalGetAsync(file, false); + var fileReference = await this.InternalGetAsync(file); + return await fileReference.ReadInMemoryAsync(); + } + + public async Task ReadAllBytesAsync(IPrivateFileReference file) + { + var fileReference = await this.InternalGetAsync(file); + return await fileReference.ReadAllBytesAsync(); + } + + public async Task ReadAllTextAsync(IPrivateFileReference file) + { + var fileReference = await this.InternalGetAsync(file); + return await fileReference.ReadAllTextAsync(); + } + + public async Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) + { + using (var stream = new SyncMemoryStream(data, 0, data.Length)) + { + return await this.SaveAsync(stream, file, contentType); + } + } + + public async Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) + { + var blockBlob = this.container.Value.GetBlockBlobReference(file.Path); + + if (await blockBlob.ExistsAsync()) + { + await blockBlob.FetchAttributesAsync(); + } + + await blockBlob.UploadFromStreamAsync(data); - await fileReference.AddMetadataAsync(metadata); + var reference = new Internal.AzureFileReference(blockBlob, withMetadata: true); - return fileReference; + reference.Properties.ContentType = contentType; + await reference.SavePropertiesAsync(); + + return reference; + } + + private async Task InternalGetAsync(IPrivateFileReference file, bool withMetadata = false) + { + var azureFile = file as Internal.AzureFileReference; + if (azureFile != null) + { + return azureFile; + } + + return await this.InternalGetAsync(new Uri(file.Path, UriKind.Relative), withMetadata); + } + + private async Task InternalGetAsync(Uri uri, bool withMetadata) + { + try + { + ICloudBlob blob; + + if (uri.IsAbsoluteUri) + { + // When the URI is absolute, we cannot get a simple reference to the blob, so the + // properties and metadata are fetched, even if it was not asked. + + blob = await this.client.Value.GetBlobReferenceFromServerAsync(uri); + withMetadata = true; + } + else + { + if (withMetadata) + { + blob = await this.container.Value.GetBlobReferenceFromServerAsync(uri.ToString()); + } + else + { + blob = this.container.Value.GetBlockBlobReference(uri.ToString()); + if (!(await blob.ExistsAsync())) + { + return null; + } + } + } + + return new Internal.AzureFileReference(blob, withMetadata); + } + catch (StorageException storageException) + { + if (storageException.RequestInformation.HttpStatusCode == 404) + { + return null; + } + + throw; + } } } } diff --git a/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs b/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs new file mode 100644 index 0000000..47a1614 --- /dev/null +++ b/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs @@ -0,0 +1,41 @@ +namespace GeekLearning.Storage.Azure.Internal +{ + using Microsoft.WindowsAzure.Storage.Blob; + using System; + using System.Collections.Generic; + + public class AzureFileProperties : IFileProperties + { + private const string DefaultCacheControl = "max-age=300, must-revalidate"; + private readonly ICloudBlob cloudBlob; + + public AzureFileProperties(ICloudBlob cloudBlob) + { + this.cloudBlob = cloudBlob; + if (string.IsNullOrEmpty(this.cloudBlob.Properties.CacheControl)) + { + this.cloudBlob.Properties.CacheControl = DefaultCacheControl; + } + } + + public DateTimeOffset? LastModified => this.cloudBlob.Properties.LastModified; + + public long Length => this.cloudBlob.Properties.Length; + + public string ContentType + { + get { return this.cloudBlob.Properties.ContentType; } + set { this.cloudBlob.Properties.ContentType = value; } + } + + public string ETag => this.cloudBlob.Properties.ETag; + + public string CacheControl + { + get { return this.cloudBlob.Properties.CacheControl; } + set { this.cloudBlob.Properties.CacheControl = value; } + } + + public IDictionary Metadata => this.cloudBlob.Metadata; + } +} diff --git a/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs b/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs index 032e1b0..65f6024 100644 --- a/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs +++ b/src/GeekLearning.Storage.Azure/Internal/AzureFileReference.cs @@ -3,64 +3,44 @@ using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using System; - using System.Collections.Generic; using System.IO; using System.Threading.Tasks; public class AzureFileReference : IFileReference { - private ICloudBlob cloudBlob; + private Lazy propertiesLazy; - public AzureFileReference(IListBlobItem blobItem) - : this(blobItem as ICloudBlob) - { - } - - public AzureFileReference(string path, IListBlobItem blobItem) - : this(path, blobItem as ICloudBlob) - { - } - - public AzureFileReference(string path, ICloudBlob cloudBlob) + public AzureFileReference(string path, ICloudBlob cloudBlob, bool withMetadata) { this.Path = path; - this.cloudBlob = cloudBlob; + this.CloudBlob = cloudBlob; + this.propertiesLazy = new Lazy(() => + { + if (withMetadata && cloudBlob.Metadata != null && cloudBlob.Properties != null) + { + return new AzureFileProperties(cloudBlob); + } + + throw new InvalidOperationException("Metadata are not loaded, please use withMetadata option"); + }); } - public AzureFileReference(ICloudBlob cloudBlob) : - this(cloudBlob.Name, cloudBlob) + public AzureFileReference(ICloudBlob cloudBlob, bool withMetadata) : + this(cloudBlob.Name, cloudBlob, withMetadata) { - } - public DateTimeOffset? LastModified => this.cloudBlob.Properties?.LastModified; - - public string ContentType => this.cloudBlob.Properties?.ContentType; - - public long? Length => this.cloudBlob.Properties?.Length; - public string Path { get; } - public string PublicUrl => cloudBlob.Uri.ToString(); + public IFileProperties Properties => this.propertiesLazy.Value; - public ICloudBlob CloudBlob => this.cloudBlob; + public string PublicUrl => this.CloudBlob.Uri.ToString(); - public IDictionary Metadata - { - get - { - if (this.cloudBlob.Metadata == null) - { - throw new InvalidOperationException("Metadata are not loaded, please use withMetadata option"); - } - - return this.cloudBlob.Metadata; - } - } + public ICloudBlob CloudBlob { get; } public Task DeleteAsync() { - return this.cloudBlob.DeleteAsync(); + return this.CloudBlob.DeleteAsync(); } public async Task ReadAsync() @@ -78,7 +58,7 @@ public async Task ReadInMemoryAsync() public Task UpdateAsync(Stream stream) { - return this.cloudBlob.UploadFromStreamAsync(stream); + return this.CloudBlob.UploadFromStreamAsync(stream); } public Task GetExpirableUriAsync() @@ -93,7 +73,7 @@ public async Task ReadToStreamAsync(Stream targetStream) public async Task ReadAllTextAsync() { - using (var reader = new StreamReader(await cloudBlob.OpenReadAsync(AccessCondition.GenerateEmptyCondition(), new BlobRequestOptions(), new OperationContext()))) + using (var reader = new StreamReader(await this.CloudBlob.OpenReadAsync(AccessCondition.GenerateEmptyCondition(), new BlobRequestOptions(), new OperationContext()))) { return await reader.ReadToEndAsync(); } @@ -104,19 +84,10 @@ public async Task ReadAllBytesAsync() return (await this.ReadInMemoryAsync()).ToArray(); } - public async Task AddMetadataAsync(IDictionary metadata) - { - foreach (var pair in metadata) - { - this.cloudBlob.Metadata[pair.Key] = pair.Value; - } - - await this.cloudBlob.SetMetadataAsync(); - } - - public Task SaveMetadataAsync() + public async Task SavePropertiesAsync() { - return this.cloudBlob.SetMetadataAsync(); + await this.CloudBlob.SetPropertiesAsync(); + await this.CloudBlob.SetMetadataAsync(); } } } diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs new file mode 100644 index 0000000..5d489bd --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesExtensions.cs @@ -0,0 +1,18 @@ +namespace GeekLearning.Storage +{ + using FileSystem; + using FileSystem.ExtendedProperties.FileSystem; + using FileSystem.ExtendedProperties.FileSystem.Internal; + using Microsoft.Extensions.DependencyInjection; + using System; + + public static class FileSystemExtendedPropertiesExtensions + { + public static IServiceCollection AddFileSystemExtendedProperties(this IServiceCollection services, Action configure) + { + services.Configure(configure); + services.AddTransient(); + return services; + } + } +} diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesOptions.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesOptions.cs new file mode 100644 index 0000000..2e0ca0d --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/FileSystemExtendedPropertiesOptions.cs @@ -0,0 +1,7 @@ +namespace GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem +{ + public class FileSystemExtendedPropertiesOptions + { + public string FolderNameFormat { get; set; } = ".{0}-extended-properties"; + } +} 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 new file mode 100644 index 0000000..7354067 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.xproj @@ -0,0 +1,21 @@ + + + + 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 new file mode 100644 index 0000000..afdf947 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Internal/ExtendedPropertiesProvider.cs @@ -0,0 +1,60 @@ +namespace GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem.Internal +{ + using Microsoft.Extensions.Options; + using Newtonsoft.Json; + using Storage.FileSystem.Internal; + using System.IO; + using System.Threading.Tasks; + + public class ExtendedPropertiesProvider : IExtendedPropertiesProvider + { + private readonly FileSystemExtendedPropertiesOptions options; + private readonly IStorageFactory storageFactory; + + public ExtendedPropertiesProvider( + IOptions options) + { + this.options = options.Value; + } + + public Task GetExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file) + { + var extendedPropertiesPath = this.GetExtendedPropertiesPath(storeAbsolutePath, file); + if (!File.Exists(extendedPropertiesPath)) + { + return Task.FromResult(new FileExtendedProperties()); + } + + var content = File.ReadAllText(extendedPropertiesPath); + return Task.FromResult(JsonConvert.DeserializeObject(content)); + } + + public Task SaveExtendedPropertiesAsync(string storeAbsolutePath, IPrivateFileReference file, FileExtendedProperties extendedProperties) + { + var extendedPropertiesPath = this.GetExtendedPropertiesPath(storeAbsolutePath, file); + var toStore = JsonConvert.SerializeObject(extendedProperties); + File.WriteAllText(extendedPropertiesPath, toStore); + return Task.FromResult(0); + } + + private string GetExtendedPropertiesPath(string storeAbsolutePath, IPrivateFileReference file) + { + var fullPath = Path.GetFullPath(storeAbsolutePath).TrimEnd(Path.DirectorySeparatorChar); + var rootPath = Path.GetDirectoryName(fullPath); + var storeName = Path.GetFileName(fullPath); + + var extendedPropertiesPath = Path.Combine(rootPath, string.Format(this.options.FolderNameFormat, storeName), file.Path + ".json"); + this.EnsurePathExists(extendedPropertiesPath); + return extendedPropertiesPath; + } + + private void EnsurePathExists(string path) + { + var directoryPath = Path.GetDirectoryName(path); + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + } + } +} diff --git a/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Properties/AssemblyInfo.cs b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6c872e6 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..af100df --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem/project.json @@ -0,0 +1,22 @@ +{ + "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 862611f..d9387f8 100644 --- a/src/GeekLearning.Storage.FileSystem.Server/FileSystemStorageServerMiddleware.cs +++ b/src/GeekLearning.Storage.FileSystem.Server/FileSystemStorageServerMiddleware.cs @@ -46,11 +46,17 @@ public async Task Invoke(HttpContext context) IStore store = storageFactory.GetStore(storeName, storeOptions); - var file = await store.GetAsync(context.Request.Path.Value.Substring(subPathStart + 1)); + var file = await store.GetAsync(context.Request.Path.Value.Substring(subPathStart + 1), withMetadata: true); if (file != null) { - context.Response.ContentType = "application/octet-stream"; + context.Response.ContentType = file.Properties.ContentType; context.Response.StatusCode = StatusCodes.Status200OK; + + if (!string.IsNullOrEmpty(file.Properties.ETag)) + { + context.Response.Headers.Add("ETag", new[] { file.Properties.ETag }); + } + await file.ReadToStreamAsync(context.Response.Body); return; } diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs b/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs index f729d18..5b7cb9d 100644 --- a/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs +++ b/src/GeekLearning.Storage.FileSystem/FileSystemStorageProvider.cs @@ -21,7 +21,14 @@ public FileSystemStorageProvider(IOptions options, IServicePr public IStore BuildStore(string storeName, IStorageStoreOptions storeOptions) { var publicUrlProvider = this.serviceProvider.GetService(); - return new FileSystemStore(storeName, storeOptions.Parameters["Path"], this.options.Value.RootPath, publicUrlProvider); + var extendedPropertiesProvider = this.serviceProvider.GetService(); + + return new FileSystemStore( + storeName, + storeOptions.Parameters["Path"], + this.options.Value.RootPath, + publicUrlProvider, + extendedPropertiesProvider); } } } diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs b/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs index d9d5047..32c0fd6 100644 --- a/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs +++ b/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs @@ -4,18 +4,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using System.Security.Cryptography; using System.Threading.Tasks; public class FileSystemStore : IStore { - private string absolutePath; - private IPublicUrlProvider publicUrlProvider; + private readonly IPublicUrlProvider publicUrlProvider; + private readonly IExtendedPropertiesProvider extendedPropertiesProvider; - public FileSystemStore(string storeName, string path, string rootPath, IPublicUrlProvider publicUrlProvider) + public FileSystemStore(string storeName, string path, string rootPath, IPublicUrlProvider publicUrlProvider, IExtendedPropertiesProvider extendedPropertiesProvider) { - this.publicUrlProvider = publicUrlProvider; - this.Name = storeName; - if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); @@ -23,119 +21,173 @@ public FileSystemStore(string storeName, string path, string rootPath, IPublicUr if (Path.IsPathRooted(path)) { - this.absolutePath = path; + this.AbsolutePath = path; } else { - this.absolutePath = Path.Combine(rootPath, path); + this.AbsolutePath = Path.Combine(rootPath, path); } + + this.Name = storeName; + this.publicUrlProvider = publicUrlProvider; + this.extendedPropertiesProvider = extendedPropertiesProvider; } public string Name { get; } - private Internal.FileSystemFileReference InternalGetAsync(IPrivateFileReference file) + internal string AbsolutePath { get; } + + public async Task ListAsync(string path, bool recursive, bool withMetadata) { - var reference = InternalGetOrCreateAsync(file); - if (File.Exists(reference.FileSystemPath)) + var directoryPath = (string.IsNullOrEmpty(path) || path == "/" || path == "\\") ? this.AbsolutePath : Path.Combine(this.AbsolutePath, path); + + var result = new List(); + if (Directory.Exists(directoryPath)) { - return reference; + var allResultPaths = Directory.GetFiles(directoryPath) + .Select(fp => fp.Replace(this.AbsolutePath, "").Trim('/', '\\')) + .ToList(); + + foreach (var resultPath in allResultPaths) + { + result.Add(await this.InternalGetAsync(resultPath, withMetadata)); + } } - return null; + return result.ToArray(); } - private Internal.FileSystemFileReference InternalGetOrCreateAsync(IPrivateFileReference file) + public async Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) { - var fullPath = Path.Combine(this.absolutePath, file.Path); - return new Internal.FileSystemFileReference(fullPath, file.Path, this.Name, this.publicUrlProvider); - } + var directoryPath = (string.IsNullOrEmpty(path) || path == "/" || path == "\\") ? this.AbsolutePath : Path.Combine(this.AbsolutePath, path); - public Task GetAsync(IPrivateFileReference file, bool withMetadata) - { - IFileReference fileReference = this.InternalGetAsync(file); - return Task.FromResult(fileReference); - } + var result = new List(); + if (Directory.Exists(directoryPath)) + { + var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher(StringComparison.Ordinal); + matcher.AddInclude(searchPattern); + + var matches = matcher.Execute(new Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoWrapper(new DirectoryInfo(directoryPath))); + var allResultPaths = matches.Files + .Select(match => Path.Combine(path, match.Path).Trim('/', '\\')) + .ToList(); + + foreach (var resultPath in allResultPaths) + { + result.Add(await this.InternalGetAsync(resultPath, withMetadata)); + } + } - public Task GetAsync(Uri uri, bool withMetadata) - { - throw new NotImplementedException(); + return result.ToArray(); } - public Task DeleteAsync(IPrivateFileReference file) + public async Task GetAsync(IPrivateFileReference file, bool withMetadata) { - var fileReference = InternalGetAsync(file); - return fileReference.DeleteAsync(); + return await this.InternalGetAsync(file, withMetadata); } - public Task ListAsync(string path, bool recursive, bool withMetadata) + public async Task GetAsync(Uri uri, bool withMetadata) { - var directoryPath = (string.IsNullOrEmpty(path) || path == "/" || path == "\\") ? this.absolutePath : Path.Combine(this.absolutePath, path); - if (!Directory.Exists(directoryPath)) + if (uri.IsAbsoluteUri) { - return Task.FromResult(new IFileReference[0]); + throw new InvalidOperationException("Cannot resolve an absolute URI with a FileSystem store."); } - return Task.FromResult(Directory.GetFiles(directoryPath) - .Select(fullPath => - (IFileReference)new Internal.FileSystemFileReference(fullPath, fullPath.Replace(this.absolutePath, "") - .Trim('/', '\\'), this.Name, this.publicUrlProvider)) - .ToArray()); + return await this.InternalGetAsync(uri.ToString(), withMetadata); } - public Task ListAsync(string path, string searchPattern, bool recursive, bool withMetadata) + public async Task DeleteAsync(IPrivateFileReference file) { - var directoryPath = (string.IsNullOrEmpty(path) || path == "/" || path == "\\") ? this.absolutePath : Path.Combine(this.absolutePath, path); - if (!Directory.Exists(directoryPath)) - { - return Task.FromResult(new IFileReference[0]); - } - - Microsoft.Extensions.FileSystemGlobbing.Matcher matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher(StringComparison.Ordinal); - matcher.AddInclude(searchPattern); - - var results = matcher.Execute(new Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoWrapper(new DirectoryInfo(directoryPath))); + var fileReference = await this.InternalGetAsync(file); + await fileReference.DeleteAsync(); + } - return Task.FromResult(results.Files - .Select(match => (IFileReference)new Internal.FileSystemFileReference(Path.Combine(directoryPath, match.Path), Path.Combine(path, match.Path).Trim('/', '\\'), this.Name, this.publicUrlProvider)) - .ToArray()); + public async Task ReadAsync(IPrivateFileReference file) + { + var fileReference = await this.InternalGetAsync(file); + return await fileReference.ReadAsync(); } - public Task ReadAsync(IPrivateFileReference file) + public async Task ReadAllBytesAsync(IPrivateFileReference file) { - var fileReference = InternalGetAsync(file); - return fileReference.ReadAsync(); + var fileReference = await this.InternalGetAsync(file); + return await fileReference.ReadAllBytesAsync(); } - public Task ReadAllBytesAsync(IPrivateFileReference file) + public async Task ReadAllTextAsync(IPrivateFileReference file) { - var fileReference = InternalGetAsync(file); - return fileReference.ReadAllBytesAsync(); + var fileReference = await this.InternalGetAsync(file); + return await fileReference.ReadAllTextAsync(); } - public Task ReadAllTextAsync(IPrivateFileReference file) + public async Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) { - var fileReference = InternalGetAsync(file); - return fileReference.ReadAllTextAsync(); + using (var stream = new MemoryStream(data, 0, data.Length)) + { + return await this.SaveAsync(stream, file, contentType); + } } public async Task SaveAsync(Stream data, IPrivateFileReference file, string contentType) { - var fileReference = InternalGetOrCreateAsync(file); - EnsurePathExists(fileReference.FileSystemPath); + var fileReference = await this.InternalGetAsync(file, withMetadata: true, checkIfExists: false); + this.EnsurePathExists(fileReference.FileSystemPath); + using (var fileStream = File.Open(fileReference.FileSystemPath, FileMode.Create, FileAccess.Write)) { await data.CopyToAsync(fileStream); } + var properties = fileReference.Properties as Internal.FileSystemFileProperties; + + properties.ContentType = contentType; + properties.ExtendedProperties.ETag = GenerateEtag(fileReference.FileSystemPath); + + await fileReference.SavePropertiesAsync(); + return fileReference; } - public Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType) + private async Task InternalGetAsync(IPrivateFileReference file, bool withMetadata = false, bool checkIfExists = true) { - var fileReference = InternalGetOrCreateAsync(file); - EnsurePathExists(fileReference.FileSystemPath); - File.WriteAllBytes(fileReference.FileSystemPath, data); - return Task.FromResult((IFileReference)fileReference); + var fileSystemFile = file as Internal.FileSystemFileReference; + if (fileSystemFile != null) + { + return fileSystemFile; + } + + return await this.InternalGetAsync(file.Path, withMetadata, checkIfExists); + } + + private async Task InternalGetAsync(string path, bool withMetadata, bool checkIfExists = true) + { + var fullPath = Path.Combine(this.AbsolutePath, path); + if (checkIfExists && !File.Exists(fullPath)) + { + return null; + } + + Internal.FileExtendedProperties extendedProperties = null; + if (withMetadata) + { + if (this.extendedPropertiesProvider == null) + { + throw new InvalidOperationException("There is no FileSystem extended properties provider."); + } + + extendedProperties = await this.extendedPropertiesProvider.GetExtendedPropertiesAsync( + this.AbsolutePath, + new Storage.Internal.PrivateFileReference(path)); + } + + return new Internal.FileSystemFileReference( + fullPath, + path, + this, + withMetadata, + extendedProperties, + this.publicUrlProvider, + this.extendedPropertiesProvider); } private void EnsurePathExists(string path) @@ -147,9 +199,18 @@ private void EnsurePathExists(string path) } } - public Task AddMetadataAsync(IPrivateFileReference file, IDictionary metadata) + private static string GenerateEtag(string fileSystemPath) { - throw new NotImplementedException(); + var etag = string.Empty; + using (var stream = File.Open(fileSystemPath, FileMode.Open, FileAccess.Read)) + using (var md5 = MD5.Create()) + { + var hash = md5.ComputeHash(stream); + string hex = BitConverter.ToString(hash); + etag = hex.Replace("-", ""); + } + + return $"\"{etag}\""; } } } diff --git a/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs b/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs new file mode 100644 index 0000000..843de53 --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/IExtendedPropertiesProvider.cs @@ -0,0 +1,11 @@ +namespace GeekLearning.Storage.FileSystem +{ + using System.Threading.Tasks; + + public interface IExtendedPropertiesProvider + { + Task 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 new file mode 100644 index 0000000..b644a6f --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/Internal/FileExtendedProperties.cs @@ -0,0 +1,20 @@ +namespace GeekLearning.Storage.FileSystem.Internal +{ + using System.Collections.Generic; + + public class FileExtendedProperties + { + public FileExtendedProperties() + { + this.Metadata = new Dictionary(); + } + + public string ContentType { get; set; } + + public string ETag { get; set; } + + public string CacheControl { 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 new file mode 100644 index 0000000..4f8288a --- /dev/null +++ b/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileProperties.cs @@ -0,0 +1,40 @@ +namespace GeekLearning.Storage.FileSystem.Internal +{ + using System; + using System.Collections.Generic; + using System.IO; + + public class FileSystemFileProperties : IFileProperties + { + private readonly FileInfo fileInfo; + private readonly FileExtendedProperties extendedProperties; + + public FileSystemFileProperties(string fileSystemPath, FileExtendedProperties extendedProperties) + { + this.fileInfo = new FileInfo(fileSystemPath); + this.extendedProperties = extendedProperties; + } + + public DateTimeOffset? LastModified => new DateTimeOffset(this.fileInfo.LastWriteTimeUtc, TimeZoneInfo.Local.BaseUtcOffset); + + public long Length => this.fileInfo.Length; + + public string ContentType + { + get { return this.extendedProperties.ContentType; } + set { this.extendedProperties.ContentType = value; } + } + + public string ETag => this.extendedProperties.ETag; + + public string CacheControl + { + get { return this.extendedProperties.CacheControl; } + set { this.extendedProperties.CacheControl = value; } + } + + 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 a85a29a..6ed1d9a 100644 --- a/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileReference.cs +++ b/src/GeekLearning.Storage.FileSystem/Internal/FileSystemFileReference.cs @@ -1,68 +1,62 @@ namespace GeekLearning.Storage.FileSystem.Internal { using System; - using System.Collections.Generic; using System.IO; using System.Threading.Tasks; public class FileSystemFileReference : IFileReference { - private string filePath; - private string path; - private IPublicUrlProvider publicUrlProvider; - private string storeName; - private FileInfo fileInfo; - - public FileSystemFileReference(string filePath, string path, string storeName, IPublicUrlProvider publicUrlProvider) + private readonly FileSystemStore store; + private readonly Lazy propertiesLazy; + private readonly Lazy publicUrlLazy; + private readonly IExtendedPropertiesProvider extendedPropertiesProvider; + + public FileSystemFileReference( + string filePath, + string path, + FileSystemStore store, + bool withMetadata, + FileExtendedProperties extendedProperties, + IPublicUrlProvider publicUrlProvider, + IExtendedPropertiesProvider extendedPropertiesProvider) { - this.storeName = storeName; - this.publicUrlProvider = publicUrlProvider; - this.filePath = filePath; - this.path = path.Replace('\\', '/'); - this.fileInfo = new FileInfo(this.FileSystemPath); - } - - public string FileSystemPath => this.filePath; + this.FileSystemPath = filePath; + this.Path = path.Replace('\\', '/'); + this.store = store; + this.extendedPropertiesProvider = extendedPropertiesProvider; - public IDictionary Metadata - { - get + this.propertiesLazy = new Lazy(() => { - throw new NotImplementedException(); - } - } - - public string Path => this.path; + if (withMetadata) + { + return new FileSystemFileProperties(this.FileSystemPath, extendedProperties); + } + throw new InvalidOperationException("Metadata are not loaded, please use withMetadata option"); + }); - public string PublicUrl - { - get + this.publicUrlLazy = new Lazy(() => { if (publicUrlProvider != null) { - return publicUrlProvider.GetPublicUrl(storeName, this); + return publicUrlProvider.GetPublicUrl(this.store.Name, this); } - throw new NotSupportedException("There is not FileSystemServer enabled."); - } + throw new InvalidOperationException("There is not FileSystemServer enabled."); + }); } - public DateTimeOffset? LastModified => new DateTimeOffset(this.fileInfo.LastWriteTimeUtc, TimeZoneInfo.Local.BaseUtcOffset); + public string FileSystemPath { get; } - public string ContentType - { - get - { - throw new NotImplementedException(); - } - } + public string Path { get; } + + public string PublicUrl => this.publicUrlLazy.Value; - public long? Length => this.fileInfo.Length; + public IFileProperties Properties => this.propertiesLazy.Value; public Task DeleteAsync() { - File.Delete(this.filePath); + File.Delete(this.FileSystemPath); return Task.FromResult(true); } @@ -83,13 +77,13 @@ public Task ReadAllTextAsync() public Task ReadAsync() { - Stream stream = File.OpenRead(this.filePath); + Stream stream = File.OpenRead(this.FileSystemPath); return Task.FromResult(stream); } public async Task ReadToStreamAsync(Stream targetStream) { - using (var file = File.Open(this.filePath, FileMode.Open, FileAccess.Read)) + using (var file = File.Open(this.FileSystemPath, FileMode.Open, FileAccess.Read)) { await file.CopyToAsync(targetStream); } @@ -97,20 +91,23 @@ public async Task ReadToStreamAsync(Stream targetStream) public async Task UpdateAsync(Stream stream) { - using (var file = File.Open(this.filePath, FileMode.Truncate, FileAccess.Write)) + using (var file = File.Open(this.FileSystemPath, FileMode.Truncate, FileAccess.Write)) { await stream.CopyToAsync(file); } } - public Task AddMetadataAsync(IDictionary metadata) + public Task SavePropertiesAsync() { - throw new NotImplementedException(); - } + if (this.extendedPropertiesProvider == null) + { + throw new InvalidOperationException("There is no FileSystem extended properties provider."); + } - public Task SaveMetadataAsync() - { - throw new NotImplementedException(); + return this.extendedPropertiesProvider.SaveExtendedPropertiesAsync( + this.store.AbsolutePath, + this, + (this.Properties as FileSystemFileProperties).ExtendedProperties); } } } diff --git a/src/GeekLearning.Storage/IFileProperties.cs b/src/GeekLearning.Storage/IFileProperties.cs new file mode 100644 index 0000000..a786d1b --- /dev/null +++ b/src/GeekLearning.Storage/IFileProperties.cs @@ -0,0 +1,20 @@ +namespace GeekLearning.Storage +{ + using System; + using System.Collections.Generic; + + public interface IFileProperties + { + DateTimeOffset? LastModified { get; } + + long Length { get; } + + string ContentType { get; set; } + + string ETag { get; } + + string CacheControl { get; set; } + + IDictionary Metadata { get; } + } +} diff --git a/src/GeekLearning.Storage/IFileReference.cs b/src/GeekLearning.Storage/IFileReference.cs index 171899c..b89f712 100644 --- a/src/GeekLearning.Storage/IFileReference.cs +++ b/src/GeekLearning.Storage/IFileReference.cs @@ -1,7 +1,5 @@ namespace GeekLearning.Storage { - using System; - using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -9,13 +7,7 @@ public interface IFileReference : IPrivateFileReference { string PublicUrl { get; } - DateTimeOffset? LastModified { get; } - - string ContentType { get; } - - long? Length { get; } - - IDictionary Metadata { get; } + IFileProperties Properties { get; } Task ReadToStreamAsync(Stream targetStream); @@ -31,8 +23,6 @@ public interface IFileReference : IPrivateFileReference Task GetExpirableUriAsync(); - Task AddMetadataAsync(IDictionary metadata); - - Task SaveMetadataAsync(); + Task SavePropertiesAsync(); } } diff --git a/src/GeekLearning.Storage/IStore.cs b/src/GeekLearning.Storage/IStore.cs index 831bad7..889d16f 100644 --- a/src/GeekLearning.Storage/IStore.cs +++ b/src/GeekLearning.Storage/IStore.cs @@ -1,7 +1,6 @@ namespace GeekLearning.Storage { using System; - using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -28,7 +27,5 @@ public interface IStore Task SaveAsync(byte[] data, IPrivateFileReference file, string contentType); Task SaveAsync(Stream data, IPrivateFileReference file, string contentType); - - Task AddMetadataAsync(IPrivateFileReference file, IDictionary metadata); } } diff --git a/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs b/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs index d89422f..4787584 100644 --- a/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs +++ b/tests/GeekLearning.Storage.Integration.Test/StoresFixture.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; + using System.IO; public class StoresFixture : IDisposable { @@ -35,7 +36,8 @@ public StoresFixture() services.AddStorage() .AddAzureStorage() - .AddFileSystemStorage(BasePath); + .AddFileSystemStorage(BasePath) + .AddFileSystemExtendedProperties(o => { }); services.Configure(Configuration.GetSection("Storage")); @@ -55,7 +57,7 @@ private void ResetFileSystemStore() var directoryName = Configuration["Storage:Stores:filesystem:Parameters:Path"]; var process = Process.Start(new ProcessStartInfo("robocopy.exe") { - Arguments = $"\"{System.IO.Path.Combine(BasePath, "SampleDirectory")}\" \"{System.IO.Path.Combine(BasePath, directoryName)}\" /MIR" + Arguments = $"\"{Path.Combine(BasePath, "SampleDirectory")}\" \"{Path.Combine(BasePath, directoryName)}\" /MIR" }); if (!process.WaitForExit(30000)) @@ -100,6 +102,21 @@ private void ResetAzureStore() public void Dispose() { container.DeleteIfExistsAsync().Wait(); + + var fileSystemPath = Configuration["Storage:Stores:filesystem:Parameters:Path"]; + var folderNameFormat = Configuration["Storage:ExtendedPropertiesFolderNameFormat"]; + + var directoryName = Path.Combine(BasePath, fileSystemPath); + if (Directory.Exists(directoryName)) + { + Directory.Delete(directoryName, true); + } + + directoryName = Path.Combine(BasePath, string.Format(folderNameFormat, fileSystemPath)); + if (Directory.Exists(directoryName)) + { + Directory.Delete(directoryName, true); + } } } } diff --git a/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs b/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs index 31176df..4d5124e 100644 --- a/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs +++ b/tests/GeekLearning.Storage.Integration.Test/UpdateTests.cs @@ -36,6 +36,37 @@ public async Task WriteAllText(string storeName) Assert.Equal(textToWrite, readFromWrittenFile); } + [Theory(DisplayName = nameof(ETagShouldBeTheSameWithSameContent)), InlineData("azure"), InlineData("filesystem")] + public async Task ETagShouldBeTheSameWithSameContent(string storeName) + { + var storageFactory = this.storeFixture.Services.GetRequiredService(); + + var store = storageFactory.GetStore(storeName); + var textToWrite = "ETag Test Compute"; + var filePath = "Update/etag-same.txt"; + + var savedReference = await store.SaveAsync(Encoding.UTF8.GetBytes(textToWrite), filePath, "text/plain"); + var readReference = await store.GetAsync(filePath, withMetadata: true); + + Assert.Equal(savedReference.Properties.ETag, readReference.Properties.ETag); + } + + [Theory(DisplayName = nameof(ETagShouldBeDifferentWithDifferentContent)), InlineData("azure"), InlineData("filesystem")] + public async Task ETagShouldBeDifferentWithDifferentContent(string storeName) + { + var storageFactory = this.storeFixture.Services.GetRequiredService(); + + var store = storageFactory.GetStore(storeName); + var textToWrite = "ETag Test Compute"; + var filePath = "Update/etag-different.txt"; + var textToUpdate = "ETag Test Compute 2"; + + var savedReference = await store.SaveAsync(Encoding.UTF8.GetBytes(textToWrite), filePath, "text/plain"); + var updatedReference = await store.SaveAsync(Encoding.UTF8.GetBytes(textToUpdate), filePath, "text/plain"); + + Assert.NotEqual(savedReference.Properties.ETag, updatedReference.Properties.ETag); + } + [Theory(DisplayName = nameof(SaveStream)), InlineData("azure"), InlineData("filesystem")] public async Task SaveStream(string storeName) { @@ -52,7 +83,7 @@ public async Task SaveStream(string storeName) Assert.Equal(textToWrite, readFromWrittenFile); } - [Theory(DisplayName = nameof(AddMetatadaRoundtrip)), InlineData("azure")] + [Theory(DisplayName = nameof(AddMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] public async Task AddMetatadaRoundtrip(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -61,23 +92,22 @@ public async Task AddMetatadaRoundtrip(string storeName) var testFile = "Metadata/TextFile.txt"; - var file = await store.GetAsync(testFile); + var file = await store.GetAsync(testFile, withMetadata: true); var id = Guid.NewGuid().ToString(); - await file.AddMetadataAsync(new Dictionary - { - ["id"] = id - }); + file.Properties.Metadata.Add("newid", id); + + await file.SavePropertiesAsync(); - file = await store.GetAsync(testFile); + file = await store.GetAsync(testFile, withMetadata: true); - var actualId = file.Metadata["id"]; + var actualId = file.Properties.Metadata["newid"]; Assert.Equal(id, actualId); } - [Theory(DisplayName = nameof(SaveMetatadaRoundtrip)), InlineData("azure")] + [Theory(DisplayName = nameof(SaveMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] public async Task SaveMetatadaRoundtrip(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -86,22 +116,22 @@ public async Task SaveMetatadaRoundtrip(string storeName) var testFile = "Metadata/TextFile.txt"; - var file = await store.GetAsync(testFile); + var file = await store.GetAsync(testFile, withMetadata: true); var id = Guid.NewGuid().ToString(); - file.Metadata["id"] = id; + file.Properties.Metadata["id"] = id; - await file.SaveMetadataAsync(); + await file.SavePropertiesAsync(); - file = await store.GetAsync(testFile); + file = await store.GetAsync(testFile, withMetadata: true); - var actualId = file.Metadata["id"]; + var actualId = file.Properties.Metadata["id"]; Assert.Equal(id, actualId); } - [Theory(DisplayName = nameof(ListMetatadaRoundtrip)), InlineData("azure")] + [Theory(DisplayName = nameof(ListMetatadaRoundtrip)), InlineData("azure"), InlineData("filesystem")] public async Task ListMetatadaRoundtrip(string storeName) { var storageFactory = this.storeFixture.Services.GetRequiredService(); @@ -110,13 +140,13 @@ public async Task ListMetatadaRoundtrip(string storeName) var testFile = "Metadata/TextFile.txt"; - var file = await store.GetAsync(testFile); + var file = await store.GetAsync(testFile, withMetadata: true); var id = Guid.NewGuid().ToString(); - file.Metadata["id"] = id; + file.Properties.Metadata["id"] = id; - await file.SaveMetadataAsync(); + await file.SavePropertiesAsync(); var files = await store.ListAsync("Metadata", withMetadata: true); @@ -126,7 +156,7 @@ public async Task ListMetatadaRoundtrip(string storeName) { if (aFile.Path == testFile) { - actualId = aFile.Metadata["id"]; + actualId = aFile.Properties.Metadata["id"]; } } diff --git a/tests/GeekLearning.Storage.Integration.Test/appsettings.json b/tests/GeekLearning.Storage.Integration.Test/appsettings.json index 22a101c..c016376 100644 --- a/tests/GeekLearning.Storage.Integration.Test/appsettings.json +++ b/tests/GeekLearning.Storage.Integration.Test/appsettings.json @@ -15,6 +15,7 @@ "Container": "templates" } } - } + }, + "ExtendedPropertiesFolderNameFormat": ".{0}-extended-properties" } } diff --git a/tests/GeekLearning.Storage.Integration.Test/project.json b/tests/GeekLearning.Storage.Integration.Test/project.json index 5f6f1f5..56f3c44 100644 --- a/tests/GeekLearning.Storage.Integration.Test/project.json +++ b/tests/GeekLearning.Storage.Integration.Test/project.json @@ -27,7 +27,8 @@ "GeekLearning.Storage": "*", "GeekLearning.Storage.Azure": "*", - "GeekLearning.Storage.FileSystem": "*" + "GeekLearning.Storage.FileSystem": "*", + "GeekLearning.Storage.FileSystem.ExtendedProperties.FileSystem": "*" }, "frameworks": {