mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
308 changed files with 4755 additions and 3177 deletions
@ -0,0 +1,38 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Core.Assets; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Commands |
||||
|
{ |
||||
|
public sealed class BulkUpdateJob |
||||
|
{ |
||||
|
public BulkUpdateAssetType Type { get; set; } |
||||
|
|
||||
|
public DomainId Id { get; set; } |
||||
|
|
||||
|
public DomainId ParentId { get; set; } |
||||
|
|
||||
|
public string? ParentPath { get; set; } |
||||
|
|
||||
|
public string? FileName { get; set; } |
||||
|
|
||||
|
public string? Slug { get; set; } |
||||
|
|
||||
|
public bool? IsProtected { get; set; } |
||||
|
|
||||
|
public bool Permanent { get; set; } |
||||
|
|
||||
|
public HashSet<string> Tags { get; set; } |
||||
|
|
||||
|
public AssetMetadata? Metadata { get; set; } |
||||
|
|
||||
|
public long ExpectedVersion { get; set; } = EtagVersion.Any; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Commands |
||||
|
{ |
||||
|
public sealed class UpsertAsset : UploadAssetCommand |
||||
|
{ |
||||
|
public DomainId? ParentId { get; set; } |
||||
|
|
||||
|
public string? ParentPath { get; set; } |
||||
|
|
||||
|
public UpsertAsset() |
||||
|
{ |
||||
|
AssetId = DomainId.NewGuid(); |
||||
|
} |
||||
|
|
||||
|
public CreateAsset AsCreate() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new CreateAsset()); |
||||
|
} |
||||
|
|
||||
|
public UpdateAsset AsUpdate() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new UpdateAsset()); |
||||
|
} |
||||
|
|
||||
|
public MoveAsset AsMove(DomainId parentId) |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new MoveAsset { ParentId = parentId }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Caching; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject |
||||
|
{ |
||||
|
public sealed class AssetFolderResolver : IAssetFolderResolver |
||||
|
{ |
||||
|
private static readonly char[] TrimChars = { '/', '\\' }; |
||||
|
private static readonly char[] SplitChars = { ' ', '/', '\\' }; |
||||
|
private readonly ILocalCache localCache; |
||||
|
private readonly IAssetQueryService assetQuery; |
||||
|
|
||||
|
public AssetFolderResolver(ILocalCache localCache, IAssetQueryService assetQuery) |
||||
|
{ |
||||
|
Guard.NotNull(localCache, nameof(localCache)); |
||||
|
Guard.NotNull(assetQuery, nameof(assetQuery)); |
||||
|
|
||||
|
this.localCache = localCache; |
||||
|
this.assetQuery = assetQuery; |
||||
|
} |
||||
|
|
||||
|
public async Task<DomainId> ResolveOrCreateAsync(Context context, ICommandBus commandBus, string path) |
||||
|
{ |
||||
|
Guard.NotNull(commandBus, nameof(commandBus)); |
||||
|
Guard.NotNull(path, nameof(path)); |
||||
|
|
||||
|
path = path.Trim(TrimChars); |
||||
|
|
||||
|
var elements = path.Split(SplitChars, StringSplitOptions.RemoveEmptyEntries); |
||||
|
|
||||
|
if (elements.Length == 0) |
||||
|
{ |
||||
|
return DomainId.Empty; |
||||
|
} |
||||
|
|
||||
|
var currentId = DomainId.Empty; |
||||
|
|
||||
|
var i = elements.Length; |
||||
|
|
||||
|
for (; i > 0; i--) |
||||
|
{ |
||||
|
var subPath = string.Join('/', elements.Take(i)); |
||||
|
|
||||
|
if (localCache.TryGetValue(GetCacheKey(subPath), out var cached) && cached is DomainId id) |
||||
|
{ |
||||
|
currentId = id; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var creating = false; |
||||
|
|
||||
|
for (; i < elements.Length; i++) |
||||
|
{ |
||||
|
var name = elements[i]; |
||||
|
|
||||
|
var isResolved = false; |
||||
|
|
||||
|
if (!creating) |
||||
|
{ |
||||
|
var children = await assetQuery.QueryAssetFoldersAsync(context, currentId); |
||||
|
|
||||
|
foreach (var child in children) |
||||
|
{ |
||||
|
var childPath = string.Join('/', elements.Take(i).Union(Enumerable.Repeat(child.FolderName, 1))); |
||||
|
|
||||
|
localCache.Add(GetCacheKey(childPath), child.Id); |
||||
|
} |
||||
|
|
||||
|
foreach (var child in children) |
||||
|
{ |
||||
|
if (child.FolderName == name) |
||||
|
{ |
||||
|
currentId = child.Id; |
||||
|
|
||||
|
isResolved = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!isResolved) |
||||
|
{ |
||||
|
var command = new CreateAssetFolder { ParentId = currentId, FolderName = name }; |
||||
|
|
||||
|
await commandBus.PublishAsync(command); |
||||
|
|
||||
|
currentId = command.AssetFolderId; |
||||
|
creating = true; |
||||
|
} |
||||
|
|
||||
|
var newPath = string.Join('/', elements.Take(i).Union(Enumerable.Repeat(name, 1))); |
||||
|
|
||||
|
localCache.Add(GetCacheKey(newPath), currentId); |
||||
|
} |
||||
|
|
||||
|
return currentId; |
||||
|
} |
||||
|
|
||||
|
private static object GetCacheKey(string path) |
||||
|
{ |
||||
|
return $"ASSET_FOLDERS_{path}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,210 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Threading.Tasks.Dataflow; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Shared; |
||||
|
|
||||
|
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
||||
|
#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject |
||||
|
{ |
||||
|
public sealed class AssetsBulkUpdateCommandMiddleware : ICommandMiddleware |
||||
|
{ |
||||
|
private readonly IContextProvider contextProvider; |
||||
|
|
||||
|
private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private sealed record BulkTask( |
||||
|
ICommandBus Bus, |
||||
|
int JobIndex, |
||||
|
BulkUpdateJob Job, |
||||
|
BulkUpdateAssets Command, |
||||
|
ConcurrentBag<BulkUpdateResultItem> Results |
||||
|
) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AssetsBulkUpdateCommandMiddleware(IContextProvider contextProvider) |
||||
|
{ |
||||
|
Guard.NotNull(contextProvider, nameof(contextProvider)); |
||||
|
|
||||
|
this.contextProvider = contextProvider; |
||||
|
} |
||||
|
|
||||
|
public async Task HandleAsync(CommandContext context, NextDelegate next) |
||||
|
{ |
||||
|
if (context.Command is BulkUpdateAssets bulkUpdates) |
||||
|
{ |
||||
|
if (bulkUpdates.Jobs?.Length > 0) |
||||
|
{ |
||||
|
var executionOptions = new ExecutionDataflowBlockOptions |
||||
|
{ |
||||
|
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) |
||||
|
}; |
||||
|
|
||||
|
var createCommandsBlock = new TransformBlock<BulkTask, BulkTaskCommand?>(task => |
||||
|
{ |
||||
|
return CreateCommand(task); |
||||
|
}, executionOptions); |
||||
|
|
||||
|
var executeCommandBlock = new ActionBlock<BulkTaskCommand?>(async command => |
||||
|
{ |
||||
|
if (command != null) |
||||
|
{ |
||||
|
await ExecuteCommandAsync(command); |
||||
|
} |
||||
|
}, executionOptions); |
||||
|
|
||||
|
createCommandsBlock.LinkTo(executeCommandBlock, new DataflowLinkOptions |
||||
|
{ |
||||
|
PropagateCompletion = true |
||||
|
}); |
||||
|
|
||||
|
contextProvider.Context.Change(b => b |
||||
|
.WithoutAssetEnrichment() |
||||
|
.WithoutCleanup() |
||||
|
.WithUnpublished(true) |
||||
|
.WithoutTotal()); |
||||
|
|
||||
|
var results = new ConcurrentBag<BulkUpdateResultItem>(); |
||||
|
|
||||
|
for (var i = 0; i < bulkUpdates.Jobs.Length; i++) |
||||
|
{ |
||||
|
var task = new BulkTask( |
||||
|
context.CommandBus, |
||||
|
i, |
||||
|
bulkUpdates.Jobs[i], |
||||
|
bulkUpdates, |
||||
|
results); |
||||
|
|
||||
|
await createCommandsBlock.SendAsync(task); |
||||
|
} |
||||
|
|
||||
|
createCommandsBlock.Complete(); |
||||
|
|
||||
|
await executeCommandBlock.Completion; |
||||
|
|
||||
|
context.Complete(new BulkUpdateResult(results)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
context.Complete(new BulkUpdateResult()); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
await next(context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) |
||||
|
{ |
||||
|
var (task, id, command) = bulkCommand; |
||||
|
|
||||
|
Exception? exception = null; |
||||
|
try |
||||
|
{ |
||||
|
await task.Bus.PublishAsync(command); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
exception = ex; |
||||
|
} |
||||
|
|
||||
|
task.Results.Add(new BulkUpdateResultItem |
||||
|
{ |
||||
|
Id = id, |
||||
|
JobIndex = task.JobIndex, |
||||
|
Exception = exception |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private BulkTaskCommand? CreateCommand(BulkTask task) |
||||
|
{ |
||||
|
var id = task.Job.Id; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var command = CreateCommandCore(task); |
||||
|
|
||||
|
command.AssetId = id; |
||||
|
|
||||
|
return new BulkTaskCommand(task, id, command); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
task.Results.Add(new BulkUpdateResultItem |
||||
|
{ |
||||
|
Id = id, |
||||
|
JobIndex = task.JobIndex, |
||||
|
Exception = ex |
||||
|
}); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private AssetCommand CreateCommandCore(BulkTask task) |
||||
|
{ |
||||
|
var job = task.Job; |
||||
|
|
||||
|
switch (job.Type) |
||||
|
{ |
||||
|
case BulkUpdateAssetType.Annotate: |
||||
|
{ |
||||
|
var command = new AnnotateAsset(); |
||||
|
|
||||
|
Enrich(task, command, Permissions.AppAssetsUpdate); |
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
case BulkUpdateAssetType.Move: |
||||
|
{ |
||||
|
var command = new MoveAsset(); |
||||
|
|
||||
|
Enrich(task, command, Permissions.AppAssetsUpdate); |
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
case BulkUpdateAssetType.Delete: |
||||
|
{ |
||||
|
var command = new DeleteAsset(); |
||||
|
|
||||
|
Enrich(task, command, Permissions.AppAssetsDelete); |
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void Enrich<T>(BulkTask task, T command, string permissionId) where T : AssetCommand |
||||
|
{ |
||||
|
SimpleMapper.Map(task.Command, command); |
||||
|
SimpleMapper.Map(task.Job, command); |
||||
|
|
||||
|
if (!contextProvider.Context.Allows(permissionId)) |
||||
|
{ |
||||
|
throw new DomainForbiddenException("Forbidden"); |
||||
|
} |
||||
|
|
||||
|
command.ExpectedVersion = task.Command.ExpectedVersion; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue