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