Browse Source

Asset store improved.

pull/76/head
Sebastian Stehle 9 years ago
parent
commit
2bf10cfcb5
  1. 43
      src/Squidex.Domain.Apps.Write/Assets/AssetCommandHandler.cs
  2. 56
      src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
  3. 74
      src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  4. 6
      src/Squidex.Infrastructure/Assets/IAssetStore.cs
  5. 8
      src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs
  6. 4
      src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs
  7. 25
      tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandHandlerTests.cs
  8. 16
      tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/HandlerTestBase.cs
  9. 53
      tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs

43
src/Squidex.Domain.Apps.Write/Assets/AssetCommandHandler.cs

@ -38,38 +38,55 @@ namespace Squidex.Domain.Apps.Write.Assets
protected async Task On(CreateAsset command, CommandContext context)
{
await handler.CreateAsync<AssetDomainObject>(context, async c =>
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
try
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
var asset = await handler.CreateAsync<AssetDomainObject>(context, async a =>
{
a.Create(command);
c.Create(command);
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead());
await assetStore.UploadAsync(c.Id.ToString(), c.FileVersion, null, command.File.OpenRead());
context.Succeed(EntityCreatedResult.Create(a.Id, a.Version));
});
context.Succeed(EntityCreatedResult.Create(c.Id, c.Version));
});
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.Id.ToString(), asset.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
}
}
protected async Task On(UpdateAsset command, CommandContext context)
{
await handler.UpdateAsync<AssetDomainObject>(context, async c =>
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
try
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
var asset = await handler.UpdateAsync<AssetDomainObject>(context, async a =>
{
a.Update(command);
c.Update(command);
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead());
});
await assetStore.UploadAsync(c.Id.ToString(), c.FileVersion, null, command.File.OpenRead());
});
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.Id.ToString(), asset.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
}
}
protected Task On(RenameAsset command, CommandContext context)
{
return handler.UpdateAsync<AssetDomainObject>(context, c => c.Rename(command));
return handler.UpdateAsync<AssetDomainObject>(context, a => a.Rename(command));
}
protected Task On(DeleteAsset command, CommandContext context)
{
return handler.UpdateAsync<AssetDomainObject>(context, c => c.Delete(command));
return handler.UpdateAsync<AssetDomainObject>(context, a => a.Delete(command));
}
public Task<bool> HandleAsync(CommandContext context)

56
src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs

@ -8,6 +8,7 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Google;
@ -41,6 +42,36 @@ namespace Squidex.Infrastructure.Assets
}
}
public Task UploadTemporaryAsync(string name, Stream stream)
{
return storageClient.UploadObjectAsync(bucketName, name, "application/octet-stream", stream);
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
{
var objectName = GetObjectName(id, version, suffix);
await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream);
}
public async Task CopyTemporaryAsync(string name, string id, long version, string suffix)
{
var objectName = GetObjectName(id, version, suffix);
try
{
await storageClient.CopyObjectAsync(bucketName, name, bucketName, objectName);
}
catch (GoogleApiException ex)
{
if (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
throw new AssetNotFoundException($"Asset {name} not found.", ex);
}
throw;
}
}
public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
{
var objectName = GetObjectName(id, version, suffix);
@ -59,11 +90,16 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
public async Task DeleteTemporaryAsync(string name)
{
var objectName = GetObjectName(id, version, suffix);
await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream);
try
{
await storageClient.DeleteObjectAsync(bucketName, name);
}
catch
{
// ignored
}
}
private string GetObjectName(string id, long version, string suffix)
@ -75,14 +111,14 @@ namespace Squidex.Infrastructure.Assets
throw new InvalidOperationException("No connection established yet.");
}
var name = $"{id}_{version}";
if (!string.IsNullOrWhiteSpace(suffix))
{
name += "_" + suffix;
}
var name = GetFileName(id, version, suffix);
return name;
}
private static string GetFileName(string id, long version, string suffix)
{
return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x)));
}
}
}

74
src/Squidex.Infrastructure/Assets/FolderAssetStore.cs

