mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user