Browse Source

Download Endpoint and UI page

pull/261/head
Sebastian Stehle 8 years ago
parent
commit
ab4bc05cd9
  1. 7
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs
  4. 2
      src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs
  5. 2
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
  6. 18
      src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs
  7. 2
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  8. 4
      src/Squidex.Infrastructure/States/Store.cs
  9. 12
      src/Squidex.Infrastructure/States/StoreExtensions.cs
  10. 16
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  11. 34
      src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs
  12. 4
      src/Squidex/Config/Domain/EntitiesServices.cs
  13. 5
      src/Squidex/Config/Domain/StoreServices.cs
  14. 1
      src/Squidex/app/features/settings/declarations.ts
  15. 6
      src/Squidex/app/features/settings/module.ts
  16. 34
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  17. 2
      src/Squidex/app/features/settings/pages/backups/backups-page.component.scss
  18. 49
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  19. 8
      src/Squidex/app/features/settings/settings-area.component.html
  20. 1
      src/Squidex/app/shared/declarations-base.ts
  21. 2
      src/Squidex/app/shared/module.ts
  22. 76
      src/Squidex/app/shared/services/backups.service.ts

7
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<BackupState> 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;

2
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);

2
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<BackupStateJob> Jobs { get; set; } = new List<BackupStateJob>();
public List<BackupStateJob> Jobs { get; } = new List<BackupStateJob>();
}
}

2
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<Stream>(new FileStream(tempFile, FileMode.Open, FileAccess.ReadWrite));
return Task.FromResult<Stream>(new FileStream(tempFile, FileMode.Create, FileAccess.ReadWrite));
}
public Task DeleteArchiveAsync(Guid backupId)

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

@ -26,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities
{
@event.SetAppId(appEvent.AppId.Id);
}
base.RaiseEvent(@event);
}
}
}

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

@ -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; }
}
}

2
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)

4
src/Squidex.Infrastructure/States/Store.cs

@ -32,12 +32,12 @@ namespace Squidex.Infrastructure.States
public IPersistence<TState> WithSnapshots<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot)
{
return CreatePersistence<TState>(owner, key, PersistenceMode.Snapshots, applySnapshot, null);
return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null);
}
public IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
return CreatePersistence<TState>(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
}
public IPersistence WithEventSourcing(Type owner, TKey key, Func<Envelope<IEvent>, Task> applyEvent)

12
src/Squidex.Infrastructure/States/StoreExtensions.cs

@ -21,12 +21,12 @@ namespace Squidex.Infrastructure.States
public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot)
{
return store.WithSnapshots<TState>(typeof(TOwner), key, applySnapshot);
return store.WithSnapshots(typeof(TOwner), key, applySnapshot);
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
return store.WithSnapshotsAndEventSourcing<TState>(typeof(TOwner), key, applySnapshot, applyEvent);
return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot, applyEvent);
}
public static IPersistence WithEventSourcing<TKey>(this IStore<TKey> store, Type owner, TKey key, Action<Envelope<IEvent>> applyEvent)
@ -36,12 +36,12 @@ namespace Squidex.Infrastructure.States
public static IPersistence<TState> WithSnapshots<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot)
{
return store.WithSnapshots<TState>(owner, key, applySnapshot.ToAsync());
return store.WithSnapshots(owner, key, applySnapshot.ToAsync());
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{
return store.WithSnapshotsAndEventSourcing<TState>(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync());
return store.WithSnapshotsAndEventSourcing(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync());
}
public static IPersistence WithEventSourcing<TOwner, TKey>(this IStore<TKey> store, TKey key, Action<Envelope<IEvent>> applyEvent)
@ -51,12 +51,12 @@ namespace Squidex.Infrastructure.States
public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot)
{
return store.WithSnapshots<TState>(typeof(TOwner), key, applySnapshot.ToAsync());
return store.WithSnapshots(typeof(TOwner), key, applySnapshot.ToAsync());
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{
return store.WithSnapshotsAndEventSourcing<TState>(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync());
return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync());
}
}
}

16
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
/// <param name="height">The target height of the asset, if it is an image.</param>
/// <param name="mode">The resize mode when the width and height is defined.</param>
/// <returns>
/// 200 => Asset found and content or (resize) image returned.
/// 200 => Asset found and content or (resized) image returned.
/// 404 => Asset or app not found.
/// </returns>
[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);
}
});
}

34
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;
}
/// <summary>
@ -71,15 +75,33 @@ namespace Squidex.Areas.Api.Controllers.Backup
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app)
public IActionResult PostBackup(string app)
{
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
await backupGrain.StartNewAsync();
backupGrain.RunAsync().Forget();
return NoContent();
}
/// <summary>
/// Get the backup content.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <returns>
/// 200 => Backup found and content returned.
/// 404 => Backup or app not found.
/// </returns>
[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));
}
/// <summary>
/// Delete a backup.
/// </summary>
@ -89,15 +111,15 @@ namespace Squidex.Areas.Api.Controllers.Backup
/// 204 => Backup started.
/// 404 => Backup or app not found.
/// </returns>
[HttpPost]
[HttpDelete]
[Route("apps/{app}/backups/{id}")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app, Guid id)
public async Task<IActionResult> DeleteBackup(string app, Guid id)
{
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
await backupGrain.StartNewAsync();
await backupGrain.DeleteAsync(id);
return NoContent();
}

4
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<CachingGraphQLService>()
.As<IGraphQLService>();
services.AddSingletonAs<TempFolderBackupArchiveLocation>()
.As<IBackupArchiveLocation>();
services.AddSingletonAs<AppProvider>()
.As<IAppProvider>();

5
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<IMigrationStatus>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<BackupState, Guid>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<BackupState, Guid>>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState, string>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<EventConsumerState, string>>()
.As<IInitializable>();

1
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';

6
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,

34
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -0,0 +1,34 @@
<sqx-title message="{app} | Backups | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-success" (click)="startBackup()">
Start Backup
</button>
</div>
<h3 class="panel-title">Backups</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content">
<table class="table table-items table-fixed">
<tbody>
<ng-template ngFor let-backup [ngForOf]="backups | async">
<tr>
<td>{{backup.started | sqxFullDateTime }}</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
</sqx-panel>

2
src/Squidex/app/features/settings/pages/backups/backups-page.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

49
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);
});
}
}

8
src/Squidex/app/features/settings/settings-area.component.html

@ -14,6 +14,12 @@
<div class="panel-main">
<div class="panel-content">
<ul class="nav nav-panel nav-dark flex-column">
<li class="nav-item" *ngIf="ctx.app.permission === 'Owner'">
<a class="nav-link" routerLink="backups" routerLinkActive="active">
Backups
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
Clients
@ -40,7 +46,7 @@
</li>
<li class="nav-item" *ngIf="ctx.app.permission === 'Owner'">
<a class="nav-link" routerLink="plans" routerLinkActive="active">
Update Plan
Subscription
<i class="icon-angle-right"></i>
</a>
</li>

1
src/Squidex/app/shared/declarations-base.ts

@ -25,6 +25,7 @@ export * from './services/apps-store.service';
export * from './services/apps.service';
export * from './services/assets.service';
export * from './services/auth.service';
export * from './services/backups.service';
export * from './services/contents.service';
export * from './services/event-consumers.service';
export * from './services/graphql.service';

2
src/Squidex/app/shared/module.ts

@ -26,6 +26,7 @@ import {
AssetUrlPipe,
AuthInterceptor,
AuthService,
BackupsService,
ContentsService,
EventConsumersService,
FileIconPipe,
@ -127,6 +128,7 @@ export class SqxSharedModule {
AppsStoreService,
AssetsService,
AuthService,
BackupsService,
ContentsService,
EventConsumersService,
GraphQlService,

76
src/Squidex/app/shared/services/backups.service.ts

@ -0,0 +1,76 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import {
AnalyticsService,
ApiUrlConfig,
DateTime
} from 'framework';
export class BackupDto {
constructor(
public readonly id: string,
public readonly isFailed: string,
public readonly started: DateTime,
public readonly stopped: DateTime | null
) {
}
}
@Injectable()
export class BackupsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig,
private readonly analytics: AnalyticsService
) {
}
public getBackups(appName: string): Observable<BackupDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.get(url)
.map(response => {
const items: any[] = <any>response;
return items.map(item => {
return new BackupDto(
item.id,
item.isFailed,
DateTime.parseISO_UTC(item.stopped),
item.stopped ? DateTime.parseISO_UTC(item.stopped) : null);
});
})
.pretifyError('Failed to load backups.');
}
public postBackup(appName: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.post(url, {})
.do(() => {
this.analytics.trackEvent('Backup', 'Started', appName);
})
.pretifyError('Failed to start backup.');
}
public deleteBackup(appName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups/${id}`);
return this.http.delete(url)
.do(() => {
this.analytics.trackEvent('Backup', 'Deleted', appName);
})
.pretifyError('Failed to delete backup.');
}
}
Loading…
Cancel
Save