diff --git a/src/GeekLearning.Storage.Azure/AzureStore.cs b/src/GeekLearning.Storage.Azure/AzureStore.cs index f1d91ff..fd08c45 100644 --- a/src/GeekLearning.Storage.Azure/AzureStore.cs +++ b/src/GeekLearning.Storage.Azure/AzureStore.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using System.Security.Cryptography; using System.Threading.Tasks; public class AzureStore : IStore @@ -155,29 +156,53 @@ public async ValueTask ReadAllTextAsync(IPrivateFileReference file) return await fileReference.ReadAllTextAsync(); } - public async ValueTask 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 ValueTask 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; } diff --git a/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs b/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs index 8d02273..013efe1 100644 --- a/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs +++ b/src/GeekLearning.Storage.Azure/Internal/AzureFileProperties.cs @@ -49,6 +49,8 @@ public string CacheControl set { this.cloudBlob.Properties.CacheControl = value; } } + public string ContentMD5 => this.cloudBlob.Properties.ContentMD5; + public IDictionary Metadata => this.decodedMetadata; internal async Task SaveAsync() diff --git a/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs b/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs index bf95af1..5d8b5af 100644 --- a/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs +++ b/src/GeekLearning.Storage.FileSystem/FileSystemStore.cs @@ -120,28 +120,45 @@ public async ValueTask ReadAllTextAsync(IPrivateFileReference file) return await fileReference.ReadAllTextAsync(); } - public async ValueTask 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 ValueTask 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(); @@ -198,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 index bae7d7f..283f2c0 100644 --- a/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj +++ b/src/GeekLearning.Storage.FileSystem/GeekLearning.Storage.FileSystem.csproj @@ -18,6 +18,7 @@ + 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/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/GeekLearning.Storage.csproj b/src/GeekLearning.Storage/GeekLearning.Storage.csproj index d8fcbc9..906ec1e 100644 --- a/src/GeekLearning.Storage/GeekLearning.Storage.csproj +++ b/src/GeekLearning.Storage/GeekLearning.Storage.csproj @@ -24,6 +24,7 @@ + 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/IStore.cs b/src/GeekLearning.Storage/IStore.cs index bf21425..f82c7e8 100644 --- a/src/GeekLearning.Storage/IStore.cs +++ b/src/GeekLearning.Storage/IStore.cs @@ -26,9 +26,9 @@ public interface IStore ValueTask ReadAllTextAsync(IPrivateFileReference file); - ValueTask SaveAsync(byte[] data, IPrivateFileReference file, string contentType); + ValueTask SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always); - ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType); + ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always); ValueTask GetSharedAccessSignatureAsync(ISharedAccessPolicy policy); } diff --git a/src/GeekLearning.Storage/IStoreExtensions.cs b/src/GeekLearning.Storage/IStoreExtensions.cs index 30f789f..e063ad0 100644 --- a/src/GeekLearning.Storage/IStoreExtensions.cs +++ b/src/GeekLearning.Storage/IStoreExtensions.cs @@ -26,10 +26,10 @@ public static ValueTask ReadAllBytesAsync(this IStore store, string 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) - => store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); + 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) - => store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType); + 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/Internal/GenericStoreProxy.cs b/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs index c8582c0..8d4f4d3 100644 --- a/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs +++ b/src/GeekLearning.Storage/Internal/GenericStoreProxy.cs @@ -41,9 +41,9 @@ public GenericStoreProxy(IStorageFactory factory, IOptions options) public ValueTask ReadAsync(IPrivateFileReference file) => this.innerStore.ReadAsync(file); - public ValueTask SaveAsync(Stream data, IPrivateFileReference file, string contentType) => this.innerStore.SaveAsync(data, file, contentType); + 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) => this.innerStore.SaveAsync(data, file, contentType); + 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/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