Feature/backup fs (#2665)

* port 040 config, WIP

* update fixtures

* use taiga modal for backups too

* fix: update Taiga UI and refactor everything to work

* chore: package-lock

* fix interfaces and mocks for interfaces

* better mocks

* function to transform old spec to new

* delete unused fns

* delete unused FE config utils

* fix exports from sdk

* reorganize exports

* functions to translate config

* rename unionSelectKey and unionValueKey

* new backup fs

* update sdk types

* change types, include fuse module

* fix casing

* rework setup wiz

* rework UI

* only fuse3

* fix arm build

* misc fixes

* fix duplicate server select

* fix: fix throwing inside dialog

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2024-07-11 11:32:46 -06:00
committed by GitHub
parent f2a02b392e
commit 87322744d4
67 changed files with 880 additions and 563 deletions

View File

@@ -18,8 +18,8 @@
<ion-label>
<h2>{{ option.title }}</h2>
<p>Version {{ option.version }}</p>
<p>Backup made: {{ option.timestamp | date : 'medium' }}</p>
<p *ngIf="!option.installed && !option['newer-eos']">
<p>Created: {{ option.timestamp | date : 'medium' }}</p>
<p *ngIf="!option.installed && !option.newerOS">
<ion-text color="success">Ready to restore</ion-text>
</p>
<p *ngIf="option.installed">
@@ -27,7 +27,7 @@
Unavailable. {{ option.title }} is already installed.
</ion-text>
</p>
<p *ngIf="option['newer-eos']">
<p *ngIf="option.newerOS">
<ion-text color="danger">
Unavailable. Backup was made on a newer version of StartOS.
</ion-text>
@@ -36,7 +36,7 @@
<ion-checkbox
slot="end"
[(ngModel)]="option.checked"
[disabled]="option.installed || option['newer-eos']"
[disabled]="option.installed || option.newerOS"
(ionChange)="handleChange(options)"
></ion-checkbox>
</ion-item>

View File

@@ -14,10 +14,10 @@ import { AppRecoverOption } from './to-options.pipe'
styleUrls: ['./app-recover-select.page.scss'],
})
export class AppRecoverSelectPage {
@Input() id!: string
@Input() targetId!: string
@Input() serverId!: string
@Input() backupInfo!: BackupInfo
@Input() password!: string
@Input() oldPassword?: string
readonly packageData$ = this.patch.watch$('packageData').pipe(take(1))
@@ -46,8 +46,8 @@ export class AppRecoverSelectPage {
try {
await this.embassyApi.restorePackages({
ids,
targetId: this.id,
oldPassword: this.oldPassword || null,
targetId: this.targetId,
serverId: this.serverId,
password: this.password,
})
this.modalCtrl.dismiss(undefined, 'success')

View File

@@ -10,7 +10,7 @@ export interface AppRecoverOption extends PackageBackupInfo {
id: string
checked: boolean
installed: boolean
'newer-eos': boolean
newerOS: boolean
}
@Pipe({
@@ -34,7 +34,7 @@ export class ToOptionsPipe implements PipeTransform {
id,
installed: !!packageData[id],
checked: false,
'newer-eos': this.compare(packageBackups[id].osVersion),
newerOS: this.compare(packageBackups[id].osVersion),
}))
.sort((a, b) =>
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { BackupServerSelectModal } from './backup-server-select.page'
import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module'
@NgModule({
declarations: [BackupServerSelectModal],
imports: [CommonModule, FormsModule, IonicModule, AppRecoverSelectPageModule],
exports: [BackupServerSelectModal],
})
export class BackupServerSelectModule {}

View File

@@ -0,0 +1,35 @@
<ion-header>
<ion-toolbar>
<ion-title>Select Server Backup</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item-group>
<ion-item
*ngFor="let server of target.entry.startOs | keyvalue"
button
(click)="presentModalPassword(server.key, server.value)"
>
<ion-label>
<h2>
<b>Local Hostname</b>
: {{ server.value.hostname }}.local
</h2>
<h2>
<b>StartOS Version</b>
: {{ server.value.version }}
</h2>
<h2>
<b>Created</b>
: {{ server.value.timestamp | date : 'medium' }}
</h2>
</ion-label>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,103 @@
import { Component, Input } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import * as argon2 from '@start9labs/argon2'
import {
ErrorService,
LoadingService,
StartOSDiskInfo,
} from '@start9labs/shared'
import {
BackupInfo,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { AppRecoverSelectPage } from '../app-recover-select/app-recover-select.page'
import { PasswordPromptModal } from './password-prompt.modal'
@Component({
selector: 'backup-server-select',
templateUrl: 'backup-server-select.page.html',
styleUrls: ['backup-server-select.page.scss'],
})
export class BackupServerSelectModal {
@Input() target!: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>
constructor(
private readonly modalCtrl: ModalController,
private readonly loader: LoadingService,
private readonly api: ApiService,
private readonly navCtrl: NavController,
private readonly errorService: ErrorService,
) {}
dismiss() {
this.modalCtrl.dismiss()
}
async presentModalPassword(
serverId: string,
server: StartOSDiskInfo,
): Promise<void> {
const modal = await this.modalCtrl.create({
component: PasswordPromptModal,
})
modal.present()
const { data, role } = await modal.onWillDismiss()
if (role === 'confirm') {
try {
argon2.verify(server.passwordHash!, data)
await this.restoreFromBackup(serverId, data)
} catch (e: any) {
this.errorService.handleError(e)
}
}
}
private async restoreFromBackup(
serverId: string,
password: string,
): Promise<void> {
const loader = this.loader.open('Decrypting drive...').subscribe()
try {
const backupInfo = await this.api.getBackupInfo({
targetId: this.target.id,
serverId,
password,
})
this.presentModalSelect(serverId, backupInfo, password)
} finally {
loader.unsubscribe()
}
}
private async presentModalSelect(
serverId: string,
backupInfo: BackupInfo,
password: string,
): Promise<void> {
const modal = await this.modalCtrl.create({
componentProps: {
targetId: this.target.id,
serverId,
backupInfo,
password,
},
presentingElement: await this.modalCtrl.getTop(),
component: AppRecoverSelectPage,
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
this.modalCtrl.dismiss(undefined, 'success')
this.navCtrl.navigateRoot('/services')
}
})
await modal.present()
}
}

View File

@@ -0,0 +1,69 @@
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { IonicModule, ModalController } from '@ionic/angular'
import { TuiInputPasswordModule } from '@taiga-ui/kit'
@Component({
standalone: true,
template: `
<ion-header>
<ion-toolbar>
<ion-title>Decrypt Backup</ion-title>
<ion-buttons slot="end">
<ion-button (click)="cancel()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>
Enter the password that was used to encrypt this backup. On the next
screen, you will select the individual services you want to restore.
</p>
<p>
<tui-input-password [(ngModel)]="password">
Enter password
</tui-input-password>
</p>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-button
class="ion-padding-end"
slot="end"
color="dark"
(click)="cancel()"
>
Cancel
</ion-button>
<ion-button
class="ion-padding-end"
slot="end"
color="primary"
strong="true"
[disabled]="!password"
(click)="confirm()"
>
Next
</ion-button>
</ion-toolbar>
</ion-footer>
`,
imports: [IonicModule, FormsModule, TuiInputPasswordModule],
})
export class PasswordPromptModal {
password = ''
constructor(private modalCtrl: ModalController) {}
cancel() {
return this.modalCtrl.dismiss(null, 'cancel')
}
confirm() {
return this.modalCtrl.dismiss(this.password, 'confirm')
}
}