Browse Source

Fix tag names.

pull/364/head^2
Sebastian Stehle 7 years ago
parent
commit
5c83dcec16
  1. 64
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  2. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  3. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs
  4. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  5. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  6. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  7. 8
      src/Squidex.Infrastructure/CollectionExtensions.cs
  8. 6
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  9. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  10. 2
      src/Squidex/app/shared/services/assets.service.ts
  11. 84
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

64
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -24,25 +25,28 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators; private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware( public AssetCommandMiddleware(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAssetQueryService assetQuery, IAssetQueryService assetQuery,
IAssetStore assetStore, IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators) IEnumerable<ITagGenerator<CreateAsset>> tagGenerators,
ITagService tagService)
: base(grainFactory) : base(grainFactory)
{ {
Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators)); Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore; this.assetStore = assetStore;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators; this.tagGenerators = tagGenerators;
this.tagService = tagService;
} }
public override async Task HandleAsync(CommandContext context, Func<Task> next) public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>(); createAsset.Tags = new HashSet<string>();
} }
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, context);
createAsset.FileHash = await UploadAsync(context, createAsset.File);
try try
{ {
@ -70,7 +73,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
if (IsDuplicate(createAsset, existing)) if (IsDuplicate(createAsset, existing))
{ {
result = new AssetCreatedResult(existing, true); var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags);
result = new AssetCreatedResult(existing, true, new HashSet<string>(denormalizedTags.Values));
} }
break; break;
@ -85,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset); var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset);
result = new AssetCreatedResult(asset, false); result = new AssetCreatedResult(asset, false, createAsset.Tags);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null); await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
} }
@ -102,16 +107,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset: case UpdateAsset updateAsset:
{ {
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead()); await EnrichWithImageInfosAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, context);
updateAsset.FileHash = await UploadAsync(context, updateAsset.File);
try try
{ {
var result = (IAssetEntity)await ExecuteCommandAsync(updateAsset); var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset);
context.Complete(result); context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null); await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null);
} }
finally finally
{ {
@ -121,29 +126,54 @@ namespace Squidex.Domain.Apps.Entities.Assets
break; break;
} }
case AssetCommand command:
{
var result = await ExecuteAndAdjustTagsAsync(command);
context.Complete(result);
break;
}
default: default:
await base.HandleAsync(context, next); await base.HandleAsync(context, next);
break; break;
} }
} }
private async Task<object> ExecuteAndAdjustTagsAsync(AssetCommand command)
{
var result = await ExecuteCommandAsync(command);
if (result is IAssetEntity asset)
{
var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags);
return new AssetResult(asset, new HashSet<string>(denormalizedTags.Values));
}
return result;
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset) private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)
{ {
return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize; return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize;
} }
private async Task<string> UploadAsync(CommandContext context, AssetFile file) private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{ {
string hash; command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256)) private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, CommandContext context)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
{ {
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream); await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64(); command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
} }
return hash;
} }
} }
} }

11
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,18 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetCreatedResult public sealed class AssetCreatedResult : AssetResult
{ {
public IAssetEntity Asset { get; }
public bool IsDuplicate { get; } public bool IsDuplicate { get; }
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate) public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet<string> tags)
: base(asset, tags)
{ {
Asset = asset;
IsDuplicate = isDuplicate; IsDuplicate = isDuplicate;
} }
} }

25
src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetResult
{
public IAssetEntity Asset { get; }
public HashSet<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
}
}

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -8,22 +8,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class CreateAsset : AssetCommand, IAppCommand public sealed class CreateAsset : UploadAssetCommand, IAppCommand
{ {
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset() public CreateAsset()
{ {
AssetId = Guid.NewGuid(); AssetId = Guid.NewGuid();

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs

@ -5,16 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class UpdateAsset : AssetCommand public sealed class UpdateAsset : UploadAssetCommand
{ {
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
} }
} }

20
src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public abstract class UploadAssetCommand : AssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

8
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,14 @@ namespace Squidex.Infrastructure
{ {
public static class CollectionExtensions public static class CollectionExtensions
{ {
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
foreach (var value in source)
{
target.Add(value);
}
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable) public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{ {
var random = new Random(); var random = new Random();

6
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -182,7 +182,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetCreatedResult>(); var result = context.Result<AssetCreatedResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate); var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate);
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
} }
@ -267,8 +267,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAssetEntity>(); var result = context.Result<AssetResult>();
var response = AssetDto.FromAsset(result, this, app); var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags);
return response; return response;
} }

7
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -118,10 +118,15 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[JsonProperty("_meta")] [JsonProperty("_meta")]
public AssetMetadata Metadata { get; set; } public AssetMetadata Metadata { get; set; }
public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet<string> tags = null, bool isDuplicate = false)
{ {
var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
if (tags != null)
{
response.Tags = tags;
}
if (isDuplicate) if (isDuplicate)
{ {
response.Metadata = new AssetMetadata { IsDuplicate = "true" }; response.Metadata = new AssetMetadata { IsDuplicate = "true" };

2
src/Squidex/app/shared/services/assets.service.ts

@ -242,7 +242,7 @@ export class AssetsService {
tap(() => { tap(() => {
this.analytics.trackEvent('Analytics', 'Updated', appName); this.analytics.trackEvent('Analytics', 'Updated', appName);
}), }),
pretifyError('Failed to delete asset. Please reload.')); pretifyError('Failed to update asset. Please reload.'));
} }
public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> { public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> {

84
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -54,19 +54,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored)) A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IAssetEntity>()); .Returns(new List<IAssetEntity>());
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored)) A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>
{
["1"] = "foundTag1",
["2"] = "foundTag2"
});
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset); .Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }, tagService);
} }
[Fact] [Fact]
public async Task Create_should_create_domain_object() public async Task Create_should_create_domain_object()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupTags(command); SetupTags(command);
@ -80,6 +84,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Contains("tag1", command.Tags); Assert.Contains("tag1", command.Tags);
Assert.Contains("tag2", command.Tags); Assert.Contains("tag2", command.Tags);
Assert.Equal(new HashSet<string> { "tag1", "tag2" }, result.Tags);
AssertAssetHasBeenUploaded(0, context.ContextId); AssertAssetHasBeenUploaded(0, context.ContextId);
AssertAssetImageChecked(); AssertAssetImageChecked();
} }
@ -87,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_calculate_hash() public async Task Create_should_calculate_hash()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _); SetupSameHashAsset(file.FileName, file.FileSize, out _);
@ -108,13 +114,15 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.HandleAsync(context); await sut.HandleAsync(context);
Assert.True(context.Result<AssetCreatedResult>().IsDuplicate); var result = context.Result<AssetCreatedResult>();
Assert.True(result.IsDuplicate);
} }
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupSameHashAsset("other-name", file.FileSize, out _); SetupSameHashAsset("other-name", file.FileSize, out _);
@ -122,13 +130,31 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.HandleAsync(context); await sut.HandleAsync(context);
Assert.False(context.Result<AssetCreatedResult>().IsDuplicate); var result = context.Result<AssetCreatedResult>();
Assert.False(result.IsDuplicate);
}
[Fact]
public async Task Create_should_resolve_tag_names_for_duplicate()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _);
SetupImageInfo();
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
} }
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found() public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, 12345, out _); SetupSameHashAsset(file.FileName, 12345, out _);
@ -142,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_update_domain_object() public async Task Update_should_update_domain_object()
{ {
var command = new UpdateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -158,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_calculate_hash() public async Task Update_should_calculate_hash()
{ {
var command = new UpdateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -170,6 +196,40 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.True(command.FileHash.Length > 10); Assert.True(command.FileHash.Length > 10);
} }
[Fact]
public async Task Update_should_resolve_tags()
{
var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
var result = context.Result<AssetResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
}
[Fact]
public async Task AnnotateAsset_should_resolve_tags()
{
var command = CreateCommand(new AnnotateAsset { AssetId = assetId, FileName = "newName" });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
var result = context.Result<AssetResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
}
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
{ {
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));

Loading…
Cancel
Save