mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
101 changed files with 2847 additions and 993 deletions
@ -0,0 +1,47 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public sealed class DefaultAppImageStore : IAppImageStore |
||||
|
{ |
||||
|
private readonly IAssetStore assetStore; |
||||
|
|
||||
|
public DefaultAppImageStore(IAssetStore assetStore) |
||||
|
{ |
||||
|
Guard.NotNull(assetStore, nameof(assetStore)); |
||||
|
|
||||
|
this.assetStore = assetStore; |
||||
|
} |
||||
|
|
||||
|
public Task DownloadAsync(Guid backupId, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(backupId); |
||||
|
|
||||
|
return assetStore.DownloadAsync(fileName, stream, ct); |
||||
|
} |
||||
|
|
||||
|
public Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(backupId); |
||||
|
|
||||
|
return assetStore.UploadAsync(fileName, stream, true, ct); |
||||
|
} |
||||
|
|
||||
|
private string GetFileName(Guid backupId) |
||||
|
{ |
||||
|
return backupId.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public interface IAppImageStore |
||||
|
{ |
||||
|
Task UploadAsync(Guid appId, Stream stream, CancellationToken ct = default); |
||||
|
|
||||
|
Task DownloadAsync(Guid appId, Stream stream, CancellationToken ct = default); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public sealed class DefaultAssetFileStore : IAssetFileStore |
||||
|
{ |
||||
|
private readonly IAssetStore assetStore; |
||||
|
|
||||
|
public DefaultAssetFileStore(IAssetStore assetStore) |
||||
|
{ |
||||
|
this.assetStore = assetStore; |
||||
|
} |
||||
|
|
||||
|
public string? GeneratePublicUrl(Guid id, long fileVersion) |
||||
|
{ |
||||
|
var fileName = GetFileName(id, fileVersion); |
||||
|
|
||||
|
return assetStore.GeneratePublicUrl(fileName); |
||||
|
} |
||||
|
|
||||
|
public Task UploadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(id, fileVersion); |
||||
|
|
||||
|
return assetStore.UploadAsync(fileName, stream, true, ct); |
||||
|
} |
||||
|
|
||||
|
public Task UploadAsync(string tempFile, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
return assetStore.UploadAsync(tempFile, stream, false, ct); |
||||
|
} |
||||
|
|
||||
|
public Task CopyAsync(string tempFile, Guid id, long fileVersion, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(id, fileVersion); |
||||
|
|
||||
|
return assetStore.CopyAsync(tempFile, fileName, ct); |
||||
|
} |
||||
|
|
||||
|
public Task DownloadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(id, fileVersion); |
||||
|
|
||||
|
return assetStore.DownloadAsync(fileName, stream, ct); |
||||
|
} |
||||
|
|
||||
|
public Task DeleteAsync(Guid id, long fileVersion) |
||||
|
{ |
||||
|
var fileName = GetFileName(id, fileVersion); |
||||
|
|
||||
|
return assetStore.DeleteAsync(fileName); |
||||
|
} |
||||
|
|
||||
|
public Task DeleteAsync(string tempFile) |
||||
|
{ |
||||
|
return assetStore.DeleteAsync(tempFile); |
||||
|
} |
||||
|
|
||||
|
private static string GetFileName(Guid id, long fileVersion) |
||||
|
{ |
||||
|
return $"{id}_{fileVersion}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public interface IAssetFileStore |
||||
|
{ |
||||
|
string? GeneratePublicUrl(Guid id, long fileVersion); |
||||
|
|
||||
|
Task CopyAsync(string tempFile, Guid id, long fileVersion, CancellationToken ct = default); |
||||
|
|
||||
|
Task UploadAsync(string tempFile, Stream stream, CancellationToken ct = default); |
||||
|
|
||||
|
Task UploadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default); |
||||
|
|
||||
|
Task DownloadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default); |
||||
|
|
||||
|
Task DeleteAsync(string tempFile); |
||||
|
|
||||
|
Task DeleteAsync(Guid id, long fileVersion); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public sealed class BackupContext : BackupContextBase |
||||
|
{ |
||||
|
public IBackupWriter Writer { get; } |
||||
|
|
||||
|
public BackupContext(Guid appId, IUserMapping userMapping, IBackupWriter writer) |
||||
|
: base(appId, userMapping) |
||||
|
{ |
||||
|
Guard.NotNull(writer); |
||||
|
|
||||
|
Writer = writer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public abstract class BackupContextBase |
||||
|
{ |
||||
|
public IUserMapping UserMapping { get; } |
||||
|
|
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
public RefToken Initiator |
||||
|
{ |
||||
|
get { return UserMapping.Initiator; } |
||||
|
} |
||||
|
|
||||
|
protected BackupContextBase(Guid appId, IUserMapping userMapping) |
||||
|
{ |
||||
|
Guard.NotNull(userMapping); |
||||
|
|
||||
|
AppId = appId; |
||||
|
|
||||
|
UserMapping = userMapping; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,54 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// 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 Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
using Squidex.Infrastructure.States; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup |
|
||||
{ |
|
||||
public abstract class BackupHandlerWithStore : BackupHandler |
|
||||
{ |
|
||||
private readonly IStore<Guid> store; |
|
||||
|
|
||||
protected BackupHandlerWithStore(IStore<Guid> store) |
|
||||
{ |
|
||||
Guard.NotNull(store); |
|
||||
|
|
||||
this.store = store; |
|
||||
} |
|
||||
|
|
||||
protected async Task RebuildManyAsync(IEnumerable<Guid> ids, Func<Guid, Task> action) |
|
||||
{ |
|
||||
foreach (var id in ids) |
|
||||
{ |
|
||||
await action(id); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected async Task RebuildAsync<TState, TGrain>(Guid key) where TState : IDomainState<TState>, new() |
|
||||
{ |
|
||||
var state = new TState |
|
||||
{ |
|
||||
Version = EtagVersion.Empty |
|
||||
}; |
|
||||
|
|
||||
var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => |
|
||||
{ |
|
||||
state = state.Apply(e); |
|
||||
|
|
||||
state.Version++; |
|
||||
}); |
|
||||
|
|
||||
await persistence.ReadAsync(); |
|
||||
await persistence.WriteSnapshotAsync(state); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,76 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Orleans; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public sealed class BackupService : IBackupService |
||||
|
{ |
||||
|
private readonly IGrainFactory grainFactory; |
||||
|
|
||||
|
public BackupService(IGrainFactory grainFactory) |
||||
|
{ |
||||
|
Guard.NotNull(grainFactory); |
||||
|
|
||||
|
this.grainFactory = grainFactory; |
||||
|
} |
||||
|
|
||||
|
public Task StartBackupAsync(Guid appId, RefToken actor) |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<IBackupGrain>(appId); |
||||
|
|
||||
|
return grain.BackupAsync(actor); |
||||
|
} |
||||
|
|
||||
|
public Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName) |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id); |
||||
|
|
||||
|
return grain.RestoreAsync(url, actor, newAppName); |
||||
|
} |
||||
|
|
||||
|
public async Task<IRestoreJob?> GetRestoreAsync() |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id); |
||||
|
|
||||
|
var state = await grain.GetStateAsync(); |
||||
|
|
||||
|
return state.Value; |
||||
|
} |
||||
|
|
||||
|
public async Task<List<IBackupJob>> GetBackupsAsync(Guid appId) |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<IBackupGrain>(appId); |
||||
|
|
||||
|
var state = await grain.GetStateAsync(); |
||||
|
|
||||
|
return state.Value; |
||||
|
} |
||||
|
|
||||
|
public async Task<IBackupJob?> GetBackupAsync(Guid appId, Guid backupId) |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<IBackupGrain>(appId); |
||||
|
|
||||
|
var state = await grain.GetStateAsync(); |
||||
|
|
||||
|
return state.Value.Find(x => x.Id == backupId); |
||||
|
} |
||||
|
|
||||
|
public Task DeleteBackupAsync(Guid appId, Guid backupId) |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<IBackupGrain>(appId); |
||||
|
|
||||
|
return grain.DeleteAsync(backupId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public sealed class DefaultBackupArchiveStore : IBackupArchiveStore |
||||
|
{ |
||||
|
private readonly IAssetStore assetStore; |
||||
|
|
||||
|
public DefaultBackupArchiveStore(IAssetStore assetStore) |
||||
|
{ |
||||
|
Guard.NotNull(assetStore, nameof(assetStore)); |
||||
|
|
||||
|
this.assetStore = assetStore; |
||||
|
} |
||||
|
|
||||
|
public Task DownloadAsync(Guid backupId, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(backupId); |
||||
|
|
||||
|
return assetStore.DownloadAsync(fileName, stream, ct); |
||||
|
} |
||||
|
|
||||
|
public Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
var fileName = GetFileName(backupId); |
||||
|
|
||||
|
return assetStore.UploadAsync(fileName, stream, true, ct); |
||||
|
} |
||||
|
|
||||
|
public Task DeleteAsync(Guid backupId) |
||||
|
{ |
||||
|
var fileName = GetFileName(backupId); |
||||
|
|
||||
|
return assetStore.DeleteAsync(fileName); |
||||
|
} |
||||
|
|
||||
|
private string GetFileName(Guid backupId) |
||||
|
{ |
||||
|
return $"{backupId}_0"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,87 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Net.Http; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Infrastructure.Json; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup.Helpers |
|
||||
{ |
|
||||
public static class Downloader |
|
||||
{ |
|
||||
public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, string id) |
|
||||
{ |
|
||||
if (string.Equals(url.Scheme, "file")) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) |
|
||||
{ |
|
||||
using (var sourceStream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read)) |
|
||||
{ |
|
||||
await sourceStream.CopyToAsync(targetStream); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
catch (IOException ex) |
|
||||
{ |
|
||||
throw new BackupRestoreException($"Cannot download the archive: {ex.Message}.", ex); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
HttpResponseMessage? response = null; |
|
||||
try |
|
||||
{ |
|
||||
using (var client = new HttpClient()) |
|
||||
{ |
|
||||
response = await client.GetAsync(url); |
|
||||
response.EnsureSuccessStatusCode(); |
|
||||
|
|
||||
using (var sourceStream = await response.Content.ReadAsStreamAsync()) |
|
||||
{ |
|
||||
using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) |
|
||||
{ |
|
||||
await sourceStream.CopyToAsync(targetStream); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
catch (HttpRequestException ex) |
|
||||
{ |
|
||||
throw new BackupRestoreException($"Cannot download the archive. Got status code: {response?.StatusCode}.", ex); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static async Task<BackupReader> OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, string id, IJsonSerializer serializer) |
|
||||
{ |
|
||||
Stream? stream = null; |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
stream = await backupArchiveLocation.OpenStreamAsync(id); |
|
||||
|
|
||||
return new BackupReader(serializer, stream); |
|
||||
} |
|
||||
catch (IOException) |
|
||||
{ |
|
||||
stream?.Dispose(); |
|
||||
|
|
||||
throw new BackupRestoreException("The backup archive is correupt and cannot be opened."); |
|
||||
} |
|
||||
catch (Exception) |
|
||||
{ |
|
||||
stream?.Dispose(); |
|
||||
|
|
||||
throw; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,62 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Infrastructure.Assets; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup.Helpers |
|
||||
{ |
|
||||
public static class Safe |
|
||||
{ |
|
||||
public static async Task DeleteAsync(IBackupArchiveLocation backupArchiveLocation, string id, ISemanticLog log) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await backupArchiveLocation.DeleteArchiveAsync(id); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
log.LogError(ex, id, (logOperationId, w) => w |
|
||||
.WriteProperty("action", "deleteArchive") |
|
||||
.WriteProperty("status", "failed") |
|
||||
.WriteProperty("operationId", logOperationId)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static async Task DeleteAsync(IAssetStore assetStore, string id, ISemanticLog log) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await assetStore.DeleteAsync(id, 0, null); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
log.LogError(ex, id, (logOperationId, w) => w |
|
||||
.WriteProperty("action", "deleteBackup") |
|
||||
.WriteProperty("status", "failed") |
|
||||
.WriteProperty("operationId", logOperationId)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static async Task CleanupRestoreErrorAsync(BackupHandler handler, Guid appId, Guid id, ISemanticLog log) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await handler.CleanupRestoreErrorAsync(appId); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
log.LogError(ex, id.ToString(), (logOperationId, w) => w |
|
||||
.WriteProperty("action", "cleanupRestore") |
|
||||
.WriteProperty("status", "failed") |
|
||||
.WriteProperty("operationId", logOperationId)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public interface IBackupArchiveStore |
||||
|
{ |
||||
|
Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default); |
||||
|
|
||||
|
Task DownloadAsync(Guid backupId, Stream stream, CancellationToken ct = default); |
||||
|
|
||||
|
Task DeleteAsync(Guid backupId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public interface IBackupReader : IDisposable |
||||
|
{ |
||||
|
int ReadAttachments { get; } |
||||
|
|
||||
|
int ReadEvents { get; } |
||||
|
|
||||
|
Guid OldGuid(Guid newId); |
||||
|
|
||||
|
Task ReadBlobAsync(string name, Func<Stream, Task> handler); |
||||
|
|
||||
|
Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope<IEvent> Event), Task> handler); |
||||
|
|
||||
|
Task<T> ReadJsonAttachmentAsync<T>(string name); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public interface IBackupService |
||||
|
{ |
||||
|
Task StartBackupAsync(Guid appId, RefToken actor); |
||||
|
|
||||
|
Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName); |
||||
|
|
||||
|
Task<IRestoreJob?> GetRestoreAsync(); |
||||
|
|
||||
|
Task<List<IBackupJob>> GetBackupsAsync(Guid appId); |
||||
|
|
||||
|
Task<IBackupJob?> GetBackupAsync(Guid appId, Guid backupId); |
||||
|
|
||||
|
Task DeleteBackupAsync(Guid appId, Guid backupId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public interface IBackupWriter : IDisposable |
||||
|
{ |
||||
|
int WrittenAttachments { get; } |
||||
|
|
||||
|
int WrittenEvents { get; } |
||||
|
|
||||
|
Task WriteBlobAsync(string name, Func<Stream, Task> handler); |
||||
|
|
||||
|
void WriteEvent(StoredEvent storedEvent); |
||||
|
|
||||
|
Task WriteJsonAsync(string name, object value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public interface IUserMapping |
||||
|
{ |
||||
|
RefToken Initiator { get; } |
||||
|
|
||||
|
void Backup(RefToken token); |
||||
|
|
||||
|
void Backup(string userId); |
||||
|
|
||||
|
bool TryMap(RefToken token, out RefToken result); |
||||
|
|
||||
|
bool TryMap(string userId, out RefToken result); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public sealed class RestoreContext : BackupContextBase |
||||
|
{ |
||||
|
public IBackupReader Reader { get; } |
||||
|
|
||||
|
public RestoreContext(Guid appId, IUserMapping userMapping, IBackupReader reader) |
||||
|
: base(appId, userMapping) |
||||
|
{ |
||||
|
Guard.NotNull(reader); |
||||
|
|
||||
|
Reader = reader; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public class UserMapping : IUserMapping |
||||
|
{ |
||||
|
private const string UsersFile = "Users.json"; |
||||
|
private readonly Dictionary<string, RefToken> userMap = new Dictionary<string, RefToken>(); |
||||
|
private readonly RefToken initiator; |
||||
|
|
||||
|
public RefToken Initiator |
||||
|
{ |
||||
|
get { return initiator; } |
||||
|
} |
||||
|
|
||||
|
public UserMapping(RefToken initiator) |
||||
|
{ |
||||
|
Guard.NotNull(initiator); |
||||
|
|
||||
|
this.initiator = initiator; |
||||
|
} |
||||
|
|
||||
|
public void Backup(RefToken token) |
||||
|
{ |
||||
|
Guard.NotNull(userMap); |
||||
|
|
||||
|
if (!token.IsSubject) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
userMap[token.Identifier] = token; |
||||
|
} |
||||
|
|
||||
|
public void Backup(string userId) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(userId); |
||||
|
|
||||
|
if (!userMap.ContainsKey(userId)) |
||||
|
{ |
||||
|
userMap[userId] = new RefToken(RefTokenType.Subject, userId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task StoreAsync(IBackupWriter writer, IUserResolver userResolver) |
||||
|
{ |
||||
|
Guard.NotNull(writer); |
||||
|
Guard.NotNull(userResolver); |
||||
|
|
||||
|
var users = await userResolver.QueryManyAsync(userMap.Keys.ToArray()); |
||||
|
|
||||
|
var json = users.ToDictionary(x => x.Key, x => x.Value.Email); |
||||
|
|
||||
|
await writer.WriteJsonAsync(UsersFile, json); |
||||
|
} |
||||
|
|
||||
|
public async Task RestoreAsync(IBackupReader reader, IUserResolver userResolver) |
||||
|
{ |
||||
|
Guard.NotNull(reader); |
||||
|
Guard.NotNull(userResolver); |
||||
|
|
||||
|
var json = await reader.ReadJsonAttachmentAsync<Dictionary<string, string>>(UsersFile); |
||||
|
|
||||
|
foreach (var kvp in json) |
||||
|
{ |
||||
|
var email = kvp.Value; |
||||
|
|
||||
|
var user = await userResolver.FindByIdOrEmailAsync(email); |
||||
|
|
||||
|
if (user == null && await userResolver.CreateUserIfNotExistsAsync(kvp.Value, false)) |
||||
|
{ |
||||
|
user = await userResolver.FindByIdOrEmailAsync(email); |
||||
|
} |
||||
|
|
||||
|
if (user != null) |
||||
|
{ |
||||
|
userMap[kvp.Key] = new RefToken(RefTokenType.Subject, user.Id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool TryMap(string userId, out RefToken result) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(userId); |
||||
|
|
||||
|
if (userMap.TryGetValue(userId, out var mapped)) |
||||
|
{ |
||||
|
result = mapped; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
result = initiator; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public bool TryMap(RefToken token, out RefToken result) |
||||
|
{ |
||||
|
Guard.NotNull(token); |
||||
|
|
||||
|
if (token.IsClient) |
||||
|
{ |
||||
|
result = token; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (userMap.TryGetValue(token.Identifier, out var mapped)) |
||||
|
{ |
||||
|
result = mapped; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
result = initiator; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,74 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public static class AssetStoreExtensions |
|
||||
{ |
|
||||
public static string? GeneratePublicUrl(this IAssetStore store, Guid id, long version, string? suffix) |
|
||||
{ |
|
||||
return store.GeneratePublicUrl(id.ToString(), version, suffix); |
|
||||
} |
|
||||
|
|
||||
public static string? GeneratePublicUrl(this IAssetStore store, string id, long version, string? suffix) |
|
||||
{ |
|
||||
return store.GeneratePublicUrl(GetFileName(id, version, suffix)); |
|
||||
} |
|
||||
|
|
||||
public static Task CopyAsync(this IAssetStore store, string sourceFileName, Guid id, long version, string? suffix, CancellationToken ct = default) |
|
||||
{ |
|
||||
return store.CopyAsync(sourceFileName, id.ToString(), version, suffix, ct); |
|
||||
} |
|
||||
|
|
||||
public static Task CopyAsync(this IAssetStore store, string sourceFileName, string id, long version, string? suffix, CancellationToken ct = default) |
|
||||
{ |
|
||||
return store.CopyAsync(sourceFileName, GetFileName(id, version, suffix), ct); |
|
||||
} |
|
||||
|
|
||||
public static Task DownloadAsync(this IAssetStore store, Guid id, long version, string? suffix, Stream stream, CancellationToken ct = default) |
|
||||
{ |
|
||||
return store.DownloadAsync(id.ToString(), version, suffix, stream, ct); |
|
||||
} |
|
||||
|
|
||||
public static Task DownloadAsync(this IAssetStore store, string id, long version, string? suffix, Stream stream, CancellationToken ct = default) |
|
||||
{ |
|
||||
return store.DownloadAsync(GetFileName(id, version, suffix), stream, ct); |
|
||||
} |
|
||||
|
|
||||
public static Task UploadAsync(this IAssetStore store, Guid id, long version, string? suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
return store.UploadAsync(id.ToString(), version, suffix, stream, overwrite, ct); |
|
||||
} |
|
||||
|
|
||||
public static Task UploadAsync(this IAssetStore store, string id, long version, string? suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
return store.UploadAsync(GetFileName(id, version, suffix), stream, overwrite, ct); |
|
||||
} |
|
||||
|
|
||||
public static Task DeleteAsync(this IAssetStore store, Guid id, long version, string? suffix) |
|
||||
{ |
|
||||
return store.DeleteAsync(id.ToString(), version, suffix); |
|
||||
} |
|
||||
|
|
||||
public static Task DeleteAsync(this IAssetStore store, string id, long version, string? suffix) |
|
||||
{ |
|
||||
return store.DeleteAsync(GetFileName(id, version, suffix)); |
|
||||
} |
|
||||
|
|
||||
public static string GetFileName(string id, long version, string? suffix = null) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(id); |
|
||||
|
|
||||
return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,114 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Threading.Tasks.Dataflow; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public delegate Task IdSource(Func<Guid, Task> add); |
||||
|
|
||||
|
public class Rebuilder |
||||
|
{ |
||||
|
private readonly ILocalCache localCache; |
||||
|
private readonly IStore<Guid> store; |
||||
|
private readonly IEventStore eventStore; |
||||
|
|
||||
|
public Rebuilder( |
||||
|
ILocalCache localCache, |
||||
|
IStore<Guid> store, |
||||
|
IEventStore eventStore) |
||||
|
{ |
||||
|
Guard.NotNull(localCache); |
||||
|
Guard.NotNull(store); |
||||
|
Guard.NotNull(eventStore); |
||||
|
|
||||
|
this.eventStore = eventStore; |
||||
|
this.localCache = localCache; |
||||
|
this.store = store; |
||||
|
} |
||||
|
|
||||
|
public Task RebuildAsync<TState, TGrain>(string filter, CancellationToken ct) where TState : IDomainState<TState>, new() |
||||
|
{ |
||||
|
return RebuildAsync<TState, TGrain>(async target => |
||||
|
{ |
||||
|
await eventStore.QueryAsync(async storedEvent => |
||||
|
{ |
||||
|
var id = storedEvent.Data.Headers.AggregateId(); |
||||
|
|
||||
|
await target(id); |
||||
|
}, filter, ct: ct); |
||||
|
}, ct); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task RebuildAsync<TState, TGrain>(IdSource source, CancellationToken ct = default) where TState : IDomainState<TState>, new() |
||||
|
{ |
||||
|
Guard.NotNull(source); |
||||
|
|
||||
|
await store.GetSnapshotStore<TState>().ClearAsync(); |
||||
|
|
||||
|
await InsertManyAsync<TState, TGrain>(source, ct); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task InsertManyAsync<TState, TGrain>(IdSource source, CancellationToken ct = default) where TState : IDomainState<TState>, new() |
||||
|
{ |
||||
|
Guard.NotNull(source); |
||||
|
|
||||
|
var worker = new ActionBlock<Guid>(async id => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var state = new TState |
||||
|
{ |
||||
|
Version = EtagVersion.Empty |
||||
|
}; |
||||
|
|
||||
|
var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), id, (TState s) => state = s, e => |
||||
|
{ |
||||
|
state = state.Apply(e); |
||||
|
|
||||
|
state.Version++; |
||||
|
}); |
||||
|
|
||||
|
await persistence.ReadAsync(); |
||||
|
await persistence.WriteSnapshotAsync(state); |
||||
|
} |
||||
|
catch (DomainObjectNotFoundException) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
}, |
||||
|
new ExecutionDataflowBlockOptions |
||||
|
{ |
||||
|
MaxDegreeOfParallelism = Environment.ProcessorCount * 2 |
||||
|
}); |
||||
|
|
||||
|
var handledIds = new HashSet<Guid>(); |
||||
|
|
||||
|
using (localCache.StartContext()) |
||||
|
{ |
||||
|
await source(new Func<Guid, Task>(async id => |
||||
|
{ |
||||
|
if (handledIds.Add(id)) |
||||
|
{ |
||||
|
await worker.SendAsync(id, ct); |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
worker.Complete(); |
||||
|
|
||||
|
await worker.Completion; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,369 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Indexes; |
||||
|
using Squidex.Domain.Apps.Entities.Backup; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable IDE0067 // Dispose objects before losing scope
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class BackupAppsTests |
||||
|
{ |
||||
|
private readonly IAppsIndex index = A.Fake<IAppsIndex>(); |
||||
|
private readonly IAppUISettings appUISettings = A.Fake<IAppUISettings>(); |
||||
|
private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly RefToken actor = new RefToken(RefTokenType.Subject, "123"); |
||||
|
private readonly BackupApps sut; |
||||
|
|
||||
|
public BackupAppsTests() |
||||
|
{ |
||||
|
sut = new BackupApps(appImageStore, index, appUISettings); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_provide_name() |
||||
|
{ |
||||
|
Assert.Equal("Apps", sut.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_reserve_app_name() |
||||
|
{ |
||||
|
const string appName = "my-app"; |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => index.ReserveAsync(appId, appName)) |
||||
|
.Returns("Reservation"); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppCreated |
||||
|
{ |
||||
|
Name = appName |
||||
|
}), context); |
||||
|
|
||||
|
A.CallTo(() => index.ReserveAsync(appId, appName)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_complete_reservation_with_previous_token() |
||||
|
{ |
||||
|
const string appName = "my-app"; |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => index.ReserveAsync(appId, appName)) |
||||
|
.Returns("Reservation"); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppCreated |
||||
|
{ |
||||
|
Name = appName |
||||
|
}), context); |
||||
|
|
||||
|
await sut.CompleteRestoreAsync(context); |
||||
|
|
||||
|
A.CallTo(() => index.AddAsync("Reservation")) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_cleanup_reservation_with_previous_token() |
||||
|
{ |
||||
|
const string appName = "my-app"; |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => index.ReserveAsync(appId, appName)) |
||||
|
.Returns("Reservation"); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppCreated |
||||
|
{ |
||||
|
Name = appName |
||||
|
}), context); |
||||
|
|
||||
|
await sut.CleanupRestoreErrorAsync(appId); |
||||
|
|
||||
|
A.CallTo(() => index.RemoveReservationAsync("Reservation")) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_no_reservation_token_returned() |
||||
|
{ |
||||
|
const string appName = "my-app"; |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => index.ReserveAsync(appId, appName)) |
||||
|
.Returns(Task.FromResult<string?>(null)); |
||||
|
|
||||
|
await Assert.ThrowsAsync<BackupRestoreException>(() => |
||||
|
{ |
||||
|
return sut.RestoreEventAsync(Envelope.Create(new AppCreated |
||||
|
{ |
||||
|
Name = appName |
||||
|
}), context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_cleanup_reservation_when_no_reservation_token_hold() |
||||
|
{ |
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
await sut.CleanupRestoreErrorAsync(appId); |
||||
|
|
||||
|
A.CallTo(() => index.RemoveReservationAsync("Reservation")) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_writer_user_settings() |
||||
|
{ |
||||
|
var settings = JsonValue.Object(); |
||||
|
|
||||
|
var context = CreateBackupContext(); |
||||
|
|
||||
|
A.CallTo(() => appUISettings.GetAsync(appId, null)) |
||||
|
.Returns(settings); |
||||
|
|
||||
|
await sut.BackupAsync(context); |
||||
|
|
||||
|
A.CallTo(() => context.Writer.WriteJsonAsync(A<string>.Ignored, settings)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_read_user_settings() |
||||
|
{ |
||||
|
var settings = JsonValue.Object(); |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Reader.ReadJsonAttachmentAsync<JsonObject>(A<string>.Ignored)) |
||||
|
.Returns(settings); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
A.CallTo(() => appUISettings.SetAsync(appId, null, settings)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_map_contributor_id_when_assigned() |
||||
|
{ |
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
var @event = Envelope.Create(new AppContributorAssigned |
||||
|
{ |
||||
|
ContributorId = "found" |
||||
|
}); |
||||
|
|
||||
|
var result = await sut.RestoreEventAsync(@event, context); |
||||
|
|
||||
|
Assert.True(result); |
||||
|
Assert.Equal("found_mapped", @event.Payload.ContributorId); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_ignore_contributor_event_when_assigned_user_not_mapped() |
||||
|
{ |
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
var @event = Envelope.Create(new AppContributorAssigned |
||||
|
{ |
||||
|
ContributorId = "unknown" |
||||
|
}); |
||||
|
|
||||
|
var result = await sut.RestoreEventAsync(@event, context); |
||||
|
|
||||
|
Assert.False(result); |
||||
|
Assert.Equal("unknown", @event.Payload.ContributorId); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_map_contributor_id_when_revoked() |
||||
|
{ |
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
var @event = Envelope.Create(new AppContributorRemoved |
||||
|
{ |
||||
|
ContributorId = "found" |
||||
|
}); |
||||
|
|
||||
|
var result = await sut.RestoreEventAsync(@event, context); |
||||
|
|
||||
|
Assert.True(result); |
||||
|
Assert.Equal("found_mapped", @event.Payload.ContributorId); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_ignore_contributor_event_when_removed_user_not_mapped() |
||||
|
{ |
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
var @event = Envelope.Create(new AppContributorRemoved |
||||
|
{ |
||||
|
ContributorId = "unknown" |
||||
|
}); |
||||
|
|
||||
|
var result = await sut.RestoreEventAsync(@event, context); |
||||
|
|
||||
|
Assert.False(result); |
||||
|
Assert.Equal("unknown", @event.Payload.ContributorId); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_ignore_exception_when_app_image_to_backup_does_not_exist() |
||||
|
{ |
||||
|
var imageStream = new MemoryStream(); |
||||
|
|
||||
|
var context = CreateBackupContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Writer.WriteBlobAsync(A<string>.Ignored, A<Func<Stream, Task>>.Ignored)) |
||||
|
.Invokes((string _, Func<Stream, Task> handler) => handler(imageStream)); |
||||
|
|
||||
|
A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, default)) |
||||
|
.Throws(new AssetNotFoundException("Image")); |
||||
|
|
||||
|
await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_backup_app_image() |
||||
|
{ |
||||
|
var imageStream = new MemoryStream(); |
||||
|
|
||||
|
var context = CreateBackupContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Writer.WriteBlobAsync(A<string>.Ignored, A<Func<Stream, Task>>.Ignored)) |
||||
|
.Invokes((string _, Func<Stream, Task> handler) => handler(imageStream)); |
||||
|
|
||||
|
await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context); |
||||
|
|
||||
|
A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_app_image() |
||||
|
{ |
||||
|
var imageStream = new MemoryStream(); |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Reader.ReadBlobAsync(A<string>.Ignored, A<Func<Stream, Task>>.Ignored)) |
||||
|
.Invokes((string _, Func<Stream, Task> handler) => handler(imageStream)); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context); |
||||
|
|
||||
|
A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_ignore_exception_when_app_image_cannot_be_overriden() |
||||
|
{ |
||||
|
var imageStream = new MemoryStream(); |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Reader.ReadBlobAsync(A<string>.Ignored, A<Func<Stream, Task>>.Ignored)) |
||||
|
.Invokes((string _, Func<Stream, Task> handler) => handler(imageStream)); |
||||
|
|
||||
|
A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, default)) |
||||
|
.Throws(new AssetAlreadyExistsException("Image")); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_indices_for_all_non_deleted_schemas() |
||||
|
{ |
||||
|
var userId1 = "found1"; |
||||
|
var userId2 = "found2"; |
||||
|
var userId3 = "found3"; |
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppContributorAssigned |
||||
|
{ |
||||
|
ContributorId = userId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppContributorAssigned |
||||
|
{ |
||||
|
ContributorId = userId2 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppContributorAssigned |
||||
|
{ |
||||
|
ContributorId = userId3 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AppContributorRemoved |
||||
|
{ |
||||
|
ContributorId = userId3 |
||||
|
}), context); |
||||
|
|
||||
|
HashSet<string>? newIndex = null; |
||||
|
|
||||
|
A.CallTo(() => index.RebuildByContributorsAsync(appId, A<HashSet<string>>.Ignored)) |
||||
|
.Invokes(new Action<Guid, HashSet<string>>((_, i) => newIndex = i)); |
||||
|
|
||||
|
await sut.CompleteRestoreAsync(context); |
||||
|
|
||||
|
Assert.Equal(new HashSet<string> |
||||
|
{ |
||||
|
"found1_mapped", |
||||
|
"found2_mapped", |
||||
|
}, newIndex); |
||||
|
} |
||||
|
|
||||
|
private BackupContext CreateBackupContext() |
||||
|
{ |
||||
|
return new BackupContext(appId, CreateUserMapping(), A.Fake<IBackupWriter>()); |
||||
|
} |
||||
|
|
||||
|
private RestoreContext CreateRestoreContext() |
||||
|
{ |
||||
|
return new RestoreContext(appId, CreateUserMapping(), A.Fake<IBackupReader>()); |
||||
|
} |
||||
|
|
||||
|
private IUserMapping CreateUserMapping() |
||||
|
{ |
||||
|
var mapping = A.Fake<IUserMapping>(); |
||||
|
|
||||
|
A.CallTo(() => mapping.Initiator).Returns(actor); |
||||
|
|
||||
|
RefToken mapped; |
||||
|
|
||||
|
A.CallTo(() => mapping.TryMap(A<string>.That.Matches(x => x.StartsWith("found", StringComparison.OrdinalIgnoreCase)), out mapped)) |
||||
|
.Returns(true) |
||||
|
.AssignsOutAndRefParametersLazily( |
||||
|
new Func<string, RefToken, object[]>((x, _) => |
||||
|
new[] { new RefToken(RefTokenType.Subject, $"{x}_mapped") })); |
||||
|
|
||||
|
A.CallTo(() => mapping.TryMap("notfound", out mapped)) |
||||
|
.Returns(false); |
||||
|
|
||||
|
return mapping; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class DefaultAppImageStoreTests |
||||
|
{ |
||||
|
private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly string fileName; |
||||
|
private readonly DefaultAppImageStore sut; |
||||
|
|
||||
|
public DefaultAppImageStoreTests() |
||||
|
{ |
||||
|
fileName = appId.ToString(); |
||||
|
|
||||
|
sut = new DefaultAppImageStore(assetStore); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_upload_archive() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.UploadAsync(appId, stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_download_archive() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.DownloadAsync(appId, stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.DownloadAsync(fileName, stream, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,256 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Tags; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.State; |
||||
|
using Squidex.Domain.Apps.Entities.Backup; |
||||
|
using Squidex.Domain.Apps.Events.Assets; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable IDE0067 // Dispose objects before losing scope
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public class BackupAssetsTests |
||||
|
{ |
||||
|
private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); |
||||
|
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); |
||||
|
private readonly ITagService tagService = A.Fake<ITagService>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly RefToken actor = new RefToken(RefTokenType.Subject, "123"); |
||||
|
private readonly BackupAssets sut; |
||||
|
|
||||
|
public BackupAssetsTests() |
||||
|
{ |
||||
|
sut = new BackupAssets(rebuilder, assetFileStore, tagService); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_provide_name() |
||||
|
{ |
||||
|
Assert.Equal("Assets", sut.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_writer_tags() |
||||
|
{ |
||||
|
var tags = new TagsExport(); |
||||
|
|
||||
|
var context = CreateBackupContext(); |
||||
|
|
||||
|
A.CallTo(() => tagService.GetExportableTagsAsync(appId, TagGroups.Assets)) |
||||
|
.Returns(tags); |
||||
|
|
||||
|
await sut.BackupAsync(context); |
||||
|
|
||||
|
A.CallTo(() => context.Writer.WriteJsonAsync(A<string>.Ignored, tags)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_read_tags() |
||||
|
{ |
||||
|
var tags = new TagsExport(); |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Reader.ReadJsonAttachmentAsync<TagsExport>(A<string>.Ignored)) |
||||
|
.Returns(tags); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
A.CallTo(() => tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_backup_created_asset() |
||||
|
{ |
||||
|
var @event = new AssetCreated { AssetId = Guid.NewGuid() }; |
||||
|
|
||||
|
await TestBackupEventAsync(@event, 0); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_backup_updated_asset() |
||||
|
{ |
||||
|
var @event = new AssetUpdated { AssetId = Guid.NewGuid(), FileVersion = 3 }; |
||||
|
|
||||
|
await TestBackupEventAsync(@event, @event.FileVersion); |
||||
|
} |
||||
|
|
||||
|
private async Task TestBackupEventAsync(AssetEvent @event, long version) |
||||
|
{ |
||||
|
var assetStream = new MemoryStream(); |
||||
|
var assetId = @event.AssetId; |
||||
|
|
||||
|
var context = CreateBackupContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Writer.WriteBlobAsync($"{assetId}_{version}.asset", A<Func<Stream, Task>>.Ignored)) |
||||
|
.Invokes((string _, Func<Stream, Task> handler) => handler(assetStream)); |
||||
|
|
||||
|
await sut.BackupEventAsync(Envelope.Create(@event), context); |
||||
|
|
||||
|
A.CallTo(() => assetFileStore.DownloadAsync(assetId, version, assetStream, default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_created_asset() |
||||
|
{ |
||||
|
var @event = new AssetCreated { AssetId = Guid.NewGuid() }; |
||||
|
|
||||
|
await TestRestoreAsync(@event, 0); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_updated_asset() |
||||
|
{ |
||||
|
var @event = new AssetUpdated { AssetId = Guid.NewGuid(), FileVersion = 3 }; |
||||
|
|
||||
|
await TestRestoreAsync(@event, @event.FileVersion); |
||||
|
} |
||||
|
|
||||
|
private async Task TestRestoreAsync(AssetEvent @event, long version) |
||||
|
{ |
||||
|
var oldId = Guid.NewGuid(); |
||||
|
|
||||
|
var assetStream = new MemoryStream(); |
||||
|
var assetId = @event.AssetId; |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
A.CallTo(() => context.Reader.OldGuid(assetId)) |
||||
|
.Returns(oldId); |
||||
|
|
||||
|
A.CallTo(() => context.Reader.ReadBlobAsync($"{oldId}_{version}.asset", A<Func<Stream, Task>>.Ignored)) |
||||
|
.Invokes((string _, Func<Stream, Task> handler) => handler(assetStream)); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(@event), context); |
||||
|
|
||||
|
A.CallTo(() => assetFileStore.UploadAsync(assetId, version, assetStream, default)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_states_for_all_assets() |
||||
|
{ |
||||
|
var assetId1 = Guid.NewGuid(); |
||||
|
var assetId2 = Guid.NewGuid(); |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AssetCreated |
||||
|
{ |
||||
|
AssetId = assetId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AssetCreated |
||||
|
{ |
||||
|
AssetId = assetId2 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AssetDeleted |
||||
|
{ |
||||
|
AssetId = assetId2 |
||||
|
}), context); |
||||
|
|
||||
|
var rebuildAssets = new HashSet<Guid>(); |
||||
|
|
||||
|
var add = new Func<Guid, Task>(id => |
||||
|
{ |
||||
|
rebuildAssets.Add(id); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => rebuilder.InsertManyAsync<AssetState, AssetGrain>(A<IdSource>.Ignored, A<CancellationToken>.Ignored)) |
||||
|
.Invokes((IdSource source, CancellationToken _) => source(add)); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
Assert.Equal(new HashSet<Guid> |
||||
|
{ |
||||
|
assetId1, |
||||
|
assetId2 |
||||
|
}, rebuildAssets); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_states_for_all_asset_folders() |
||||
|
{ |
||||
|
var assetFolderId1 = Guid.NewGuid(); |
||||
|
var assetFolderId2 = Guid.NewGuid(); |
||||
|
|
||||
|
var context = CreateRestoreContext(); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AssetFolderCreated |
||||
|
{ |
||||
|
AssetFolderId = assetFolderId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AssetFolderCreated |
||||
|
{ |
||||
|
AssetFolderId = assetFolderId2 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new AssetFolderDeleted |
||||
|
{ |
||||
|
AssetFolderId = assetFolderId2 |
||||
|
}), context); |
||||
|
|
||||
|
var rebuildAssets = new HashSet<Guid>(); |
||||
|
|
||||
|
var add = new Func<Guid, Task>(id => |
||||
|
{ |
||||
|
rebuildAssets.Add(id); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => rebuilder.InsertManyAsync<AssetFolderState, AssetFolderGrain>(A<IdSource>.Ignored, A<CancellationToken>.Ignored)) |
||||
|
.Invokes((IdSource source, CancellationToken _) => source(add)); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
Assert.Equal(new HashSet<Guid> |
||||
|
{ |
||||
|
assetFolderId1, |
||||
|
assetFolderId2 |
||||
|
}, rebuildAssets); |
||||
|
} |
||||
|
|
||||
|
private BackupContext CreateBackupContext() |
||||
|
{ |
||||
|
return new BackupContext(appId, CreateUserMapping(), A.Fake<IBackupWriter>()); |
||||
|
} |
||||
|
|
||||
|
private RestoreContext CreateRestoreContext() |
||||
|
{ |
||||
|
return new RestoreContext(appId, CreateUserMapping(), A.Fake<IBackupReader>()); |
||||
|
} |
||||
|
|
||||
|
private IUserMapping CreateUserMapping() |
||||
|
{ |
||||
|
var mapping = A.Fake<IUserMapping>(); |
||||
|
|
||||
|
A.CallTo(() => mapping.Initiator).Returns(actor); |
||||
|
|
||||
|
return mapping; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public class DefaultAssetFileStoreTests |
||||
|
{ |
||||
|
private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); |
||||
|
private readonly Guid assetId = Guid.NewGuid(); |
||||
|
private readonly long assetFileVersion = 21; |
||||
|
private readonly string fileName; |
||||
|
private readonly DefaultAssetFileStore sut; |
||||
|
|
||||
|
public DefaultAssetFileStoreTests() |
||||
|
{ |
||||
|
fileName = $"{assetId}_{assetFileVersion}"; |
||||
|
|
||||
|
sut = new DefaultAssetFileStore(assetStore); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_invoke_asset_store_to_generate_public_url() |
||||
|
{ |
||||
|
var url = "http_//squidex.io/assets"; |
||||
|
|
||||
|
A.CallTo(() => assetStore.GeneratePublicUrl(fileName)) |
||||
|
.Returns(url); |
||||
|
|
||||
|
var result = sut.GeneratePublicUrl(assetId, assetFileVersion); |
||||
|
|
||||
|
Assert.Equal(url, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_temporary_upload_file() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.UploadAsync("Temp", stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.UploadAsync("Temp", stream, false, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_upload_file() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.UploadAsync(assetId, assetFileVersion, stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_download_file() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.DownloadAsync(assetId, assetFileVersion, stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.DownloadAsync(fileName, stream, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_copy_from_temporary_file() |
||||
|
{ |
||||
|
await sut.CopyAsync("Temp", assetId, assetFileVersion); |
||||
|
|
||||
|
A.CallTo(() => assetStore.CopyAsync("Temp", fileName, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_delete_temporary_file() |
||||
|
{ |
||||
|
await sut.DeleteAsync("Temp"); |
||||
|
|
||||
|
A.CallTo(() => assetStore.DeleteAsync("Temp")) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_delete_file() |
||||
|
{ |
||||
|
await sut.DeleteAsync(assetId, assetFileVersion); |
||||
|
|
||||
|
A.CallTo(() => assetStore.DeleteAsync(fileName)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,142 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Orleans; |
||||
|
using Squidex.Domain.Apps.Entities.Backup.State; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public class BackupServiceTests |
||||
|
{ |
||||
|
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly Guid backupId = Guid.NewGuid(); |
||||
|
private readonly RefToken actor = new RefToken(RefTokenType.Subject, "me"); |
||||
|
private readonly BackupService sut; |
||||
|
|
||||
|
public BackupServiceTests() |
||||
|
{ |
||||
|
sut = new BackupService(grainFactory); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_when_restoring_backup() |
||||
|
{ |
||||
|
var grain = A.Fake<IRestoreGrain>(); |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id, null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
var initiator = new RefToken(RefTokenType.Subject, "me"); |
||||
|
|
||||
|
var restoreUrl = new Uri("http://squidex.io"); |
||||
|
var restoreAppName = "New App"; |
||||
|
|
||||
|
await sut.StartRestoreAsync(initiator, restoreUrl, restoreAppName); |
||||
|
|
||||
|
A.CallTo(() => grain.RestoreAsync(restoreUrl, initiator, restoreAppName)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_to_get_restore_status() |
||||
|
{ |
||||
|
IRestoreJob state = new RestoreJob(); |
||||
|
|
||||
|
var grain = A.Fake<IRestoreGrain>(); |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id, null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
A.CallTo(() => grain.GetStateAsync()) |
||||
|
.Returns(state.AsJ()); |
||||
|
|
||||
|
var result = await sut.GetRestoreAsync(); |
||||
|
|
||||
|
Assert.Same(state, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_to_start_backup() |
||||
|
{ |
||||
|
var grain = A.Fake<IBackupGrain>(); |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<IBackupGrain>(appId, null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
await sut.StartBackupAsync(appId, actor); |
||||
|
|
||||
|
A.CallTo(() => grain.BackupAsync(actor)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_to_get_backups() |
||||
|
{ |
||||
|
var state = new List<IBackupJob> |
||||
|
{ |
||||
|
new BackupJob { Id = backupId } |
||||
|
}; |
||||
|
|
||||
|
var grain = A.Fake<IBackupGrain>(); |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<IBackupGrain>(appId, null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
A.CallTo(() => grain.GetStateAsync()) |
||||
|
.Returns(state.AsJ()); |
||||
|
|
||||
|
var result = await sut.GetBackupsAsync(appId); |
||||
|
|
||||
|
Assert.Same(state, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_to_get_backup() |
||||
|
{ |
||||
|
var state = new List<IBackupJob> |
||||
|
{ |
||||
|
new BackupJob { Id = backupId } |
||||
|
}; |
||||
|
|
||||
|
var grain = A.Fake<IBackupGrain>(); |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<IBackupGrain>(appId, null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
A.CallTo(() => grain.GetStateAsync()) |
||||
|
.Returns(state.AsJ()); |
||||
|
|
||||
|
var result1 = await sut.GetBackupAsync(appId, backupId); |
||||
|
var result2 = await sut.GetBackupAsync(appId, Guid.NewGuid()); |
||||
|
|
||||
|
Assert.Same(state[0], result1); |
||||
|
Assert.Null(result2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_to_delete_backup() |
||||
|
{ |
||||
|
var grain = A.Fake<IBackupGrain>(); |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<IBackupGrain>(appId, null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
await sut.DeleteBackupAsync(appId, backupId); |
||||
|
|
||||
|
A.CallTo(() => grain.DeleteAsync(backupId)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public class DefaultBackupArchiveStoreTests |
||||
|
{ |
||||
|
private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); |
||||
|
private readonly Guid backupId = Guid.NewGuid(); |
||||
|
private readonly string fileName; |
||||
|
private readonly DefaultBackupArchiveStore sut; |
||||
|
|
||||
|
public DefaultBackupArchiveStoreTests() |
||||
|
{ |
||||
|
fileName = $"{backupId}_0"; |
||||
|
|
||||
|
sut = new DefaultBackupArchiveStore(assetStore); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_upload_archive_using_suffix_for_compatibility() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.UploadAsync(backupId, stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_download_archive_using_suffix_for_compatibility() |
||||
|
{ |
||||
|
var stream = new MemoryStream(); |
||||
|
|
||||
|
await sut.DownloadAsync(backupId, stream); |
||||
|
|
||||
|
A.CallTo(() => assetStore.DownloadAsync(fileName, stream, CancellationToken.None)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_asset_store_to_delete_archive_using_suffix_for_compatibility() |
||||
|
{ |
||||
|
await sut.DeleteAsync(backupId); |
||||
|
|
||||
|
A.CallTo(() => assetStore.DeleteAsync(fileName)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup |
||||
|
{ |
||||
|
public class UserMappingTests |
||||
|
{ |
||||
|
private readonly RefToken initiator = Subject("me"); |
||||
|
private readonly UserMapping sut; |
||||
|
|
||||
|
public UserMappingTests() |
||||
|
{ |
||||
|
sut = new UserMapping(initiator); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_backup_users_but_no_clients() |
||||
|
{ |
||||
|
sut.Backup("user1"); |
||||
|
sut.Backup(Subject("user2")); |
||||
|
|
||||
|
sut.Backup(Client("client")); |
||||
|
|
||||
|
var user1 = CreateUser("user1", "mail1@squidex.io"); |
||||
|
var user2 = CreateUser("user2", "mail2@squidex.io"); |
||||
|
|
||||
|
var users = new Dictionary<string, IUser> |
||||
|
{ |
||||
|
[user1.Id] = user1, |
||||
|
[user2.Id] = user2 |
||||
|
}; |
||||
|
|
||||
|
var userResolver = A.Fake<IUserResolver>(); |
||||
|
|
||||
|
A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.That.Is(user1.Id, user2.Id))) |
||||
|
.Returns(users); |
||||
|
|
||||
|
var writer = A.Fake<IBackupWriter>(); |
||||
|
|
||||
|
Dictionary<string, string>? storedUsers = null; |
||||
|
|
||||
|
A.CallTo(() => writer.WriteJsonAsync(A<string>.Ignored, A<object>.Ignored)) |
||||
|
.Invokes((string _, object json) => storedUsers = (Dictionary<string, string>)json); |
||||
|
|
||||
|
await sut.StoreAsync(writer, userResolver); |
||||
|
|
||||
|
Assert.Equal(new Dictionary<string, string> |
||||
|
{ |
||||
|
[user1.Id] = user1.Email, |
||||
|
[user2.Id] = user2.Email |
||||
|
}, storedUsers); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_users() |
||||
|
{ |
||||
|
var user1 = CreateUser("user1", "mail1@squidex.io"); |
||||
|
var user2 = CreateUser("user2", "mail2@squidex.io"); |
||||
|
|
||||
|
var reader = SetupReader(user1, user2); |
||||
|
|
||||
|
var userResolver = A.Fake<IUserResolver>(); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(user1.Email)) |
||||
|
.Returns(user1); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(user2.Email)) |
||||
|
.Returns(user2); |
||||
|
|
||||
|
await sut.RestoreAsync(reader, userResolver); |
||||
|
|
||||
|
Assert.True(sut.TryMap("user1_old", out var mapped1)); |
||||
|
Assert.Equal(Subject("user1"), mapped1); |
||||
|
|
||||
|
Assert.True(sut.TryMap(Subject("user2_old"), out var mapped2)); |
||||
|
Assert.Equal(Subject("user2"), mapped2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_create_user_if_not_found() |
||||
|
{ |
||||
|
var user = CreateUser("newId1", "mail1@squidex.io"); |
||||
|
|
||||
|
var reader = SetupReader(user); |
||||
|
|
||||
|
var userResolver = A.Fake<IUserResolver>(); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(user.Email)) |
||||
|
.Returns(Task.FromResult<IUser?>(null)); |
||||
|
|
||||
|
await sut.RestoreAsync(reader, userResolver); |
||||
|
|
||||
|
A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, false)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_initiator_if_user_not_found() |
||||
|
{ |
||||
|
var user = Subject("user1"); |
||||
|
|
||||
|
Assert.False(sut.TryMap(user, out var mapped)); |
||||
|
Assert.Same(initiator, mapped); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_same_token_if_mapping_client() |
||||
|
{ |
||||
|
var client = Client("client1"); |
||||
|
|
||||
|
Assert.True(sut.TryMap(client, out var mapped)); |
||||
|
Assert.Same(client, mapped); |
||||
|
} |
||||
|
|
||||
|
private IUser CreateUser(string id, string email) |
||||
|
{ |
||||
|
var user = A.Fake<IUser>(); |
||||
|
|
||||
|
A.CallTo(() => user.Id).Returns(id); |
||||
|
A.CallTo(() => user.Email).Returns(email); |
||||
|
|
||||
|
return user; |
||||
|
} |
||||
|
|
||||
|
private static IBackupReader SetupReader(params IUser[] users) |
||||
|
{ |
||||
|
var storedUsers = users.ToDictionary(x => $"{x.Id}_old", x => x.Email); |
||||
|
|
||||
|
var reader = A.Fake<IBackupReader>(); |
||||
|
|
||||
|
A.CallTo(() => reader.ReadJsonAttachmentAsync<Dictionary<string, string>>(A<string>.Ignored)) |
||||
|
.Returns(storedUsers); |
||||
|
|
||||
|
return reader; |
||||
|
} |
||||
|
|
||||
|
private static RefToken Client(string identifier) |
||||
|
{ |
||||
|
return new RefToken(RefTokenType.Client, identifier); |
||||
|
} |
||||
|
|
||||
|
private static RefToken Subject(string identifier) |
||||
|
{ |
||||
|
return new RefToken(RefTokenType.Subject, identifier); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Entities.Backup; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.State; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Domain.Apps.Events.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public class BackupContentsTests |
||||
|
{ |
||||
|
private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); |
||||
|
private readonly BackupContents sut; |
||||
|
|
||||
|
public BackupContentsTests() |
||||
|
{ |
||||
|
sut = new BackupContents(rebuilder); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_provide_name() |
||||
|
{ |
||||
|
Assert.Equal("Contents", sut.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_states_for_all_contents() |
||||
|
{ |
||||
|
var appId = Guid.NewGuid(); |
||||
|
|
||||
|
var schemaId1 = NamedId.Of(Guid.NewGuid(), "my-schema1"); |
||||
|
var schemaId2 = NamedId.Of(Guid.NewGuid(), "my-schema2"); |
||||
|
|
||||
|
var contentId1 = Guid.NewGuid(); |
||||
|
var contentId2 = Guid.NewGuid(); |
||||
|
var contentId3 = Guid.NewGuid(); |
||||
|
|
||||
|
var context = new RestoreContext(appId, new UserMapping(new RefToken(RefTokenType.Subject, "123")), A.Fake<IBackupReader>()); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new ContentCreated |
||||
|
{ |
||||
|
ContentId = contentId1, |
||||
|
SchemaId = schemaId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new ContentCreated |
||||
|
{ |
||||
|
ContentId = contentId2, |
||||
|
SchemaId = schemaId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new ContentCreated |
||||
|
{ |
||||
|
ContentId = contentId3, |
||||
|
SchemaId = schemaId2 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new ContentDeleted |
||||
|
{ |
||||
|
ContentId = contentId2, |
||||
|
SchemaId = schemaId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new SchemaDeleted |
||||
|
{ |
||||
|
SchemaId = schemaId2 |
||||
|
}), context); |
||||
|
|
||||
|
var rebuildContents = new HashSet<Guid>(); |
||||
|
|
||||
|
var add = new Func<Guid, Task>(id => |
||||
|
{ |
||||
|
rebuildContents.Add(id); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => rebuilder.InsertManyAsync<ContentState, ContentGrain>(A<IdSource>.Ignored, A<CancellationToken>.Ignored)) |
||||
|
.Invokes((IdSource source, CancellationToken _) => source(add)); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
Assert.Equal(new HashSet<Guid> |
||||
|
{ |
||||
|
contentId1, |
||||
|
contentId2 |
||||
|
}, rebuildContents); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Backup; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Indexes; |
||||
|
using Squidex.Domain.Apps.Events.Rules; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public class BackupRulesTests |
||||
|
{ |
||||
|
private readonly IRulesIndex index = A.Fake<IRulesIndex>(); |
||||
|
private readonly BackupRules sut; |
||||
|
|
||||
|
public BackupRulesTests() |
||||
|
{ |
||||
|
sut = new BackupRules(index); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_provide_name() |
||||
|
{ |
||||
|
Assert.Equal("Rules", sut.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_indices_for_all_non_deleted_rules() |
||||
|
{ |
||||
|
var appId = Guid.NewGuid(); |
||||
|
|
||||
|
var ruleId1 = Guid.NewGuid(); |
||||
|
var ruleId2 = Guid.NewGuid(); |
||||
|
var ruleId3 = Guid.NewGuid(); |
||||
|
|
||||
|
var context = new RestoreContext(appId, new UserMapping(new RefToken(RefTokenType.Subject, "123")), A.Fake<IBackupReader>()); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new RuleCreated |
||||
|
{ |
||||
|
RuleId = ruleId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new RuleCreated |
||||
|
{ |
||||
|
RuleId = ruleId2 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new RuleCreated |
||||
|
{ |
||||
|
RuleId = ruleId3 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new RuleDeleted |
||||
|
{ |
||||
|
RuleId = ruleId3 |
||||
|
}), context); |
||||
|
|
||||
|
HashSet<Guid>? newIndex = null; |
||||
|
|
||||
|
A.CallTo(() => index.RebuildAsync(appId, A<HashSet<Guid>>.Ignored)) |
||||
|
.Invokes(new Action<Guid, HashSet<Guid>>((_, i) => newIndex = i)); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
Assert.Equal(new HashSet<Guid> |
||||
|
{ |
||||
|
ruleId1, |
||||
|
ruleId2, |
||||
|
}, newIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Backup; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas.Indexes; |
||||
|
using Squidex.Domain.Apps.Events.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Schemas |
||||
|
{ |
||||
|
public class BackupSchemasTests |
||||
|
{ |
||||
|
private readonly ISchemasIndex index = A.Fake<ISchemasIndex>(); |
||||
|
private readonly BackupSchemas sut; |
||||
|
|
||||
|
public BackupSchemasTests() |
||||
|
{ |
||||
|
sut = new BackupSchemas(index); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_provide_name() |
||||
|
{ |
||||
|
Assert.Equal("Schemas", sut.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_restore_indices_for_all_non_deleted_schemas() |
||||
|
{ |
||||
|
var appId = Guid.NewGuid(); |
||||
|
|
||||
|
var schemaId1 = NamedId.Of(Guid.NewGuid(), "my-schema1"); |
||||
|
var schemaId2 = NamedId.Of(Guid.NewGuid(), "my-schema2"); |
||||
|
var schemaId3 = NamedId.Of(Guid.NewGuid(), "my-schema3"); |
||||
|
|
||||
|
var context = new RestoreContext(appId, new UserMapping(new RefToken(RefTokenType.Subject, "123")), A.Fake<IBackupReader>()); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new SchemaCreated |
||||
|
{ |
||||
|
SchemaId = schemaId1 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new SchemaCreated |
||||
|
{ |
||||
|
SchemaId = schemaId2 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new SchemaCreated |
||||
|
{ |
||||
|
SchemaId = schemaId3 |
||||
|
}), context); |
||||
|
|
||||
|
await sut.RestoreEventAsync(Envelope.Create(new SchemaDeleted |
||||
|
{ |
||||
|
SchemaId = schemaId3 |
||||
|
}), context); |
||||
|
|
||||
|
Dictionary<string, Guid>? newIndex = null; |
||||
|
|
||||
|
A.CallTo(() => index.RebuildAsync(appId, A<Dictionary<string, Guid>>.Ignored)) |
||||
|
.Invokes(new Action<Guid, Dictionary<string, Guid>>((_, i) => newIndex = i)); |
||||
|
|
||||
|
await sut.RestoreAsync(context); |
||||
|
|
||||
|
Assert.Equal(new Dictionary<string, Guid> |
||||
|
{ |
||||
|
[schemaId1.Name] = schemaId1.Id, |
||||
|
[schemaId2.Name] = schemaId2.Id |
||||
|
}, newIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,112 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using FakeItEasy; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public class AssetExtensionTests |
|
||||
{ |
|
||||
private readonly IAssetStore sut = A.Fake<IAssetStore>(); |
|
||||
private readonly Guid id = Guid.NewGuid(); |
|
||||
private readonly Stream stream = new MemoryStream(); |
|
||||
private readonly string fileName = Guid.NewGuid().ToString(); |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_copy_with_id_and_version() |
|
||||
{ |
|
||||
sut.CopyAsync(fileName, id, 1, string.Empty); |
|
||||
|
|
||||
A.CallTo(() => sut.CopyAsync(fileName, $"{id}_1", default)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_copy_with_id_and_version_and_suffix() |
|
||||
{ |
|
||||
sut.CopyAsync(fileName, id, 1, "Crop"); |
|
||||
|
|
||||
A.CallTo(() => sut.CopyAsync(fileName, $"{id}_1_Crop", default)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_upload_with_id_and_version() |
|
||||
{ |
|
||||
sut.UploadAsync(id, 1, string.Empty, stream, true); |
|
||||
|
|
||||
A.CallTo(() => sut.UploadAsync($"{id}_1", stream, true, default)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_upload_with_id_and_version_and_suffix() |
|
||||
{ |
|
||||
sut.UploadAsync(id, 1, "Crop", stream, true); |
|
||||
|
|
||||
A.CallTo(() => sut.UploadAsync($"{id}_1_Crop", stream, true, default)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_download_with_id_and_version() |
|
||||
{ |
|
||||
sut.DownloadAsync(id, 1, string.Empty, stream); |
|
||||
|
|
||||
A.CallTo(() => sut.DownloadAsync($"{id}_1", stream, default)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_download_with_id_and_version_and_suffix() |
|
||||
{ |
|
||||
sut.DownloadAsync(id, 1, "Crop", stream); |
|
||||
|
|
||||
A.CallTo(() => sut.DownloadAsync($"{id}_1_Crop", stream, default)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_delete_with_id_and_version() |
|
||||
{ |
|
||||
sut.DeleteAsync(id, 1, string.Empty); |
|
||||
|
|
||||
A.CallTo(() => sut.DeleteAsync($"{id}_1")) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_delete_with_id_and_version_and_suffix() |
|
||||
{ |
|
||||
sut.DeleteAsync(id, 1, "Crop"); |
|
||||
|
|
||||
A.CallTo(() => sut.DeleteAsync($"{id}_1_Crop")) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_generate_url_with_id_and_version() |
|
||||
{ |
|
||||
sut.GeneratePublicUrl(id, 1, string.Empty); |
|
||||
|
|
||||
A.CallTo(() => sut.GeneratePublicUrl($"{id}_1")) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_generate_url_with_id_and_version_and_suffix() |
|
||||
{ |
|
||||
sut.GeneratePublicUrl(id, 1, "Crop"); |
|
||||
|
|
||||
A.CallTo(() => sut.GeneratePublicUrl($"{id}_1_Crop")) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,125 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using System.Threading.Tasks.Dataflow; |
|
||||
using Squidex.Domain.Apps.Entities.Apps; |
|
||||
using Squidex.Domain.Apps.Entities.Apps.State; |
|
||||
using Squidex.Domain.Apps.Entities.Assets; |
|
||||
using Squidex.Domain.Apps.Entities.Assets.State; |
|
||||
using Squidex.Domain.Apps.Entities.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.State; |
|
||||
using Squidex.Domain.Apps.Entities.Rules; |
|
||||
using Squidex.Domain.Apps.Entities.Rules.State; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas.State; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Caching; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
using Squidex.Infrastructure.EventSourcing; |
|
||||
using Squidex.Infrastructure.States; |
|
||||
|
|
||||
namespace Migrate_01 |
|
||||
{ |
|
||||
public sealed class Rebuilder |
|
||||
{ |
|
||||
private readonly ILocalCache localCache; |
|
||||
private readonly IStore<Guid> store; |
|
||||
private readonly IEventStore eventStore; |
|
||||
|
|
||||
public Rebuilder( |
|
||||
ILocalCache localCache, |
|
||||
IStore<Guid> store, |
|
||||
IEventStore eventStore) |
|
||||
{ |
|
||||
this.eventStore = eventStore; |
|
||||
this.localCache = localCache; |
|
||||
this.store = store; |
|
||||
} |
|
||||
|
|
||||
public Task RebuildAppsAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
return RebuildManyAsync<AppState, AppGrain>("^app\\-", ct); |
|
||||
} |
|
||||
|
|
||||
public Task RebuildSchemasAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
return RebuildManyAsync<SchemaState, SchemaGrain>("^schema\\-", ct); |
|
||||
} |
|
||||
|
|
||||
public Task RebuildRulesAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
return RebuildManyAsync<RuleState, RuleGrain>("^rule\\-", ct); |
|
||||
} |
|
||||
|
|
||||
public Task RebuildAssetsAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
return RebuildManyAsync<AssetState, AssetGrain>("^asset\\-", ct); |
|
||||
} |
|
||||
|
|
||||
public Task RebuildContentAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
return RebuildManyAsync<ContentState, ContentGrain>("^content\\-", ct); |
|
||||
} |
|
||||
|
|
||||
private async Task RebuildManyAsync<TState, TGrain>(string filter, CancellationToken ct) where TState : IDomainState<TState>, new() |
|
||||
{ |
|
||||
var handledIds = new HashSet<Guid>(); |
|
||||
|
|
||||
var worker = new ActionBlock<Guid>(async id => |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var state = new TState |
|
||||
{ |
|
||||
Version = EtagVersion.Empty |
|
||||
}; |
|
||||
|
|
||||
var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), id, (TState s) => state = s, e => |
|
||||
{ |
|
||||
state = state.Apply(e); |
|
||||
|
|
||||
state.Version++; |
|
||||
}); |
|
||||
|
|
||||
await persistence.ReadAsync(); |
|
||||
await persistence.WriteSnapshotAsync(state); |
|
||||
} |
|
||||
catch (DomainObjectNotFoundException) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
}, |
|
||||
new ExecutionDataflowBlockOptions |
|
||||
{ |
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount * 2 |
|
||||
}); |
|
||||
|
|
||||
using (localCache.StartContext()) |
|
||||
{ |
|
||||
await store.GetSnapshotStore<TState>().ClearAsync(); |
|
||||
|
|
||||
await eventStore.QueryAsync(async storedEvent => |
|
||||
{ |
|
||||
var id = storedEvent.Data.Headers.AggregateId(); |
|
||||
|
|
||||
if (handledIds.Add(id)) |
|
||||
{ |
|
||||
await worker.SendAsync(id, ct); |
|
||||
} |
|
||||
}, filter, ct: ct); |
|
||||
|
|
||||
worker.Complete(); |
|
||||
|
|
||||
await worker.Completion; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue