Browse Source

Backup started.

pull/261/head
Sebastian Stehle 8 years ago
parent
commit
a706ad5757
  1. 242
      src/Squidex.Domain.Apps.Backup/BackupGrain.cs
  2. 80
      src/Squidex.Domain.Apps.Backup/EventStreamWriter.cs
  3. 13
      src/Squidex.Domain.Apps.Backup/IBackupArchiveLocation.cs
  4. 24
      src/Squidex.Domain.Apps.Backup/IBackupGrain.cs
  5. 25
      src/Squidex.Domain.Apps.Backup/IBackupJob.cs
  6. 22
      src/Squidex.Domain.Apps.Backup/Squidex.Domain.Apps.Backup.csproj
  7. 18
      src/Squidex.Domain.Apps.Backup/State/BackupState.cs
  8. 31
      src/Squidex.Domain.Apps.Backup/State/BackupStateJob.cs
  9. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  10. 12
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  11. 242
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  12. 79
      src/Squidex.Domain.Apps.Entities/Backup/EventStreamWriter.cs
  13. 20
      src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs
  14. 24
      src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs
  15. 23
      src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs
  16. 18
      src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs
  17. 28
      src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs
  18. 44
      src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs
  19. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  20. 22
      src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs
  21. 26
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentStateScheduleItem.cs
  22. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  23. 2
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  24. 31
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
  25. 2
      src/Squidex.Domain.Apps.Entities/SquidexEntities.cs
  26. 18
      src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs
  27. 23
      src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
  28. 19
      src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
  29. 18
      src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  30. 11
      src/Squidex.Infrastructure/Assets/IAssetStore.cs
  31. 4
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  32. 105
      src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs
  33. 38
      src/Squidex/Areas/Api/Controllers/Backup/Models/BackupJobDto.cs
  34. 13
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  35. 9
      tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs
  36. 12
      tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs

242
src/Squidex.Domain.Apps.Backup/BackupGrain.cs

@ -0,0 +1,242 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NodaTime;
using Orleans;
using Orleans.Concurrency;
using Squidex.Domain.Apps.Backup.State;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Backup
{
[Reentrant]
public sealed class BackupGrain : Grain, IBackupGrain
{
private const int MaxBackups = 10;
private readonly IClock clock;
private readonly IAssetStore assetStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly ISemanticLog log;
private readonly IEventStore eventStore;
private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IStore<Guid> store;
private CancellationTokenSource currentTask;
private BackupStateJob currentJob;
private Guid appId;
private BackupState state;
private IPersistence<BackupState> persistence;
public BackupGrain(
IAssetStore assetStore,
IBackupArchiveLocation backupArchiveLocation,
IClock clock,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISemanticLog log,
IStore<Guid> store)
{
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation));
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(store, nameof(store));
Guard.NotNull(log, nameof(log));
this.assetStore = assetStore;
this.backupArchiveLocation = backupArchiveLocation;
this.clock = clock;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.store = store;
this.log = log;
}
public override Task OnActivateAsync()
{
return OnActivateAsync(this.GetPrimaryKey());
}
public async Task OnActivateAsync(Guid appId)
{
this.appId = appId;
persistence = store.WithSnapshots<BackupState, Guid>(GetType(), appId, s => state = s);
await ReadAsync();
await CleanupAsync();
}
private async Task ReadAsync()
{
await persistence.ReadAsync();
}
private async Task WriteAsync()
{
await persistence.WriteSnapshotAsync(state);
}
private async Task CleanupAsync()
{
var hasUpdated = false;
foreach (var job in state.Jobs)
{
if (!job.Stopped.HasValue)
{
await CleanupAsync(job);
job.Stopped = clock.GetCurrentInstant();
job.Failed = true;
hasUpdated = true;
}
}
if (hasUpdated)
{
await WriteAsync();
}
}
private async Task CleanupAsync(BackupStateJob job)
{
await backupArchiveLocation.DeleteArchiveAsync(job.Id);
}
public async Task StartNewAsync()
{
if (currentTask != null)
{
throw new DomainException("Another backup process is already running.");
}
if (state.Jobs.Count >= MaxBackups)
{
throw new DomainException($"You cannot have more than {MaxBackups} backups.");
}
var job = new BackupStateJob { Id = Guid.NewGuid(), Started = clock.GetCurrentInstant() };
currentTask = new CancellationTokenSource();
currentJob = job;
state.Jobs.Add(job);
await WriteAsync();
try
{
using (var stream = await backupArchiveLocation.OpenStreamAsync(job.Id))
{
using (var writer = new EventStreamWriter(stream))
{
await eventStore.QueryAsync(async @event =>
{
var eventData = @event.Data;
if (eventData.Type == nameof(AssetCreated) ||
eventData.Type == nameof(AssetUpdated))
{
var parsedEvent = eventDataFormatter.Parse(eventData);
var assetVersion = 0L;
var assetId = Guid.Empty;
if (parsedEvent.Payload is AssetCreated assetCreated)
{
assetId = assetCreated.AssetId;
assetVersion = assetCreated.FileVersion;
}
if (parsedEvent.Payload is AssetUpdated asetUpdated)
{
assetId = asetUpdated.AssetId;
assetVersion = asetUpdated.FileVersion;
}
await writer.WriteEventAsync(eventData, async attachmentStream =>
{
await assetStore.DownloadAsync(assetId.ToString(), assetVersion, null, attachmentStream);
});
}
else
{
await writer.WriteEventAsync(eventData);
}
}, "AppId", appId, null, currentTask.Token);
}
stream.Position = 0;
currentTask.Token.ThrowIfCancellationRequested();
await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream);
currentTask.Token.ThrowIfCancellationRequested();
}
}
catch
{
job.Failed = true;
}
finally
{
job.Stopped = clock.GetCurrentInstant();
await WriteAsync();
currentTask = null;
currentJob = null;
}
}
public async Task DeleteAsync(Guid id)
{
var job = state.Jobs.FirstOrDefault(x => x.Id == id);
if (job == null)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(IBackupJob));
}
if (currentJob == job)
{
currentTask?.Cancel();
}
else
{
state.Jobs.Remove(job);
await WriteAsync();
await CleanupAsync(job);
}
}
public Task<J<List<IBackupJob>>> GetStateAsync()
{
return Task.FromResult(new J<List<IBackupJob>>(state.Jobs.OfType<IBackupJob>().ToList()));
}
private bool IsRunning()
{
return state.Jobs.Any(x => !x.Stopped.HasValue);
}
}
}

80
src/Squidex.Domain.Apps.Backup/EventStreamWriter.cs

@ -0,0 +1,80 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Backup
{
public sealed class EventStreamWriter : DisposableObjectBase
{
private const int MaxItemsPerFolder = 1000;
private readonly StreamWriter streamWriter;
private readonly ZipArchive archive;
private int writtenEvents;
private int writtenAttachments;
public EventStreamWriter(Stream stream)
{
archive = new ZipArchive(stream, ZipArchiveMode.Update, true);
}
public async Task WriteEventAsync(EventData eventData, Func<Stream, Task> attachment = null)
{
var eventObject =
new JObject(
new JProperty("type", eventData.Type),
new JProperty("payload", eventData.Payload),
new JProperty("metadata", eventData.Metadata));
var eventFolder = writtenEvents / MaxItemsPerFolder;
var eventPath = $"events/{eventFolder}/{writtenEvents}.json";
var eventEntry = archive.GetEntry(eventPath) ?? archive.CreateEntry(eventPath);
using (var stream = eventEntry.Open())
{
using (var textWriter = new StreamWriter(stream))
{
using (var jsonWriter = new JsonTextWriter(textWriter))
{
await eventObject.WriteToAsync(jsonWriter);
}
}
}
writtenEvents++;
if (attachment != null)
{
var attachmentFolder = writtenAttachments / MaxItemsPerFolder;
var attachmentPath = $"attachments/{attachmentFolder}/{writtenEvents}.blob";
var attachmentEntry = archive.GetEntry(attachmentPath) ?? archive.CreateEntry(attachmentPath);
using (var stream = eventEntry.Open())
{
await attachment(stream);
}
writtenAttachments++;
}
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
archive.Dispose();
}
}
}
}

13
src/Squidex.Domain.Apps.Backup/IBackupArchiveLocation.cs

@ -0,0 +1,13 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Backup
{
public interface IBackupArchiveLocation
{
Task<Stream> OpenStreamAsync(Guid backupId);
Task DeleteArchiveAsync(Guid backupId);
}
}

24
src/Squidex.Domain.Apps.Backup/IBackupGrain.cs

@ -0,0 +1,24 @@
// ==========================================================================
// 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.Orleans;
namespace Squidex.Domain.Apps.Backup
{
public interface IBackupGrain : IGrainWithGuidKey
{
Task StartNewAsync();
Task DeleteAsync(Guid id);
Task<J<List<IBackupJob>>> GetStateAsync();
}
}

25
src/Squidex.Domain.Apps.Backup/IBackupJob.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Backup
{
public interface IBackupJob
{
Guid Id { get; }
Instant Started { get; }
Instant? Stopped { get; }
bool Failed { get; }
string DownloadPath { get; }
}
}

22
src/Squidex.Domain.Apps.Backup/Squidex.Domain.Apps.Backup.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0-rc2" />
<PackageReference Include="NodaTime" Version="2.2.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>

18
src/Squidex.Domain.Apps.Backup/State/BackupState.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Backup.State
{
public sealed class BackupState
{
[JsonProperty]
public List<BackupStateJob> Jobs { get; set; } = new List<BackupStateJob>();
}
}

31
src/Squidex.Domain.Apps.Backup/State/BackupStateJob.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Newtonsoft.Json;
using NodaTime;
namespace Squidex.Domain.Apps.Backup.State
{
public sealed class BackupStateJob : IBackupJob
{
[JsonProperty]
public Guid Id { get; set; }
[JsonProperty]
public Instant Started { get; set; }
[JsonProperty]
public Instant? Stopped { get; set; }
[JsonProperty]
public string DownloadPath { get; set; }
[JsonProperty]
public bool Failed { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -25,7 +25,7 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppGrain : DomainObjectGrain<AppState>, IAppGrain
public class AppGrain : SquidexDomainObjectGrain<AppState>, IAppGrain
{
private readonly InitialPatterns initialPatterns;
private readonly IAppProvider appProvider;

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

@ -41,18 +41,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), createAsset.File.OpenRead());
await assetStore.UploadAsync(context.ContextId.ToString(), createAsset.File.OpenRead());
try
{
var result = await ExecuteCommandAsync(createAsset) as AssetSavedResult;
context.Complete(EntityCreatedResult.Create(createAsset.AssetId, result.Version));
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
await assetStore.DeleteAsync(context.ContextId.ToString());
}
break;
@ -62,18 +62,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead());
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), updateAsset.File.OpenRead());
await assetStore.UploadAsync(context.ContextId.ToString(), updateAsset.File.OpenRead());
try
{
var result = await ExecuteCommandAsync(updateAsset) as AssetSavedResult;
context.Complete(result);
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
await assetStore.DeleteAsync(context.ContextId.ToString());
}
break;

242
src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs

