Browse Source

Recursive deletion of asset folders. (#490)

4_1
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
c598bf503d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs
  2. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  3. 109
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  4. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  6. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs
  7. 3
      backend/src/Squidex/Config/Domain/AssetServices.cs
  8. 110
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs
  9. 31
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  10. 4
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs

18
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs

@ -6,6 +6,8 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
@ -45,12 +47,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>("QueryAsyncByQuery"))
{
var assetFolders =
var assetFolderEntities =
await Collection
.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).SortBy(x => x.FolderName)
.ToListAsync();
return ResultList.Create<IAssetFolderEntity>(assetFolders.Count, assetFolders);
return ResultList.Create<IAssetFolderEntity>(assetFolderEntities.Count, assetFolderEntities);
}
}
public async Task<IReadOnlyList<Guid>> QueryChildIdsAsync(Guid appId, Guid parentId)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetFolderEntities =
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.Id)
.ToListAsync();
return assetFolderEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
}

12
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -111,6 +111,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IReadOnlyList<Guid>> QueryChildIdsAsync(Guid appId, Guid parentId)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntities =
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.Id)
.ToListAsync();
return assetEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
}
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids)
{
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByIds"))

109
backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs

@ -0,0 +1,109 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class RecursiveDeleter : IEventConsumer
{
private readonly ICommandBus commandBus;
private readonly IAssetRepository assetRepository;
private readonly IAssetFolderRepository assetFolderRepository;
private readonly ISemanticLog log;
private readonly string folderDeletedType;
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^assetFolder\\-"; }
}
public RecursiveDeleter(
ICommandBus commandBus,
IAssetRepository assetRepository,
IAssetFolderRepository assetFolderRepository,
TypeNameRegistry typeNameRegistry,
ISemanticLog log)
{
Guard.NotNull(commandBus);
Guard.NotNull(assetRepository);
Guard.NotNull(assetFolderRepository);
Guard.NotNull(typeNameRegistry);
Guard.NotNull(log);
this.commandBus = commandBus;
this.assetRepository = assetRepository;
this.assetFolderRepository = assetFolderRepository;
this.log = log;
folderDeletedType = typeNameRegistry.GetName<AssetFolderDeleted>();
}
public Task ClearAsync()
{
return TaskHelper.Done;
}
public bool Handles(StoredEvent @event)
{
return @event.Data.Type == folderDeletedType;
}
public async Task On(Envelope<IEvent> @event)
{
if (@event.Payload is AssetFolderDeleted folderDeleted)
{
async Task PublishAsync(SquidexCommand command)
{
try
{
command.Actor = folderDeleted.Actor;
await commandBus.PublishAsync(command);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "DeleteAssetsRecursive")
.WriteProperty("status", "Failed"));
}
}
var appId = folderDeleted.AppId;
var childAssetFolders = await assetFolderRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId);
foreach (var assetFolderId in childAssetFolders)
{
await PublishAsync(new DeleteAssetFolder { AssetFolderId = assetFolderId });
}
var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId);
foreach (var assetId in childAssets)
{
await PublishAsync(new DeleteAsset { AssetId = assetId });
}
}
}
}
}

3
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
Task<IResultList<IAssetFolderEntity>> QueryAsync(Guid appId, Guid parentId);
Task<IReadOnlyList<Guid>> QueryChildIdsAsync(Guid appId, Guid parentId);
Task<IAssetFolderEntity?> FindAssetFolderAsync(Guid id);
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, HashSet<Guid> ids);
Task<IReadOnlyList<Guid>> QueryChildIdsAsync(Guid appId, Guid parentId);
Task<IAssetEntity?> FindAssetAsync(Guid id);
Task<IAssetEntity?> FindAssetBySlugAsync(Guid appId, string slug);

2
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs

@ -84,7 +84,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = await InvokeCommandAsync(app, command);
return Ok(response);
return CreatedAtAction(nameof(GetAssetFolders), new { parentId = request.ParentId, app }, response);
}
/// <summary>

3
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -28,6 +28,9 @@ namespace Squidex.Config.Domain
services.Configure<AssetOptions>(
config.GetSection("assets"));
services.AddTransientAs<RecursiveDeleter>()
.As<IEventConsumer>();
services.AddTransientAs<AssetDomainObject>()
.AsSelf();

110
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs

@ -0,0 +1,110 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class RecursiveDeleterTests
{
private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry();
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly RecursiveDeleter sut;
public RecursiveDeleterTests()
{
typeNameRegistry.Map(typeof(AssetFolderDeleted));
sut = new RecursiveDeleter(commandBus, assetRepository, assetFolderRepository, typeNameRegistry, log);
}
[Fact]
public async Task Should_do_nothing_on_clear()
{
await sut.ClearAsync();
}
[Fact]
public async Task Should_invoke_delete_commands_for_all_subfolders()
{
var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = Guid.NewGuid() };
var childFolderId1 = Guid.NewGuid();
var childFolderId2 = Guid.NewGuid();
A.CallTo(() => assetFolderRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId))
.Returns(new List<Guid> { childFolderId1, childFolderId2 });
await sut.On(Envelope.Create(@event));
A.CallTo(() => commandBus.PublishAsync(A<DeleteAssetFolder>.That.Matches(x => x.AssetFolderId == childFolderId1)))
.MustHaveHappened();
A.CallTo(() => commandBus.PublishAsync(A<DeleteAssetFolder>.That.Matches(x => x.AssetFolderId == childFolderId2)))
.MustHaveHappened();
}
[Fact]
public async Task Should_invoke_delete_commands_for_all_assets()
{
var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = Guid.NewGuid() };
var childId1 = Guid.NewGuid();
var childId2 = Guid.NewGuid();
A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId))
.Returns(new List<Guid> { childId1, childId2 });
await sut.On(Envelope.Create(@event));
A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId1)))
.MustHaveHappened();
A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId2)))
.MustHaveHappened();
}
[Fact]
public async Task Should_ignore_exceptions()
{
var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = Guid.NewGuid() };
var childId1 = Guid.NewGuid();
var childId2 = Guid.NewGuid();
A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId1)))
.Throws(new InvalidOperationException());
A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId))
.Returns(new List<Guid> { childId1, childId2 });
await sut.On(Envelope.Create(@event));
A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId2)))
.MustHaveHappened();
A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A<Action<None, IObjectWriter>>.Ignored))
.MustHaveHappened();
}
}
}

31
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -188,5 +188,36 @@ namespace TestSuite.ApiTests
Assert.Contains(assets_2.Items, x => x.Id == asset_1.Id);
}
[Fact]
public async Task Should_delete_recursively()
{
// STEP 1: Create asset folder
var createRequest1 = new CreateAssetFolderDto { FolderName = "folder1" };
var folder_1 = await _.Assets.PostAssetFolderAsync(_.AppName, createRequest1);
// STEP 2: Create nested asset folder
var createRequest2 = new CreateAssetFolderDto { FolderName = "subfolder", ParentId = folder_1.Id };
var folder_2 = await _.Assets.PostAssetFolderAsync(_.AppName, createRequest2);
// STEP 3: Create asset in folder
var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png", null, folder_2.Id);
// STEP 4: Delete folder.
await _.Assets.DeleteAssetFolderAsync(_.AppName, folder_1.Id.ToString());
// STEP 5: Wait for recursive deleter to delete the asset.
await Task.Delay(5000);
var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => _.Assets.GetAssetAsync(_.AppName, asset_1.Id.ToString()));
Assert.Equal(404, ex.StatusCode);
}
}
}

4
backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs

@ -55,7 +55,7 @@ namespace TestSuite.Fixtures
}
}
public async Task<AssetDto> UploadFileAsync(string path, string mimeType, string fileName = null)
public async Task<AssetDto> UploadFileAsync(string path, string mimeType, string fileName = null, Guid? parentId = null)
{
var fileInfo = new FileInfo(path);
@ -63,7 +63,7 @@ namespace TestSuite.Fixtures
{
var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), mimeType);
return await Assets.PostAssetAsync(AppName, upload);
return await Assets.PostAssetAsync(AppName, upload, parentId);
}
}

Loading…
Cancel
Save