diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index 2596d4647..3ea5d0ad3 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -14,6 +14,7 @@ using NodaTime; using Orleans; using Orleans.Concurrency; using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; @@ -38,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Backup private CancellationTokenSource currentTask; private BackupStateJob currentJob; private Guid appId; - private BackupState state; + private BackupState state = new BackupState(); private IPersistence persistence; public BackupGrain( @@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Backup await backupArchiveLocation.DeleteArchiveAsync(job.Id); } - public async Task StartNewAsync() + public async Task RunAsync() { if (currentTask != null) { @@ -180,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { await writer.WriteEventAsync(eventData); } - }, "AppId", appId, null, currentTask.Token); + }, SquidexHeaders.AppId, appId.ToString(), null, currentTask.Token); } stream.Position = 0; diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs index 3a2736204..21f66e2ff 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { public interface IBackupGrain : IGrainWithGuidKey { - Task StartNewAsync(); + Task RunAsync(); Task DeleteAsync(Guid id); diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs b/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs index 5729030d5..e75eef133 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs @@ -13,6 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Backup.State public sealed class BackupState { [JsonProperty] - public List Jobs { get; set; } = new List(); + public List Jobs { get; } = new List(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs index faa099b4b..b710a46ac 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var tempFile = GetTempFile(backupId); - return Task.FromResult(new FileStream(tempFile, FileMode.Open, FileAccess.ReadWrite)); + return Task.FromResult(new FileStream(tempFile, FileMode.Create, FileAccess.ReadWrite)); } public Task DeleteArchiveAsync(Guid backupId) diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs index 9808cedb4..b3fd6f7d8 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs @@ -26,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities { @event.SetAppId(appEvent.AppId.Id); } + + base.RaiseEvent(@event); } } } diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs deleted file mode 100644 index 315972adf..000000000 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// 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; } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index c90506746..19f641f38 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.States public async Task<(T Value, long Version)> ReadAsync(TKey key) { var existing = - await Collection.Find(x => Equals(x.Id, key)) + await Collection.Find(x => x.Id.Equals(key)) .FirstOrDefaultAsync(); if (existing != null) diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index a3d6b1bc6..3a010d3ed 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -32,12 +32,12 @@ namespace Squidex.Infrastructure.States public IPersistence WithSnapshots(Type owner, TKey key, Func applySnapshot) { - return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); + return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); } public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, Func applySnapshot, Func, Task> applyEvent) { - return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } public IPersistence WithEventSourcing(Type owner, TKey key, Func, Task> applyEvent) diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 3cee24593..1164807ad 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -21,12 +21,12 @@ namespace Squidex.Infrastructure.States public static IPersistence WithSnapshots(this IStore store, TKey key, Func applySnapshot) { - return store.WithSnapshots(typeof(TOwner), key, applySnapshot); + return store.WithSnapshots(typeof(TOwner), key, applySnapshot); } public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Func applySnapshot, Func, Task> applyEvent) { - return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot, applyEvent); + return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot, applyEvent); } public static IPersistence WithEventSourcing(this IStore store, Type owner, TKey key, Action> applyEvent) @@ -36,12 +36,12 @@ namespace Squidex.Infrastructure.States public static IPersistence WithSnapshots(this IStore store, Type owner, TKey key, Action applySnapshot) { - return store.WithSnapshots(owner, key, applySnapshot.ToAsync()); + return store.WithSnapshots(owner, key, applySnapshot.ToAsync()); } public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, Type owner, TKey key, Action applySnapshot, Action> applyEvent) { - return store.WithSnapshotsAndEventSourcing(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync()); + return store.WithSnapshotsAndEventSourcing(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync()); } public static IPersistence WithEventSourcing(this IStore store, TKey key, Action> applyEvent) @@ -51,12 +51,12 @@ namespace Squidex.Infrastructure.States public static IPersistence WithSnapshots(this IStore store, TKey key, Action applySnapshot) { - return store.WithSnapshots(typeof(TOwner), key, applySnapshot.ToAsync()); + return store.WithSnapshots(typeof(TOwner), key, applySnapshot.ToAsync()); } public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Action applySnapshot, Action> applyEvent) { - return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync()); + return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync()); } } } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index f0e4c4d03..9e1ec9fdd 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -27,18 +27,18 @@ namespace Squidex.Areas.Api.Controllers.Assets [SwaggerTag(nameof(Assets))] public sealed class AssetContentController : ApiController { - private readonly IAssetStore assetStorage; + private readonly IAssetStore assetStore; private readonly IAssetRepository assetRepository; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AssetContentController( ICommandBus commandBus, - IAssetStore assetStorage, + IAssetStore assetStore, IAssetRepository assetRepository, IAssetThumbnailGenerator assetThumbnailGenerator) : base(commandBus) { - this.assetStorage = assetStorage; + this.assetStore = assetStore; this.assetRepository = assetRepository; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// The target height of the asset, if it is an image. /// The resize mode when the width and height is defined. /// - /// 200 => Asset found and content or (resize) image returned. + /// 200 => Asset found and content or (resized) image returned. /// 404 => Asset or app not found. /// [HttpGet] @@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Assets try { - await assetStorage.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream); + await assetStore.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream); } catch (AssetNotFoundException) { @@ -87,13 +87,13 @@ namespace Squidex.Areas.Api.Controllers.Assets { using (var destinationStream = GetTempStream()) { - await assetStorage.DownloadAsync(assetId, entity.FileVersion, null, sourceStream); + await assetStore.DownloadAsync(assetId, entity.FileVersion, null, sourceStream); sourceStream.Position = 0; await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode); destinationStream.Position = 0; - await assetStorage.UploadAsync(assetId, entity.FileVersion, assetSuffix, destinationStream); + await assetStore.UploadAsync(assetId, entity.FileVersion, assetSuffix, destinationStream); destinationStream.Position = 0; await destinationStream.CopyToAsync(bodyStream); @@ -103,7 +103,7 @@ namespace Squidex.Areas.Api.Controllers.Assets } else { - await assetStorage.DownloadAsync(assetId, entity.FileVersion, null, bodyStream); + await assetStore.DownloadAsync(assetId, entity.FileVersion, null, bodyStream); } }); } diff --git a/src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs b/src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs index 4cb34a669..f4991ceec 100644 --- a/src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs +++ b/src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs @@ -14,8 +14,10 @@ using NSwag.Annotations; using Orleans; using Squidex.Areas.Api.Controllers.Backup.Models; using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Tasks; using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.Backup @@ -31,11 +33,13 @@ namespace Squidex.Areas.Api.Controllers.Backup public class BackupController : ApiController { private readonly IGrainFactory grainFactory; + private readonly IAssetStore assetStore; - public BackupController(ICommandBus commandBus, IGrainFactory grainFactory) + public BackupController(ICommandBus commandBus, IGrainFactory grainFactory, IAssetStore assetStore) : base(commandBus) { this.grainFactory = grainFactory; + this.assetStore = assetStore; } /// @@ -71,15 +75,33 @@ namespace Squidex.Areas.Api.Controllers.Backup [Route("apps/{app}/backups/")] [ProducesResponseType(typeof(List), 200)] [ApiCosts(0)] - public async Task PostBackup(string app) + public IActionResult PostBackup(string app) { var backupGrain = grainFactory.GetGrain(App.Id); - await backupGrain.StartNewAsync(); + backupGrain.RunAsync().Forget(); return NoContent(); } + /// + /// Get the backup content. + /// + /// The name of the app. + /// The id of the asset. + /// + /// 200 => Backup found and content returned. + /// 404 => Backup or app not found. + /// + [HttpGet] + [Route("apps/{app}/backups/{id}")] + [ProducesResponseType(200)] + [ApiCosts(0.5)] + public IActionResult GetBackupContent(string app, Guid id) + { + return new FileCallbackResult("application/zip", "Backup.zip", bodyStream => assetStore.DownloadAsync(id.ToString(), 0, null, bodyStream)); + } + /// /// Delete a backup. /// @@ -89,15 +111,15 @@ namespace Squidex.Areas.Api.Controllers.Backup /// 204 => Backup started. /// 404 => Backup or app not found. /// - [HttpPost] + [HttpDelete] [Route("apps/{app}/backups/{id}")] [ProducesResponseType(typeof(List), 200)] [ApiCosts(0)] - public async Task PostBackup(string app, Guid id) + public async Task DeleteBackup(string app, Guid id) { var backupGrain = grainFactory.GetGrain(App.Id); - await backupGrain.StartNewAsync(); + await backupGrain.DeleteAsync(id); return NoContent(); } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index cabc20e5c..158c3afdf 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Edm; @@ -50,6 +51,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 537b06688..f4f8e56ba 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Backup.State; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.History; @@ -69,6 +70,10 @@ namespace Squidex.Config.Domain .As() .As(); + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) + .As>() + .As(); + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) .As>() .As(); diff --git a/src/Squidex/app/features/settings/declarations.ts b/src/Squidex/app/features/settings/declarations.ts index f9932b668..66917745d 100644 --- a/src/Squidex/app/features/settings/declarations.ts +++ b/src/Squidex/app/features/settings/declarations.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +export * from './pages/backups/backups-page.component'; export * from './pages/clients/client.component'; export * from './pages/clients/clients-page.component'; export * from './pages/contributors/contributors-page.component'; diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index 519f86b39..40b5ccd3c 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -17,6 +17,7 @@ import { } from 'shared'; import { + BackupsPageComponent, ClientComponent, ClientsPageComponent, ContributorsPageComponent, @@ -45,6 +46,10 @@ const routes: Routes = [ path: 'more', component: MorePageComponent }, + { + path: 'backups', + component: BackupsPageComponent + }, { path: 'clients', component: ClientsPageComponent, @@ -137,6 +142,7 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ + BackupsPageComponent, ClientComponent, ClientsPageComponent, ContributorsPageComponent, diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html new file mode 100644 index 000000000..fdeb85a57 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -0,0 +1,34 @@ + + + +
+
+
+ +
+ +

Backups

+
+ + + + +
+ +
+
+ + + + + + + + + +
{{backup.started | sqxFullDateTime }}
+
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss b/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts new file mode 100644 index 000000000..851cacb5f --- /dev/null +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts @@ -0,0 +1,49 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { AppContext, BackupsService } from 'shared'; + +@Component({ + selector: 'sqx-backups-page', + styleUrls: ['./backups-page.component.scss'], + templateUrl: './backups-page.component.html', + providers: [ + AppContext + ] +}) +export class BackupsPageComponent { + public backups = + Observable.timer(0, 5000) + .switchMap(t => this.backupsService.getBackups(this.ctx.appName)); + + constructor(public readonly ctx: AppContext, + private readonly backupsService: BackupsService + ) { + } + + public startBackup() { + this.backupsService.postBackup(this.ctx.appName) + .subscribe(() => { + this.ctx.notifyInfo('Backup started.'); + }, error => { + this.ctx.notifyError(error); + }); + } + + public deleteBackup(id: string) { + this.backupsService.deleteBackup(this.ctx.appName, id) + .subscribe(() => { + this.ctx.notifyInfo('Backup deleting.'); + }, error => { + this.ctx.notifyError(error); + }); + } +} + diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/src/Squidex/app/features/settings/settings-area.component.html index 0f5a96935..7b3900ea5 100644 --- a/src/Squidex/app/features/settings/settings-area.component.html +++ b/src/Squidex/app/features/settings/settings-area.component.html @@ -14,6 +14,12 @@