@ -7,8 +7,10 @@
// ==========================================================================
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Assets
{
@ -49,6 +51,26 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task UploadTemporaryAsync(string name, Stream stream)
{
var file = GetFile(name);
using (var fileStream = file.OpenWrite())
{
await stream.CopyToAsync(fileStream);
}
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
{
var file = GetFile(id, version, suffix);
using (var fileStream = file.OpenWrite())
{
await stream.CopyToAsync(fileStream);
}
}
public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
{
var file = GetFile(id, version, suffix);
@ -66,28 +88,60 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
public Task CopyTemporaryAsync(string name, string id, long version, string suffix)
{
var file = GetFile(id, version, suffix);
try
{
var file = GetFile(name);
using (var fileStream = file.OpenWrite())
file.CopyTo(GetPath(id, version, suffix));
return TaskHelper.Done;
}
catch (FileNotFoundException ex)
{
await stream.CopyToAsync(fileStream);
throw new AssetNotFoundException($"Asset {name} not found.", ex);
}
}
private FileInfo GetFile(string id, long version, string suffix)
public Task DeleteTemporaryAsync(string name)
{
Guard.NotNullOrEmpty(id, nameof(id));
try
{
var file = GetFile(name);
var path = Path.Combine(directory.FullName, $"{id}_{version}");
file.Delete();
if (!string.IsNullOrWhiteSpace(suffix))
return TaskHelper.Done;
}
catch (FileNotFoundException ex)
{
path += "_" + suffix;
throw new AssetNotFoundException($"Asset {name} not found.", ex);
}
}
return new FileInfo(path);
private FileInfo GetFile(string id, long version, string suffix)
{
Guard.NotNullOrEmpty(id, nameof(id));
return GetFile(GetPath(id, version, suffix));
}
private FileInfo GetFile(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
return new FileInfo(GetPath(name));
}
private string GetPath(string name)
{
return Path.Combine(directory.FullName, name);
}
private string GetPath(string id, long version, string suffix)
{
return Path.Combine(directory.FullName, string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x))));
}
}
}

6
src/Squidex.Infrastructure/Assets/IAssetStore.cs

@ -13,8 +13,14 @@ namespace Squidex.Infrastructure.Assets
{
public interface IAssetStore
{
Task CopyTemporaryAsync(string name, string id, long version, string suffix);
Task DownloadAsync(string id, long version, string suffix, Stream stream);
Task UploadTemporaryAsync(string name, Stream stream);
Task UploadAsync(string id, long version, string suffix, Stream stream);
Task DeleteTemporaryAsync(string name);
}
}

8
src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs

@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
this.domainObjectRepository = domainObjectRepository;
}
public async Task CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IAggregate
public async Task<T> CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IAggregate
{
Guard.NotNull(creator, nameof(creator));
Guard.NotNull(context, nameof(context));
@ -54,9 +54,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
context.Succeed(new EntityCreatedResult<Guid>(aggregate.Id, aggregate.Version));
}
return aggregate;
}
public async Task UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IAggregate
public async Task<T> UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IAggregate
{
Guard.NotNull(updater, nameof(updater));
Guard.NotNull(context, nameof(context));
@ -72,6 +74,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
context.Succeed(new EntitySavedResult(aggregate.Version));
}
return aggregate;
}
private static IAggregateCommand GetCommand(CommandContext context)

4
src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs

@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
public interface IAggregateHandler
{
Task CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IAggregate;
Task<T> CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IAggregate;
Task UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IAggregate;
Task<T> UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IAggregate;
}
}

25
tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandHandlerTests.cs

@ -17,6 +17,7 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Tasks;
using Xunit;
// ReSharper disable ImplicitlyCapturedClosure
// ReSharper disable ConvertToConstant.Local
namespace Squidex.Domain.Apps.Write.Assets
@ -44,11 +45,11 @@ namespace Squidex.Domain.Apps.Write.Assets
[Fact]
public async Task Create_should_create_asset()
{
SetupStore(0);
SetupImageInfo();
var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file });
SetupStore(0, context.ContextId);
SetupImageInfo();
await TestCreate(asset, async _ =>
{
await sut.HandleAsync(context);
@ -63,13 +64,13 @@ namespace Squidex.Domain.Apps.Write.Assets
[Fact]
public async Task Update_should_update_domain_object()
{
SetupStore(1);
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
SetupStore(1, context.ContextId);
SetupImageInfo();
CreateAsset();
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
await TestUpdate(asset, async _ =>
{
await sut.HandleAsync(context);
@ -117,10 +118,18 @@ namespace Squidex.Domain.Apps.Write.Assets
.Verifiable();
}
private void SetupStore(long version)
private void SetupStore(long version, Guid commitId)
{
assetStore
.Setup(x => x.UploadAsync(assetId.ToString(), version, null, stream)).Returns(TaskHelper.Done)
.Setup(x => x.UploadTemporaryAsync(commitId.ToString(), stream)).Returns(TaskHelper.Done)
.Verifiable();
assetStore
.Setup(x => x.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).Returns(TaskHelper.Done)
.Verifiable();
assetStore
.Setup(x => x.DeleteTemporaryAsync(commitId.ToString())).Returns(TaskHelper.Done)
.Verifiable();
}
}

16
tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/HandlerTestBase.cs

@ -34,18 +34,26 @@ namespace Squidex.Domain.Apps.Write.TestHelpers
IsUpdated = false;
}
public Task CreateAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IAggregate
public async Task<V> CreateAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IAggregate
{
IsCreated = true;
return creator(domainObject as V);
var @do = domainObject as V;
await creator(domainObject as V);
return @do;
}
public Task UpdateAsync<V>(CommandContext context, Func<V, Task> updater) where V : class, IAggregate
public async Task<V> UpdateAsync<V>(CommandContext context, Func<V, Task> updater) where V : class, IAggregate
{
IsUpdated = true;
return updater(domainObject as V);
var @do = domainObject as V;
await updater(domainObject as V);
return @do;
}
}

53
tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs

@ -48,11 +48,19 @@ namespace Squidex.Infrastructure.Assets
}
[Fact]
public Task Should_throw_exception_if_asset_not_found()
public Task Should_throw_exception_if_asset_to_download_is_not_found()
{
sut.Connect();
return Assert.ThrowsAsync<AssetNotFoundException>(() => sut.DownloadAsync(Guid.NewGuid().ToString(), 1, "suffix", new MemoryStream()));
return Assert.ThrowsAsync<AssetNotFoundException>(() => sut.DownloadAsync(Id(), 1, "suffix", new MemoryStream()));
}
[Fact]
public Task Should_throw_exception_if_asset_to_copy_is_not_found()
{
sut.Connect();
return Assert.ThrowsAsync<AssetNotFoundException>(() => sut.CopyTemporaryAsync(Id(), Id(), 1, null));
}
[Fact]
@ -60,7 +68,7 @@ namespace Squidex.Infrastructure.Assets
{
sut.Connect();
var assetId = Guid.NewGuid().ToString();
var assetId = Id();
var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 });
await sut.UploadAsync(assetId, 1, "suffix", assetData);
@ -72,6 +80,45 @@ namespace Squidex.Infrastructure.Assets
Assert.Equal(assetData.ToArray(), readData.ToArray());
}
[Fact]
public async Task Should_commit_temporary_file()
{
sut.Connect();
var tempId = Id();
var assetId = Id();
var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 });
await sut.UploadTemporaryAsync(tempId, assetData);
await sut.CopyTemporaryAsync(tempId, assetId, 1, "suffix");
var readData = new MemoryStream();
await sut.DownloadAsync(assetId, 1, "suffix", readData);
Assert.Equal(assetData.ToArray(), readData.ToArray());
}
[Fact]
public async Task Should_ignore_when_deleting_twice()
{
sut.Connect();
var tempId = Id();
var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 });
await sut.UploadTemporaryAsync(tempId, assetData);
await sut.DeleteTemporaryAsync(tempId);
await sut.DeleteTemporaryAsync(tempId);
}
private static string Id()
{
return Guid.NewGuid().ToString();
}
private static string CreateInvalidPath()
{
var windir = Environment.GetEnvironmentVariable("windir");

Loading…
Cancel
Save