Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions src/GeekLearning.Storage.Azure/AzureStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,29 +156,53 @@ public async ValueTask<string> ReadAllTextAsync(IPrivateFileReference file)
return await fileReference.ReadAllTextAsync();
}

public async ValueTask<IFileReference> SaveAsync(byte[] data, IPrivateFileReference file, string contentType)
public async ValueTask<IFileReference> 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<IFileReference> SaveAsync(Stream data, IPrivateFileReference file, string contentType)
public async ValueTask<IFileReference> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public string CacheControl
set { this.cloudBlob.Properties.CacheControl = value; }
}

public string ContentMD5 => this.cloudBlob.Properties.ContentMD5;

public IDictionary<string, string> Metadata => this.decodedMetadata;

internal async Task SaveAsync()
Expand Down
46 changes: 34 additions & 12 deletions src/GeekLearning.Storage.FileSystem/FileSystemStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,28 +120,45 @@ public async ValueTask<string> ReadAllTextAsync(IPrivateFileReference file)
return await fileReference.ReadAllTextAsync();
}

public async ValueTask<IFileReference> SaveAsync(byte[] data, IPrivateFileReference file, string contentType)
public async ValueTask<IFileReference> 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<IFileReference> SaveAsync(Stream data, IPrivateFileReference file, string contentType)
public async ValueTask<IFileReference> 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();

Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="1.1.1" />
<PackageReference Include="System.ValueTuple" Version="4.3.1" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public FileExtendedProperties()

public string CacheControl { get; set; }

public string ContentMD5 { get; set; }

public IDictionary<string, string> Metadata { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public string CacheControl
set { this.extendedProperties.CacheControl = value; }
}

public string ContentMD5 => this.extendedProperties.ContentMD5;

public IDictionary<string, string> Metadata => this.extendedProperties.Metadata;

internal FileExtendedProperties ExtendedProperties => this.extendedProperties;
Expand Down
12 changes: 12 additions & 0 deletions src/GeekLearning.Storage/Exceptions/FileAlreadyExistsException.cs
Original file line number Diff line number Diff line change
@@ -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}.")
{
}
}
}
1 change: 1 addition & 0 deletions src/GeekLearning.Storage/GeekLearning.Storage.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<ItemGroup>
<None Include="ISharedAccessPolicy.cs" />
<None Include="OverwritePolicy.cs" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions src/GeekLearning.Storage/IFileProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public interface IFileProperties

string CacheControl { get; set; }

string ContentMD5 { get; }

IDictionary<string, string> Metadata { get; }
}
}
4 changes: 2 additions & 2 deletions src/GeekLearning.Storage/IStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public interface IStore

ValueTask<string> ReadAllTextAsync(IPrivateFileReference file);

ValueTask<IFileReference> SaveAsync(byte[] data, IPrivateFileReference file, string contentType);
ValueTask<IFileReference> SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always);

ValueTask<IFileReference> SaveAsync(Stream data, IPrivateFileReference file, string contentType);
ValueTask<IFileReference> SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always);

ValueTask<string> GetSharedAccessSignatureAsync(ISharedAccessPolicy policy);
}
Expand Down
8 changes: 4 additions & 4 deletions src/GeekLearning.Storage/IStoreExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ public static ValueTask<byte[]> ReadAllBytesAsync(this IStore store, string path
public static ValueTask<string> ReadAllTextAsync(this IStore store, string path)
=> store.ReadAllTextAsync(new Internal.PrivateFileReference(path));

public static ValueTask<IFileReference> SaveAsync(this IStore store, byte[] data, string path, string contentType)
=> store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType);
public static ValueTask<IFileReference> 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<IFileReference> SaveAsync(this IStore store, Stream data, string path, string contentType)
=> store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType);
public static ValueTask<IFileReference> SaveAsync(this IStore store, Stream data, string path, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always)
=> store.SaveAsync(data, new Internal.PrivateFileReference(path), contentType, overwritePolicy);
}
}
4 changes: 2 additions & 2 deletions src/GeekLearning.Storage/Internal/GenericStoreProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public GenericStoreProxy(IStorageFactory factory, IOptions<TOptions> options)

public ValueTask<Stream> ReadAsync(IPrivateFileReference file) => this.innerStore.ReadAsync(file);

public ValueTask<IFileReference> SaveAsync(Stream data, IPrivateFileReference file, string contentType) => this.innerStore.SaveAsync(data, file, contentType);
public ValueTask<IFileReference> SaveAsync(Stream data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) => this.innerStore.SaveAsync(data, file, contentType, overwritePolicy);

public ValueTask<IFileReference> SaveAsync(byte[] data, IPrivateFileReference file, string contentType) => this.innerStore.SaveAsync(data, file, contentType);
public ValueTask<IFileReference> SaveAsync(byte[] data, IPrivateFileReference file, string contentType, OverwritePolicy overwritePolicy = OverwritePolicy.Always) => this.innerStore.SaveAsync(data, file, contentType, overwritePolicy);

public ValueTask<string> GetSharedAccessSignatureAsync(ISharedAccessPolicy policy) => this.innerStore.GetSharedAccessSignatureAsync(policy);
}
Expand Down
9 changes: 9 additions & 0 deletions src/GeekLearning.Storage/OverwritePolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace GeekLearning.Storage
{
public enum OverwritePolicy
{
Always = 0,
IfContentModified = 1,
Never = 2,
}
}