mirror of https://github.com/Squidex/squidex.git
11 changed files with 366 additions and 0 deletions
@ -0,0 +1,147 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AzureBlobAssetStore.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.WindowsAzure.Storage; |
||||
|
using Microsoft.WindowsAzure.Storage.Blob; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.AzureStorage |
||||
|
{ |
||||
|
public class AzureBlobAssetStore : IAssetStore, IExternalSystem |
||||
|
{ |
||||
|
private readonly IBlobContainerProvider blobContainerProvider; |
||||
|
private readonly string containerName; |
||||
|
private CloudBlobContainer blobContainer; |
||||
|
private const string AssetVersion = "AssetVersion"; |
||||
|
private const string AssetId = "AssetId"; |
||||
|
|
||||
|
public AzureBlobAssetStore(IBlobContainerProvider blobContainerProvider, string containerName) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(containerName, nameof(containerName)); |
||||
|
Guard.NotNull(blobContainerProvider, nameof(blobContainerProvider)); |
||||
|
|
||||
|
this.blobContainerProvider = blobContainerProvider; |
||||
|
this.containerName = containerName; |
||||
|
} |
||||
|
|
||||
|
public async Task DownloadAsync(string id, long version, string suffix, Stream stream) |
||||
|
{ |
||||
|
var blobName = GetObjectName(id, suffix); |
||||
|
var blob = blobContainer.GetBlockBlobReference(blobName); |
||||
|
|
||||
|
if (!await blob.ExistsAsync()) |
||||
|
return; |
||||
|
|
||||
|
// look for the requested version
|
||||
|
// first check if the original blob has the requested version
|
||||
|
if (blob.Metadata.TryGetValue(AssetVersion, out string verionStr)) |
||||
|
{ |
||||
|
if (long.TryParse(verionStr, out long blobVersion)) |
||||
|
{ |
||||
|
// if not, then look for that snapshot which has the requested version number.
|
||||
|
if (blobVersion != version) |
||||
|
{ |
||||
|
var snapshotBlob = await FindSnapshotAsync(id, version); |
||||
|
blob = snapshotBlob ?? blob; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await blob.DownloadToStreamAsync(stream); |
||||
|
} |
||||
|
|
||||
|
public async Task UploadAsync(string id, long version, string suffix, Stream stream) |
||||
|
{ |
||||
|
var blobName = GetObjectName(id, suffix); |
||||
|
var blob = blobContainer.GetBlockBlobReference(blobName); |
||||
|
|
||||
|
if (await blob.ExistsAsync()) |
||||
|
{ |
||||
|
// if it's already exist create a snapshot, and we overwrite the source blob of the snapshot.
|
||||
|
// NOTE: not sure if there is an
|
||||
|
await CreateVersioningSnapshotAsync(blob, id, version); |
||||
|
} |
||||
|
|
||||
|
if (!blob.Metadata.ContainsKey(AssetVersion)) |
||||
|
blob.Metadata.Add(AssetVersion, version.ToString()); |
||||
|
else |
||||
|
blob.Metadata[AssetVersion] = version.ToString(); |
||||
|
|
||||
|
blob.Metadata[AssetId] = id; |
||||
|
|
||||
|
await blob.UploadFromStreamAsync(stream); |
||||
|
await blob.SetMetadataAsync(); |
||||
|
} |
||||
|
|
||||
|
public async void Connect() |
||||
|
{ |
||||
|
blobContainer = await blobContainerProvider.GetContainerAsync(containerName); |
||||
|
} |
||||
|
|
||||
|
private string GetObjectName(string id, string suffix) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
if (blobContainer == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("No connection established yet."); |
||||
|
} |
||||
|
|
||||
|
var name = $"{id}"; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(suffix)) |
||||
|
{ |
||||
|
name += "_" + suffix; |
||||
|
} |
||||
|
|
||||
|
return name; |
||||
|
} |
||||
|
|
||||
|
private async Task CreateVersioningSnapshotAsync(CloudBlockBlob blob, string id, long version) |
||||
|
{ |
||||
|
var metadata = new Dictionary<string, string>(); |
||||
|
metadata.Add(AssetVersion, version.ToString()); |
||||
|
metadata.Add(AssetId, id); |
||||
|
await blob.CreateSnapshotAsync(metadata, |
||||
|
null, null, null); |
||||
|
} |
||||
|
|
||||
|
private async Task<CloudBlockBlob> FindSnapshotAsync(string id, long requestedVersion) |
||||
|
{ |
||||
|
CloudBlockBlob resultBlob = null; |
||||
|
BlobContinuationToken token = null; |
||||
|
do |
||||
|
{ |
||||
|
var listingResult = await blobContainer.ListBlobsSegmentedAsync(null, true, |
||||
|
BlobListingDetails.Snapshots, 10, token, null, null); |
||||
|
token = listingResult.ContinuationToken; |
||||
|
|
||||
|
foreach (CloudBlob snapshotBlob in listingResult.Results.Cast<CloudBlob>()) |
||||
|
{ |
||||
|
if (snapshotBlob.Metadata.TryGetValue(AssetVersion, out string snapshotVersionStr) |
||||
|
&& snapshotBlob.Metadata.TryGetValue(AssetId, out string snapshotAssetId) |
||||
|
&& long.TryParse(snapshotVersionStr, out long snapshotVersion) |
||||
|
&& snapshotVersion == requestedVersion |
||||
|
&& snapshotAssetId == id) |
||||
|
{ |
||||
|
resultBlob = snapshotBlob as CloudBlockBlob; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
while (token != null); |
||||
|
|
||||
|
return resultBlob; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AzureStorageException.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.WindowsAzure.Storage; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.AzureStorage |
||||
|
{ |
||||
|
[Serializable] |
||||
|
public class AzureStorageException : StorageException |
||||
|
{ |
||||
|
public AzureStorageException() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AzureStorageException(string message) : base(message) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AzureStorageException(string message, Exception inner) : base(message, inner) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// BlobContainerProvider.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.WindowsAzure.Storage.Blob; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.AzureStorage |
||||
|
{ |
||||
|
public class BlobContainerProvider : IBlobContainerProvider |
||||
|
{ |
||||
|
private readonly IStorageAccountManager accountManager; |
||||
|
|
||||
|
public BlobContainerProvider(IStorageAccountManager accountManager) |
||||
|
{ |
||||
|
this.accountManager = accountManager; |
||||
|
} |
||||
|
|
||||
|
public async Task<CloudBlobContainer> GetContainerAsync(string name) |
||||
|
{ |
||||
|
var client = accountManager.CreateCloudBlobClient(); |
||||
|
var saneName = name.Replace("@", "-").Replace(".", "-"); |
||||
|
var container = client.GetContainerReference(saneName); |
||||
|
await container.CreateIfNotExistsAsync(); |
||||
|
|
||||
|
return container; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IBlobContainerProvider.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.WindowsAzure.Storage.Blob; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.AzureStorage |
||||
|
{ |
||||
|
public interface IBlobContainerProvider |
||||
|
{ |
||||
|
Task<CloudBlobContainer> GetContainerAsync(string name); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IStorageAccountManager.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.WindowsAzure.Storage.Blob; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.AzureStorage |
||||
|
{ |
||||
|
public interface IStorageAccountManager |
||||
|
{ |
||||
|
CloudBlobClient CreateCloudBlobClient(); |
||||
|
|
||||
|
string GetSharedAccessSignature(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard1.6</TargetFramework> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="WindowsAzure.Storage" Version="8.2.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,47 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IStorageAccountManager.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.WindowsAzure.Storage; |
||||
|
using Microsoft.WindowsAzure.Storage.Blob; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.AzureStorage |
||||
|
{ |
||||
|
public class StorageAccountManager : IStorageAccountManager |
||||
|
{ |
||||
|
private readonly CloudStorageAccount storageAccount; |
||||
|
|
||||
|
public StorageAccountManager(string storageAccountConnectionString) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
storageAccount = CloudStorageAccount.Parse(storageAccountConnectionString); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
when (ex is FormatException || ex is ArgumentException) |
||||
|
{ |
||||
|
throw new AzureStorageException("Invalid storage account information provided. Please confirm the AccountName and AccountKey are valid in the app settings file."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public CloudBlobClient CreateCloudBlobClient() |
||||
|
{ |
||||
|
return storageAccount.CreateCloudBlobClient(); |
||||
|
} |
||||
|
|
||||
|
public string GetSharedAccessSignature() |
||||
|
{ |
||||
|
return storageAccount.GetSharedAccessSignature(new SharedAccessAccountPolicy() |
||||
|
{ |
||||
|
SharedAccessStartTime = DateTimeOffset.UtcNow, |
||||
|
SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddHours(1), |
||||
|
Permissions = SharedAccessAccountPermissions.Read | SharedAccessAccountPermissions.List |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue