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) 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) 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) 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) 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) public Task<bool> HandleAsync(CommandContext context)

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

@ -8,6 +8,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Google; 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) public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
{ {
var objectName = GetObjectName(id, version, suffix); 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); try
{
await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream); await storageClient.DeleteObjectAsync(bucketName, name);
}
catch
{
// ignored
}
} }
private string GetObjectName(string id, long version, string suffix) private string GetObjectName(string id, long version, string suffix)
@ -75,14 +111,14 @@ namespace Squidex.Infrastructure.Assets
throw new InvalidOperationException("No connection established yet."); throw new InvalidOperationException("No connection established yet.");
} }
var name = $"{id}_{version}"; var name = GetFileName(id, version, suffix);
if (!string.IsNullOrWhiteSpace(suffix))
{
name += "_" + suffix;
}
return name; 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.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Assets 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) public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
{ {
var file = GetFile(id, version, suffix); 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 public interface IAssetStore
{ {
Task CopyTemporaryAsync(string name, string id, long version, string suffix);
Task DownloadAsync(string id, long version, string suffix, Stream stream); 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 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; 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(creator, nameof(creator));
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -54,9 +54,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
context.Succeed(new EntityCreatedResult<Guid>(aggregate.Id, aggregate.Version)); 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(updater, nameof(updater));
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -72,6 +74,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
context.Succeed(new EntitySavedResult(aggregate.Version)); context.Succeed(new EntitySavedResult(aggregate.Version));
} }
return aggregate;
} }
private static IAggregateCommand GetCommand(CommandContext context) 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 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 Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
// ReSharper disable ImplicitlyCapturedClosure
// ReSharper disable ConvertToConstant.Local // ReSharper disable ConvertToConstant.Local
namespace Squidex.Domain.Apps.Write.Assets namespace Squidex.Domain.Apps.Write.Assets
@ -44,11 +45,11 @@ namespace Squidex.Domain.Apps.Write.Assets
[Fact] [Fact]
public async Task Create_should_create_asset() public async Task Create_should_create_asset()
{ {
SetupStore(0);
SetupImageInfo();
var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file });
SetupStore(0, context.ContextId);
SetupImageInfo();
await TestCreate(asset, async _ => await TestCreate(asset, async _ =>
{ {
await sut.HandleAsync(context); await sut.HandleAsync(context);
@ -63,13 +64,13 @@ namespace Squidex.Domain.Apps.Write.Assets
[Fact] [Fact]
public async Task Update_should_update_domain_object() public async Task Update_should_update_domain_object()
{ {
SetupStore(1); var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
SetupStore(1, context.ContextId);
SetupImageInfo(); SetupImageInfo();
CreateAsset(); CreateAsset();
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
await TestUpdate(asset, async _ => await TestUpdate(asset, async _ =>
{ {
await sut.HandleAsync(context); await sut.HandleAsync(context);
@ -117,10 +118,18 @@ namespace Squidex.Domain.Apps.Write.Assets
.Verifiable(); .Verifiable();
} }
private void SetupStore(long version) private void SetupStore(long version, Guid commitId)
{ {
assetStore 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(); .Verifiable();
} }
} }

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

@ -34,18 +34,26 @@ namespace Squidex.Domain.Apps.Write.TestHelpers
IsUpdated = false; 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; 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; 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] [Fact]
public Task Should_throw_exception_if_asset_not_found() public Task Should_throw_exception_if_asset_to_download_is_not_found()
{ {
sut.Connect(); 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] [Fact]
@ -60,7 +68,7 @@ namespace Squidex.Infrastructure.Assets
{ {
sut.Connect(); sut.Connect();
var assetId = Guid.NewGuid().ToString(); var assetId = Id();
var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 });
await sut.UploadAsync(assetId, 1, "suffix", assetData); await sut.UploadAsync(assetId, 1, "suffix", assetData);
@ -72,6 +80,45 @@ namespace Squidex.Infrastructure.Assets
Assert.Equal(assetData.ToArray(), readData.ToArray()); 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() private static string CreateInvalidPath()
{ {
var windir = Environment.GetEnvironmentVariable("windir"); var windir = Environment.GetEnvironmentVariable("windir");

Loading…
Cancel
Save