Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/automated backups #2142

Merged
merged 25 commits into from
May 9, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
backup runs
MattDHill committed May 9, 2023
commit ad47f40a1ab7fb3c72bb321398d259319b0f2795
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ import {
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { WithId } from '../pages/backup-targets/backup-targets.page'

@Directive({
selector: '[backupCreate]',
@@ -32,13 +31,11 @@ export class BackupCreateDirective {
componentProps: { type: 'create' },
})

modal
.onDidDismiss<WithId<CifsBackupTarget | DiskBackupTarget>>()
.then(res => {
if (res.data) {
this.presentModalSelect(res.data.id)
}
})
modal.onDidDismiss<CifsBackupTarget | DiskBackupTarget>().then(res => {
if (res.data) {
this.presentModalSelect(res.data.id)
}
})

await modal.present()
}
Original file line number Diff line number Diff line change
@@ -9,16 +9,10 @@ import {
GenericInputComponent,
GenericInputOptions,
} from 'src/app/modals/generic-input/generic-input.component'
import {
BackupInfo,
BackupTarget,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { BackupInfo, BackupTarget } from 'src/app/services/api/api.types'
import { RecoverSelectPage } from 'src/app/pages/backups-routes/modals/recover-select/recover-select.page'
import * as argon2 from '@start9labs/argon2'
import { TargetSelectPage } from '../modals/target-select/target-select.page'
import { WithId } from '../pages/backup-targets/backup-targets.page'

@Directive({
selector: '[backupRestore]',
@@ -42,7 +36,7 @@ export class BackupRestoreDirective {
componentProps: { type: 'restore' },
})

modal.onDidDismiss<WithId<BackupTarget>>().then(res => {
modal.onDidDismiss<BackupTarget>().then(res => {
if (res.data) {
this.presentModalPassword(res.data)
}
@@ -51,7 +45,7 @@ export class BackupRestoreDirective {
await modal.present()
}

async presentModalPassword(target: WithId<BackupTarget>): Promise<void> {
async presentModalPassword(target: BackupTarget): Promise<void> {
const options: GenericInputOptions = {
title: 'Password Required',
message:
Original file line number Diff line number Diff line change
@@ -3,10 +3,7 @@ import { ModalController, NavController } from '@ionic/angular'
import { BehaviorSubject, Subject } from 'rxjs'
import { BackupTarget, DiskBackupTarget } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
BackupType,
WithId,
} from '../../pages/backup-targets/backup-targets.page'
import { BackupType } from '../../pages/backup-targets/backup-targets.page'

@Component({
selector: 'target-select',
@@ -19,8 +16,8 @@ export class TargetSelectPage {
@Input() isOneOff = true

targets: {
'unsaved-physical': WithId<DiskBackupTarget>[]
saved: WithId<BackupTarget>[]
'unsaved-physical': DiskBackupTarget[]
saved: BackupTarget[]
} = {
'unsaved-physical': [],
saved: [],
@@ -64,7 +61,7 @@ export class TargetSelectPage {
const targets = await this.api.getBackupTargets({})
this.targets = {
'unsaved-physical': [],
saved: Object.keys(targets).map(id => ({ id, ...targets[id] })),
saved: targets,
}
} catch (e: any) {
this.error$.next(e.message)
Original file line number Diff line number Diff line change
@@ -2,7 +2,11 @@ import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { BackupHistoryPage } from './backup-history.page'
import {
BackupHistoryPage,
DurationPipe,
HasErrorPipe,
} from './backup-history.page'

const routes: Routes = [
{
@@ -12,7 +16,7 @@ const routes: Routes = [
]

@NgModule({
declarations: [BackupHistoryPage],
declarations: [BackupHistoryPage, DurationPipe, HasErrorPipe],
imports: [CommonModule, IonicModule, RouterModule.forChild(routes)],
})
export class BackupHistoryPageModule {}
Original file line number Diff line number Diff line change
@@ -8,62 +8,52 @@
</ion-header>

<ion-content class="ion-padding-top">
<!-- <ion-item-group>
<ion-item-group>
<div class="grid-fixed">
<ion-grid class="ion-padding">
<ion-row class="grid-headings">
<ion-col size="3">Name</ion-col>
<ion-col>Type</ion-col>
<ion-col>Available</ion-col>
<ion-col size="4">Path</ion-col>
<ion-col size="2"></ion-col>
<ion-col size="2.5">Started At</ion-col>
<ion-col size="2">Duration</ion-col>
<ion-col size="1.5">Result</ion-col>
<ion-col size="2.5">Job</ion-col>
<ion-col size="2.5">Packages</ion-col>
<ion-col size="1"></ion-col>
</ion-row>

<ion-row
*ngFor="let run of runs$"
*ngFor="let run of runs$ | async; let i = index"
class="ion-align-items-center grid-row-border"
>
<ion-col size="3">{{ target.name }}</ion-col>
<ion-col class="inline">
<ion-icon
*ngIf="target.type === 'disk'"
name="save-outline"
size="small"
></ion-icon>
<ion-icon
*ngIf="target.type === 'cifs'"
name="folder-open-outline"
size="small"
></ion-icon>
<ion-icon
*ngIf="target.type === 'cloud'"
name="cloud-outline"
size="small"
></ion-icon>
&nbsp; {{ target.type | titlecase }}
<ion-col size="2.5"
>{{ run['started-at'] | date : 'medium' }}</ion-col
>
<ion-col size="2"
>{{ run['started-at']| duration : run['completed-at'] }}
Minutes</ion-col
>
<ion-col size="1.5">
<ion-text *ngIf="run.report | hasError; else noError" color="danger"
>Error</ion-text
>
<ng-template #noError>
<ion-text color="success">Success</ion-text>
</ng-template>
</ion-col>
<ion-col>
<ion-icon
[name]="target.mountable ? 'checkmark' : 'close'"
[color]="target.mountable ? 'success' : 'danger'"
></ion-icon>
<ion-col size="2.5">{{ run.job?.name || 'No job' }}</ion-col>
<ion-col size="2.5">
<a (click)="presentModalReport(run)" class="link">
{{ run['package-ids'].length }} Packages
</a>
</ion-col>
<ion-col size="4">{{ target.path }}</ion-col>
<ion-col size="2">
<ion-col size="1">
<ion-buttons style="float: right">
<ion-button size="small" (click)="presentModalUpdate(target)">
<ion-icon name="pencil"></ion-icon>
</ion-button>
<ion-button
size="small"
(click)="presentAlertDelete(target.id, i)"
>
<ion-button size="small" (click)="presentAlertDelete(run.id, i)">
<ion-icon name="trash"></ion-icon>
</ion-button>
</ion-buttons>
</ion-col>
</ion-row>
</ion-grid>
</div>
</ion-item-group> -->
</ion-item-group>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.link {
cursor: pointer;
color:cyan;
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
import { Component } from '@angular/core'
import {
BackupTarget,
DiskBackupTarget,
RR,
} from 'src/app/services/api/api.types'
import { Pipe, PipeTransform } from '@angular/core'
import { BackupReport, BackupRun } from 'src/app/services/api/api.types'
import {
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared'
import {
CifsSpec,
DropboxSpec,
GoogleDriveSpec,
RemoteBackupTargetSpec,
} from '../../types/target-types'
import { BehaviorSubject, Subject } from 'rxjs'
import { Subject } from 'rxjs'
import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page'

@Component({
selector: 'backup-history',
templateUrl: './backup-history.page.html',
styleUrls: ['./backup-history.page.scss'],
})
export class BackupHistoryPage {
readonly runs$ = new Subject<BackupRun[]>()

constructor(
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
@@ -35,13 +28,13 @@ export class BackupHistoryPage {
) {}

ngOnInit() {
this.getHistory()
this.getRuns()
}

async presentAlertDelete(id: string, index: number) {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: 'Forget backup target? This actions cannot be undone.',
message: 'Delete backup record? This actions cannot be undone.',
buttons: [
{
text: 'Cancel',
@@ -59,6 +52,17 @@ export class BackupHistoryPage {
await alert.present()
}

async presentModalReport(run: BackupRun) {
const modal = await this.modalCtrl.create({
component: BackupReportPage,
componentProps: {
report: run.report,
timestamp: run['completed-at'],
},
})
await modal.present()
}

async delete(id: string, index: number): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Removing...',
@@ -74,11 +78,33 @@ export class BackupHistoryPage {
}
}

private async getHistory(): Promise<void> {
private async getRuns(): Promise<void> {
try {
const runs = await this.api.getBackupTargets({})
const runs = await this.api.getBackupRuns({})
this.runs$.next(runs)
} catch (e: any) {
this.errToast.present(e)
}
}
}

@Pipe({
name: 'duration',
})
export class DurationPipe implements PipeTransform {
transform(start: string, finish: string): number {
const diffMs = new Date(finish).valueOf() - new Date(start).valueOf()
return diffMs / 100
}
}

@Pipe({
name: 'hasError',
})
export class HasErrorPipe implements PipeTransform {
transform(report: BackupReport): boolean {
const osErr = !!report.server.error
const pkgErr = !!Object.values(report.packages).find(pkg => pkg.error)
return osErr || pkgErr
}
}
Loading