@ -0,0 +1,242 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NodaTime;
using Orleans;
using Orleans.Concurrency;
using Squidex.Domain.Apps.Entities.Backup.State;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Backup
{
[Reentrant]
public sealed class BackupGrain : Grain, IBackupGrain
{
private const int MaxBackups = 10;
private readonly IClock clock;
private readonly IAssetStore assetStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly ISemanticLog log;
private readonly IEventStore eventStore;
private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IStore<Guid> store;
private CancellationTokenSource currentTask;
private BackupStateJob currentJob;
private Guid appId;
private BackupState state;
private IPersistence<BackupState> persistence;
public BackupGrain(
IAssetStore assetStore,
IBackupArchiveLocation backupArchiveLocation,
IClock clock,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISemanticLog log,
IStore<Guid> store)
{
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation));
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(store, nameof(store));
Guard.NotNull(log, nameof(log));
this.assetStore = assetStore;
this.backupArchiveLocation = backupArchiveLocation;
this.clock = clock;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.store = store;
this.log = log;
}
public override Task OnActivateAsync()
{
return OnActivateAsync(this.GetPrimaryKey());
}
public async Task OnActivateAsync(Guid appId)
{
this.appId = appId;
persistence = store.WithSnapshots<BackupState, Guid>(GetType(), appId, s => state = s);
await ReadAsync();
await CleanupAsync();
}
private async Task ReadAsync()
{
await persistence.ReadAsync();
}
private async Task WriteAsync()
{
await persistence.WriteSnapshotAsync(state);
}
private async Task CleanupAsync()
{
var hasUpdated = false;
foreach (var job in state.Jobs)
{
if (!job.Stopped.HasValue)
{
await CleanupAsync(job);
job.Stopped = clock.GetCurrentInstant();
job.Failed = true;
hasUpdated = true;
}
}
if (hasUpdated)
{
await WriteAsync();
}
}
private async Task CleanupAsync(BackupStateJob job)
{
await backupArchiveLocation.DeleteArchiveAsync(job.Id);
}
public async Task StartNewAsync()
{
if (currentTask != null)
{
throw new DomainException("Another backup process is already running.");
}
if (state.Jobs.Count >= MaxBackups)
{
throw new DomainException($"You cannot have more than {MaxBackups} backups.");
}
var job = new BackupStateJob { Id = Guid.NewGuid(), Started = clock.GetCurrentInstant() };
currentTask = new CancellationTokenSource();
currentJob = job;
state.Jobs.Add(job);
await WriteAsync();
try
{
using (var stream = await backupArchiveLocation.OpenStreamAsync(job.Id))
{
using (var writer = new EventStreamWriter(stream))
{
await eventStore.QueryAsync(async @event =>
{
var eventData = @event.Data;
if (eventData.Type == nameof(AssetCreated) ||
eventData.Type == nameof(AssetUpdated))
{
var parsedEvent = eventDataFormatter.Parse(eventData);
var assetVersion = 0L;
var assetId = Guid.Empty;
if (parsedEvent.Payload is AssetCreated assetCreated)
{
assetId = assetCreated.AssetId;
assetVersion = assetCreated.FileVersion;
}
if (parsedEvent.Payload is AssetUpdated asetUpdated)
{
assetId = asetUpdated.AssetId;
assetVersion = asetUpdated.FileVersion;
}
await writer.WriteEventAsync(eventData, async attachmentStream =>
{
await assetStore.DownloadAsync(assetId.ToString(), assetVersion, null, attachmentStream);
});
}
else
{
await writer.WriteEventAsync(eventData);
}
}, "AppId", appId, null, currentTask.Token);
}
stream.Position = 0;
currentTask.Token.ThrowIfCancellationRequested();
await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, currentTask.Token);
currentTask.Token.ThrowIfCancellationRequested();
}
}
catch
{
job.Failed = true;
}
finally
{
job.Stopped = clock.GetCurrentInstant();
await WriteAsync();
currentTask = null;
currentJob = null;
}
}
public async Task DeleteAsync(Guid id)
{
var job = state.Jobs.FirstOrDefault(x => x.Id == id);
if (job == null)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(IBackupJob));
}
if (currentJob == job)
{
currentTask?.Cancel();
}
else
{
state.Jobs.Remove(job);
await WriteAsync();
await CleanupAsync(job);
}
}
public Task<J<List<IBackupJob>>> GetStateAsync()
{
return Task.FromResult(new J<List<IBackupJob>>(state.Jobs.OfType<IBackupJob>().ToList()));
}
private bool IsRunning()
{
return state.Jobs.Any(x => !x.Stopped.HasValue);
}
}
}

79
src/Squidex.Domain.Apps.Entities/Backup/EventStreamWriter.cs

@ -0,0 +1,79 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed class EventStreamWriter : DisposableObjectBase
{
private const int MaxItemsPerFolder = 1000;
private readonly ZipArchive archive;
private int writtenEvents;
private int writtenAttachments;
public EventStreamWriter(Stream stream)
{
archive = new ZipArchive(stream, ZipArchiveMode.Update, true);
}
public async Task WriteEventAsync(EventData eventData, Func<Stream, Task> attachment = null)
{
var eventObject =
new JObject(
new JProperty("type", eventData.Type),
new JProperty("payload", eventData.Payload),
new JProperty("metadata", eventData.Metadata));
var eventFolder = writtenEvents / MaxItemsPerFolder;
var eventPath = $"events/{eventFolder}/{writtenEvents}.json";
var eventEntry = archive.GetEntry(eventPath) ?? archive.CreateEntry(eventPath);
using (var stream = eventEntry.Open())
{
using (var textWriter = new StreamWriter(stream))
{
using (var jsonWriter = new JsonTextWriter(textWriter))
{
await eventObject.WriteToAsync(jsonWriter);
}
}
}
writtenEvents++;
if (attachment != null)
{
var attachmentFolder = writtenAttachments / MaxItemsPerFolder;
var attachmentPath = $"attachments/{attachmentFolder}/{writtenEvents}.blob";
var attachmentEntry = archive.GetEntry(attachmentPath) ?? archive.CreateEntry(attachmentPath);
using (var stream = eventEntry.Open())
{
await attachment(stream);
}
writtenAttachments++;
}
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
archive.Dispose();
}
}
}
}

20
src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs

@ -0,0 +1,20 @@
// ==========================================================================
// 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;
namespace Squidex.Domain.Apps.Entities.Backup
{
public interface IBackupArchiveLocation
{
Task<Stream> OpenStreamAsync(Guid backupId);
Task DeleteArchiveAsync(Guid backupId);
}
}

24
src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs

@ -0,0 +1,24 @@
// ==========================================================================
// 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.Orleans;
namespace Squidex.Domain.Apps.Entities.Backup
{
public interface IBackupGrain : IGrainWithGuidKey
{
Task StartNewAsync();
Task DeleteAsync(Guid id);
Task<J<List<IBackupJob>>> GetStateAsync();
}
}

23
src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Entities.Backup
{
public interface IBackupJob
{
Guid Id { get; }
Instant Started { get; }
Instant? Stopped { get; }
bool Failed { get; }
}
}

18
src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Entities.Backup.State
{
public sealed class BackupState
{
[JsonProperty]
public List<BackupStateJob> Jobs { get; set; } = new List<BackupStateJob>();
}
}

28
src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs

@ -0,0 +1,28 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Newtonsoft.Json;
using NodaTime;
namespace Squidex.Domain.Apps.Entities.Backup.State
{
public sealed class BackupStateJob : IBackupJob
{
[JsonProperty]
public Guid Id { get; set; }
[JsonProperty]
public Instant Started { get; set; }
[JsonProperty]
public Instant? Stopped { get; set; }
[JsonProperty]
public bool Failed { get; set; }
}
}

44
src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs

@ -0,0 +1,44 @@
// ==========================================================================
// 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.Tasks;
namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation
{
public Task<Stream> OpenStreamAsync(Guid backupId)
{
var tempFile = GetTempFile(backupId);
return Task.FromResult<Stream>(new FileStream(tempFile, FileMode.Open, FileAccess.ReadWrite));
}
public Task DeleteArchiveAsync(Guid backupId)
{
var tempFile = GetTempFile(backupId);
try
{
File.Delete(tempFile);
}
catch (IOException)
{
}
return TaskHelper.Done;
}
private static string GetTempFile(Guid backupId)
{
return Path.Combine(Path.GetTempPath(), backupId.ToString());
}
}
}

2
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -24,7 +24,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentGrain : DomainObjectGrain<ContentState>, IContentGrain
public class ContentGrain : SquidexDomainObjectGrain<ContentState>, IContentGrain
{
private readonly IAppProvider appProvider;
private readonly IAssetRepository assetRepository;

22
src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentScheduleItem
{
Status ScheduledTo { get; }
Instant ScheduledAt { get; }
RefToken ScheduledBy { get; }
}
}

26
src/Squidex.Domain.Apps.Entities/Contents/State/ContentStateScheduleItem.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.State
{
public sealed class ContentStateScheduleItem : IContentScheduleItem
{
[JsonProperty]
public Instant ScheduledAt { get; set; }
[JsonProperty]
public RefToken ScheduledBy { get; set; }
[JsonProperty]
public Status ScheduledTo { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -21,7 +21,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules
{
public sealed class RuleGrain : DomainObjectGrain<RuleState>, IRuleGrain
public sealed class RuleGrain : SquidexDomainObjectGrain<RuleState>, IRuleGrain
{
private readonly IAppProvider appProvider;

2
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -24,7 +24,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain
public class SchemaGrain : SquidexDomainObjectGrain<SchemaState>, ISchemaGrain
{
private readonly IAppProvider appProvider;
private readonly FieldRegistry registry;

31
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState, new()
{
protected SquidexDomainObjectGrain(IStore<Guid> store)
: base(store)
{
}
public override void RaiseEvent(Envelope<IEvent> @event)
{
if (@event.Payload is AppEvent appEvent)
{
@event.SetAppId(appEvent.AppId.Id);
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/SquidexEntities.cs

@ -9,7 +9,7 @@ using System.Reflection;
namespace Squidex.Domain.Apps.Entities
{
public sealed class SquidexEntities
public static class SquidexEntities
{
public static readonly Assembly Assembly = typeof(SquidexEntities).Assembly;
}

18
src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentStatusChanged))]
public sealed class ContentScheduleItemRemoved : ContentEvent
{
public Guid ScheduleItemId { get; set; }
}
}

23
src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs

@ -7,6 +7,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
@ -56,7 +57,7 @@ namespace Squidex.Infrastructure.Assets
return new Uri(blobContainer.StorageUri.PrimaryUri, $"/{containerName}/{blobName}").ToString();
}
public async Task CopyTemporaryAsync(string name, string id, long version, string suffix)
public async Task CopyAsync(string name, string id, long version, string suffix, CancellationToken ct = default(CancellationToken))
{
var blobName = GetObjectName(id, version, suffix);
var blobRef = blobContainer.GetBlobReference(blobName);
@ -65,12 +66,14 @@ namespace Squidex.Infrastructure.Assets
try
{
await blobRef.StartCopyAsync(tempBlob.Uri);
await blobRef.StartCopyAsync(tempBlob.Uri, null, null, null, null, ct);
while (blobRef.CopyState.Status == CopyStatus.Pending)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(50);
await blobRef.FetchAttributesAsync();
await blobRef.FetchAttributesAsync(null, null, null, ct);
}
if (blobRef.CopyState.Status != CopyStatus.Success)
@ -84,14 +87,14 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
public async Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken))
{
var blobName = GetObjectName(id, version, suffix);
var blobRef = blobContainer.GetBlockBlobReference(blobName);
try
{
await blobRef.DownloadToStreamAsync(stream);
await blobRef.DownloadToStreamAsync(stream, null, null, null, ct);
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{
@ -99,7 +102,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
public async Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken))
{
var blobName = GetObjectName(id, version, suffix);
var blobRef = blobContainer.GetBlockBlobReference(blobName);
@ -107,18 +110,18 @@ namespace Squidex.Infrastructure.Assets
blobRef.Metadata[AssetVersion] = version.ToString();
blobRef.Metadata[AssetId] = id;
await blobRef.UploadFromStreamAsync(stream);
await blobRef.UploadFromStreamAsync(stream, null, null, null, ct);
await blobRef.SetMetadataAsync();
}
public async Task UploadTemporaryAsync(string name, Stream stream)
public async Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken))
{
var tempBlob = blobContainer.GetBlockBlobReference(name);
await tempBlob.UploadFromStreamAsync(stream);
await tempBlob.UploadFromStreamAsync(stream, null, null, null, ct);
}
public async Task DeleteTemporaryAsync(string name)
public async Task DeleteAsync(string name)
{
var tempBlob = blobContainer.GetBlockBlobReference(name);

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

@ -9,6 +9,7 @@ using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Google;
using Google.Cloud.Storage.V1;
@ -48,25 +49,25 @@ namespace Squidex.Infrastructure.Assets
return $"https://storage.cloud.google.com/{bucketName}/{objectName}";
}
public Task UploadTemporaryAsync(string name, Stream stream)
public Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken))
{
return storageClient.UploadObjectAsync(bucketName, name, "application/octet-stream", stream);
return storageClient.UploadObjectAsync(bucketName, name, "application/octet-stream", stream, cancellationToken: ct);
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
public async Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken))
{
var objectName = GetObjectName(id, version, suffix);
await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream);
await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream, cancellationToken: ct);
}
public async Task CopyTemporaryAsync(string name, string id, long version, string suffix)
public async Task CopyAsync(string name, string id, long version, string suffix, CancellationToken ct = default(CancellationToken))
{
var objectName = GetObjectName(id, version, suffix);
try
{
await storageClient.CopyObjectAsync(bucketName, name, bucketName, objectName);
await storageClient.CopyObjectAsync(bucketName, name, bucketName, objectName, cancellationToken: ct);
}
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
@ -74,13 +75,13 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
public async Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken))
{
var objectName = GetObjectName(id, version, suffix);
try
{
await storageClient.DownloadObjectAsync(bucketName, objectName, stream);
await storageClient.DownloadObjectAsync(bucketName, objectName, stream, cancellationToken: ct);
}
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
@ -88,7 +89,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task DeleteTemporaryAsync(string name)
public async Task DeleteAsync(string name)
{
try
{

18
src/Squidex.Infrastructure/Assets/FolderAssetStore.cs

@ -7,6 +7,7 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
@ -15,6 +16,7 @@ namespace Squidex.Infrastructure.Assets
{
public sealed class FolderAssetStore : IAssetStore, IInitializable
{
private const int BufferSize = 81920;
private readonly ISemanticLog log;
private readonly DirectoryInfo directory;
@ -57,27 +59,27 @@ namespace Squidex.Infrastructure.Assets
return file.FullName;
}
public async Task UploadTemporaryAsync(string name, Stream stream)
public async Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken))
{
var file = GetFile(name);
using (var fileStream = file.OpenWrite())
{
await stream.CopyToAsync(fileStream);
await stream.CopyToAsync(fileStream, BufferSize, ct);
}
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
public async Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken))
{
var file = GetFile(id, version, suffix);
using (var fileStream = file.OpenWrite())
{
await stream.CopyToAsync(fileStream);
await stream.CopyToAsync(fileStream, BufferSize, ct);
}
}
public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
public async Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken))
{
var file = GetFile(id, version, suffix);
@ -85,7 +87,7 @@ namespace Squidex.Infrastructure.Assets
{
using (var fileStream = file.OpenRead())
{
await fileStream.CopyToAsync(stream);
await fileStream.CopyToAsync(stream, BufferSize, ct);
}
}
catch (FileNotFoundException ex)
@ -94,7 +96,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public Task CopyTemporaryAsync(string name, string id, long version, string suffix)
public Task CopyAsync(string name, string id, long version, string suffix, CancellationToken ct = default(CancellationToken))
{
try
{
@ -110,7 +112,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public Task DeleteTemporaryAsync(string name)
public Task DeleteAsync(string name)
{
try
{

11
src/Squidex.Infrastructure/Assets/IAssetStore.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Assets
@ -14,14 +15,14 @@ namespace Squidex.Infrastructure.Assets
{
string GenerateSourceUrl(string id, long version, string suffix);
Task CopyTemporaryAsync(string name, string id, long version, string suffix);
Task CopyAsync(string name, string id, long version, string suffix, CancellationToken ct = default(CancellationToken));
Task DownloadAsync(string id, long version, string suffix, Stream stream);
Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken));
Task UploadTemporaryAsync(string name, Stream stream);
Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken));
Task UploadAsync(string id, long version, string suffix, Stream stream);
Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken));
Task DeleteTemporaryAsync(string name);
Task DeleteAsync(string name);
}
}

4
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -98,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <returns>
/// 204 => Language updated.
/// 400 => Language object is invalid.
/// 404 => App not found.
/// 404 => Language or app not found.
/// </returns>
[MustBeAppEditor]
[HttpPut]
@ -118,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="language">The language to delete from the app.</param>
/// <returns>
/// 204 => Language deleted.
/// 404 => App not found.
/// 404 => Language or app not found.
/// </returns>
[MustBeAppEditor]
[HttpDelete]

105
src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs

@ -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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Orleans;
using Squidex.Areas.Api.Controllers.Backup.Models;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Backup
{
/// <summary>
/// Manages backups for app.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppOwner]
[SwaggerTag(nameof(Backup))]
public class BackupController : ApiController
{
private readonly IGrainFactory grainFactory;
public BackupController(ICommandBus commandBus, IGrainFactory grainFactory)
: base(commandBus)
{
this.grainFactory = grainFactory;
}
/// <summary>
/// Get all backup jobs.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Backups returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)]
public async Task<IActionResult> GetJobs(string app)
{
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
var jobs = await backupGrain.GetStateAsync();
return Ok(jobs.Value.Select(x => SimpleMapper.Map(x, new BackupJobDto())).ToList());
}
/// <summary>
/// Start a new backup.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 204 => Backup started.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app)
{
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
await backupGrain.StartNewAsync();
return NoContent();
}
/// <summary>
/// Delete a backup.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the backup to delete.</param>
/// <returns>
/// 204 => Backup started.
/// 404 => Backup or app not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/backups/{id}")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app, Guid id)
{
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
await backupGrain.StartNewAsync();
return NoContent();
}
}
}

38
src/Squidex/Areas/Api/Controllers/Backup/Models/BackupJobDto.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
namespace Squidex.Areas.Api.Controllers.Backup.Models
{
public sealed class BackupJobDto
{
/// <summary>
/// The id of the backup job.
/// </summary>
[Required]
public Guid Id { get; set; }
/// <summary>
/// The time when the job has been started.
/// </summary>
[Required]
public Instant Started { get; set; }
/// <summary>
/// The time when the job has been stopped.
/// </summary>
public Instant? Stopped { get; }
/// <summary>
/// Indicates if the job has failed.
/// </summary>
public bool Failed { get; }
}
}

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

@ -7,6 +7,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Orleans;
@ -89,21 +90,21 @@ namespace Squidex.Domain.Apps.Entities.Assets
private void SetupStore(long version, Guid commitId)
{
A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream))
A.CallTo(() => assetStore.UploadAsync(commitId.ToString(), stream, CancellationToken.None))
.Returns(TaskHelper.Done);
A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null))
A.CallTo(() => assetStore.CopyAsync(commitId.ToString(), assetId.ToString(), version, null, CancellationToken.None))
.Returns(TaskHelper.Done);
A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString()))
A.CallTo(() => assetStore.DeleteAsync(commitId.ToString()))
.Returns(TaskHelper.Done);
}
private void AssertAssetHasBeenUploaded(long version, Guid commitId)
{
A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream))
A.CallTo(() => assetStore.UploadAsync(commitId.ToString(), stream, CancellationToken.None))
.MustHaveHappened();
A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null))
A.CallTo(() => assetStore.CopyAsync(commitId.ToString(), assetId.ToString(), version, null, CancellationToken.None))
.MustHaveHappened();
A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString()))
A.CallTo(() => assetStore.DeleteAsync(commitId.ToString()))
.MustHaveHappened();
}

9
tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs

@ -7,6 +7,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.Assets;
@ -33,18 +34,18 @@ namespace Squidex.Domain.Users
{
var stream = new MemoryStream();
A.CallTo(() => assetStore.UploadAsync(userId, 0, "picture", stream))
A.CallTo(() => assetStore.UploadAsync(userId, 0, "picture", stream, CancellationToken.None))
.Returns(TaskHelper.Done);
await sut.UploadAsync(userId, stream);
A.CallTo(() => assetStore.UploadAsync(userId, 0, "picture", stream)).MustHaveHappened();
A.CallTo(() => assetStore.UploadAsync(userId, 0, "picture", stream, CancellationToken.None)).MustHaveHappened();
}
[Fact]
public async Task Should_invoke_asset_store_to_download_picture()
{
A.CallTo(() => assetStore.DownloadAsync(userId, 0, "picture", A<Stream>.Ignored))
A.CallTo(() => assetStore.DownloadAsync(userId, 0, "picture", A<Stream>.Ignored, CancellationToken.None))
.Invokes(async (string id, long version, string suffix, Stream stream) =>
{
await stream.WriteAsync(new byte[] { 1, 2, 3, 4 }, 0, 4);
@ -55,7 +56,7 @@ namespace Squidex.Domain.Users
Assert.Equal(0, result.Position);
Assert.Equal(4, result.Length);
A.CallTo(() => assetStore.DownloadAsync(userId, 0, "picture", A<Stream>.Ignored)).MustHaveHappened();
A.CallTo(() => assetStore.DownloadAsync(userId, 0, "picture", A<Stream>.Ignored, CancellationToken.None)).MustHaveHappened();
}
}
}

12
tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs

@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.Assets
{
((IInitializable)Sut).Initialize();
return Assert.ThrowsAsync<AssetNotFoundException>(() => Sut.CopyTemporaryAsync(Id(), Id(), 1, null));
return Assert.ThrowsAsync<AssetNotFoundException>(() => Sut.CopyAsync(Id(), Id(), 1, null));
}
[Fact]
@ -73,8 +73,8 @@ namespace Squidex.Infrastructure.Assets
var assetId = Id();
var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 });
await Sut.UploadTemporaryAsync(tempId, assetData);
await Sut.CopyTemporaryAsync(tempId, assetId, 1, "suffix");
await Sut.UploadAsync(tempId, assetData);
await Sut.CopyAsync(tempId, assetId, 1, "suffix");
var readData = new MemoryStream();
@ -92,9 +92,9 @@ namespace Squidex.Infrastructure.Assets
var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 });
await Sut.UploadTemporaryAsync(tempId, assetData);
await Sut.DeleteTemporaryAsync(tempId);
await Sut.DeleteTemporaryAsync(tempId);
await Sut.UploadAsync(tempId, assetData);
await Sut.DeleteAsync(tempId);
await Sut.DeleteAsync(tempId);
}
private static string Id()

Loading…
Cancel
Save