mirror of https://github.com/Squidex/squidex.git
27 changed files with 435 additions and 309 deletions
@ -0,0 +1,62 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Backup.Helpers |
||||
|
{ |
||||
|
public static class Safe |
||||
|
{ |
||||
|
public static async Task DeleteAsync(IBackupArchiveLocation backupArchiveLocation, Guid id, ISemanticLog log) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await backupArchiveLocation.DeleteArchiveAsync(id); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "deleteArchive") |
||||
|
.WriteProperty("status", "failed") |
||||
|
.WriteProperty("operationId", id.ToString())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async Task DeleteAsync(IAssetStore assetStore, Guid id, ISemanticLog log) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await assetStore.DeleteAsync(id.ToString(), 0, null); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "deleteBackup") |
||||
|
.WriteProperty("status", "failed") |
||||
|
.WriteProperty("operationId", id.ToString())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async Task CleanupRestoreAsync(BackupHandler handler, Guid appId, Guid id, ISemanticLog log) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await handler.CleanupRestoreAsync(appId); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "cleanupRestore") |
||||
|
.WriteProperty("status", "failed") |
||||
|
.WriteProperty("operationId", id.ToString())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
<sqx-title message="Restore Backup"></sqx-title> |
||||
|
|
||||
|
<div class="container"> |
||||
|
<ng-container *ngIf="restoreJob; let job"> |
||||
|
<div class="card section"> |
||||
|
<div class="card-header"> |
||||
|
<div class="row no-gutters"> |
||||
|
<div class="col col-auto pr-2"> |
||||
|
<div *ngIf="job.status === 'Started'" class="restore-status restore-status-pending spin"> |
||||
|
<i class="icon-hour-glass"></i> |
||||
|
</div> |
||||
|
<div *ngIf="job.status === 'Failed'" class="restore-status restore-status-failed"> |
||||
|
<i class="icon-exclamation"></i> |
||||
|
</div> |
||||
|
<div *ngIf="job.status === 'Completed'" class="restore-status restore-status-success"> |
||||
|
<i class="icon-checkmark"></i> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col"> |
||||
|
<h3>Last Restore Operation</h3> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<div *ngFor="let row of job.log"> |
||||
|
{{row}} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-footer"> |
||||
|
<div class="row"> |
||||
|
<div class="col"> |
||||
|
Started: {{job.started | sqxISODate}} |
||||
|
</div> |
||||
|
<div class="col text-right" *ngIf="job.stopped"> |
||||
|
Stopped: {{job.stopped | sqxISODate}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<div class="table-items-row"> |
||||
|
<form [formGroup]="restoreForm.form" (submit)="restore()"> |
||||
|
<div class="row no-gutters"> |
||||
|
<div class="col"> |
||||
|
<input class="form-control" name="url" formControlName="url" placeholder="Url to backup" /> |
||||
|
</div> |
||||
|
<div class="col col-auto pl-1"> |
||||
|
<button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async">Restore Backup</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,59 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
$circle-size: 2rem; |
||||
|
|
||||
|
h3 { |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.section { |
||||
|
margin-bottom: .8rem; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
padding-top: 2rem; |
||||
|
} |
||||
|
|
||||
|
.card { |
||||
|
&-header { |
||||
|
h3 { |
||||
|
line-height: $circle-size; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-body { |
||||
|
font-family: monospace; |
||||
|
background: $color-border; |
||||
|
max-height: 400px; |
||||
|
min-height: 300px; |
||||
|
overflow-y: scroll; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.restore { |
||||
|
&-status { |
||||
|
& { |
||||
|
@include circle($circle-size); |
||||
|
line-height: $circle-size + .1rem; |
||||
|
text-align: center; |
||||
|
font-size: .6 * $circle-size; |
||||
|
font-weight: normal; |
||||
|
background: $color-border; |
||||
|
color: $color-dark-foreground; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
&-pending { |
||||
|
color: inherit; |
||||
|
} |
||||
|
|
||||
|
&-failed { |
||||
|
background: $color-theme-error; |
||||
|
} |
||||
|
|
||||
|
&-success { |
||||
|
background: $color-theme-green; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { FormBuilder } from '@angular/forms'; |
||||
|
import { Subscription, timer } from 'rxjs'; |
||||
|
import { switchMap } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
AuthService, |
||||
|
BackupsService, |
||||
|
DialogService, |
||||
|
RestoreDto, |
||||
|
RestoreForm |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-restore-page', |
||||
|
styleUrls: ['./restore-page.component.scss'], |
||||
|
templateUrl: './restore-page.component.html' |
||||
|
}) |
||||
|
export class RestorePageComponent implements OnDestroy, OnInit { |
||||
|
private timerSubscription: Subscription; |
||||
|
|
||||
|
public restoreJob: RestoreDto | null; |
||||
|
public restoreForm = new RestoreForm(this.formBuilder); |
||||
|
|
||||
|
constructor( |
||||
|
public readonly authState: AuthService, |
||||
|
private readonly backupsService: BackupsService, |
||||
|
private readonly dialogs: DialogService, |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.timerSubscription.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.timerSubscription = |
||||
|
timer(0, 1000).pipe(switchMap(() => this.backupsService.getRestore())) |
||||
|
.subscribe(dto => { |
||||
|
this.restoreJob = dto; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public restore() { |
||||
|
const value = this.restoreForm.submit(); |
||||
|
|
||||
|
if (value) { |
||||
|
this.restoreForm.submitCompleted({}); |
||||
|
|
||||
|
this.backupsService.postRestore(value.url) |
||||
|
.subscribe(() => { |
||||
|
this.dialogs.notifyInfo('Restore started, it can take several minutes to complete.'); |
||||
|
}, error => { |
||||
|
this.dialogs.notifyError(error); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue