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:
@@ -3,7 +3,7 @@
|
||||
<ion-icon *ngFor="let icon of icons" [name]="icon"></ion-icon>
|
||||
|
||||
<!-- 3rd party components -->
|
||||
<qr-code value="hello"></qr-code>
|
||||
<qr-code control="hello"></qr-code>
|
||||
|
||||
<!-- Ionic components -->
|
||||
<ion-accordion></ion-accordion>
|
||||
@@ -58,21 +58,21 @@
|
||||
<img *ngFor="let icon of taiga" src="assets/taiga-ui/icons/{{ icon }}.svg" />
|
||||
|
||||
<!-- Taiga UI components -->
|
||||
<tui-input></tui-input>
|
||||
<tui-input-time></tui-input-time>
|
||||
<tui-input-date></tui-input-date>
|
||||
<tui-input-date-time></tui-input-date-time>
|
||||
<tui-input-files></tui-input-files>
|
||||
<tui-input-number></tui-input-number>
|
||||
<tui-text-area></tui-text-area>
|
||||
<tui-select></tui-select>
|
||||
<tui-multi-select></tui-multi-select>
|
||||
<tui-input [formControl]="control"></tui-input>
|
||||
<tui-input-time [formControl]="control"></tui-input-time>
|
||||
<tui-input-date [formControl]="control"></tui-input-date>
|
||||
<tui-input-date-time [formControl]="control"></tui-input-date-time>
|
||||
<tui-input-files [formControl]="control"></tui-input-files>
|
||||
<tui-input-number [formControl]="control"></tui-input-number>
|
||||
<tui-textarea [formControl]="control"></tui-textarea>
|
||||
<tui-select [formControl]="control"></tui-select>
|
||||
<tui-multi-select [formControl]="control"></tui-multi-select>
|
||||
<tui-toggle [formControl]="control"></tui-toggle>
|
||||
<tui-radio-list [formControl]="control"></tui-radio-list>
|
||||
<tui-tooltip></tui-tooltip>
|
||||
<tui-toggle></tui-toggle>
|
||||
<tui-radio-list></tui-radio-list>
|
||||
<tui-error></tui-error>
|
||||
<tui-svg></tui-svg>
|
||||
<tui-icon></tui-icon>
|
||||
<tui-svg src="tuiIconTrash"></tui-svg>
|
||||
<tui-icon icon="tuiIconTrash"></tui-icon>
|
||||
<tui-expand></tui-expand>
|
||||
<tui-elastic-container></tui-elastic-container>
|
||||
<tui-scrollbar></tui-scrollbar>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
import {
|
||||
ActionSheetController,
|
||||
AlertController,
|
||||
@@ -122,6 +123,7 @@ const TAIGA = [
|
||||
export class PreloaderComponent {
|
||||
readonly icons = ICONS
|
||||
readonly taiga = TAIGA
|
||||
readonly control = new FormControl()
|
||||
|
||||
constructor(
|
||||
_modals: ModalController,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
TuiErrorModule,
|
||||
@@ -26,7 +27,7 @@ import {
|
||||
TuiProgressModule,
|
||||
TuiRadioListModule,
|
||||
TuiSelectModule,
|
||||
TuiTextAreaModule,
|
||||
TuiTextareaModule,
|
||||
TuiToggleModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
@@ -35,6 +36,7 @@ import { PreloaderComponent } from './preloader.component'
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
IonicModule,
|
||||
QrCodeModule,
|
||||
TuiTooltipModule,
|
||||
@@ -52,7 +54,7 @@ import { PreloaderComponent } from './preloader.component'
|
||||
TuiInputNumberModule,
|
||||
TuiExpandModule,
|
||||
TuiSelectModule,
|
||||
TuiTextAreaModule,
|
||||
TuiTextareaModule,
|
||||
TuiToggleModule,
|
||||
TuiElasticContainerModule,
|
||||
TuiCellModule,
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<div class="inline">
|
||||
<h2 *ngIf="type === 'create'; else restore">
|
||||
<ion-icon name="cloud-outline" color="success"></ion-icon>
|
||||
{{
|
||||
hasValidBackup
|
||||
? 'Available, contains existing backup'
|
||||
: 'Available for fresh backup'
|
||||
}}
|
||||
Available for backup
|
||||
</h2>
|
||||
<ng-template #restore>
|
||||
<h2 *ngIf="hasValidBackup">
|
||||
<h2 *ngIf="hasAnyBackup">
|
||||
<ion-icon name="cloud-done-outline" color="success"></ion-icon>
|
||||
StartOS backup detected
|
||||
StartOS backups detected
|
||||
</h2>
|
||||
<h2 *ngIf="!hasValidBackup">
|
||||
<h2 *ngIf="!hasAnyBackup">
|
||||
<ion-icon name="cloud-offline-outline" color="danger"></ion-icon>
|
||||
No StartOS backup
|
||||
No StartOS backups
|
||||
</h2>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<ng-container *ngIf="cifs.mountable">
|
||||
<backup-drives-status
|
||||
[type]="type"
|
||||
[hasValidBackup]="target.hasValidBackup"
|
||||
[hasAnyBackup]="target.hasAnyBackup"
|
||||
></backup-drives-status>
|
||||
</ng-container>
|
||||
<h2 *ngIf="!cifs.mountable" class="inline">
|
||||
@@ -155,7 +155,7 @@
|
||||
<h1>{{ drive.label || drive.logicalname }}</h1>
|
||||
<backup-drives-status
|
||||
[type]="type"
|
||||
[hasValidBackup]="target.hasValidBackup"
|
||||
[hasAnyBackup]="target.hasAnyBackup"
|
||||
></backup-drives-status>
|
||||
<p>
|
||||
{{ drive.vendor || 'Unknown Vendor' }} -
|
||||
|
||||
@@ -72,10 +72,10 @@ export class BackupDrivesComponent {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.type === 'restore' && !target.hasValidBackup) {
|
||||
if (this.type === 'restore' && !target.hasAnyBackup) {
|
||||
const message = `${
|
||||
target.entry.type === 'cifs' ? 'Network Folder' : 'Drive partition'
|
||||
} does not contain a valid Start9 Server backup.`
|
||||
} does not contain a valid backup.`
|
||||
this.presentAlertError(message)
|
||||
return
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export class BackupDrivesComponent {
|
||||
const [id, entry] = Object.entries(res)[0]
|
||||
this.backupService.cifs.unshift({
|
||||
id,
|
||||
hasValidBackup: this.backupService.hasValidBackup(entry),
|
||||
hasAnyBackup: this.backupService.hasAnyBackup(entry),
|
||||
entry,
|
||||
})
|
||||
return true
|
||||
@@ -258,7 +258,7 @@ export class BackupDrivesHeaderComponent {
|
||||
})
|
||||
export class BackupDrivesStatusComponent {
|
||||
@Input() type!: BackupType
|
||||
@Input() hasValidBackup!: boolean
|
||||
@Input() hasAnyBackup!: boolean
|
||||
}
|
||||
|
||||
const cifsSpec = CB.Config.of({
|
||||
|
||||
@@ -34,7 +34,7 @@ export class BackupService {
|
||||
.map(([id, cifs]) => {
|
||||
return {
|
||||
id,
|
||||
hasValidBackup: this.hasValidBackup(cifs),
|
||||
hasAnyBackup: this.hasAnyBackup(cifs),
|
||||
entry: cifs as CifsBackupTarget,
|
||||
}
|
||||
})
|
||||
@@ -44,7 +44,7 @@ export class BackupService {
|
||||
.map(([id, drive]) => {
|
||||
return {
|
||||
id,
|
||||
hasValidBackup: this.hasValidBackup(drive),
|
||||
hasAnyBackup: this.hasAnyBackup(drive),
|
||||
entry: drive as DiskBackupTarget,
|
||||
}
|
||||
})
|
||||
@@ -55,8 +55,16 @@ export class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
hasValidBackup(target: BackupTarget): boolean {
|
||||
const backup = target.startOs
|
||||
return !!backup && this.emver.compare(backup.version, '0.3.0') !== -1
|
||||
hasAnyBackup(target: BackupTarget): boolean {
|
||||
return Object.values(target.startOs).some(
|
||||
s => this.emver.compare(s.version, '0.3.6') !== -1,
|
||||
)
|
||||
}
|
||||
|
||||
async hasThisBackup(target: BackupTarget, id: string): Promise<boolean> {
|
||||
return (
|
||||
target.startOs[id] &&
|
||||
this.emver.compare(target.startOs[id].version, '0.3.6') !== -1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container
|
||||
*ngFor="let entry of spec | keyvalue: asIsOrder"
|
||||
*ngFor="let entry of spec | keyvalue : asIsOrder"
|
||||
tuiMode="onDark"
|
||||
[ngSwitch]="entry.value.type"
|
||||
[tuiTextfieldCleaner]="true"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tui-text-area
|
||||
<tui-textarea
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
@@ -12,4 +12,4 @@
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
<textarea tuiTextfield [placeholder]="spec.placeholder || ''"></textarea>
|
||||
</tui-text-area>
|
||||
</tui-textarea>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
TuiPromptModule,
|
||||
TuiSelectModule,
|
||||
TuiTagModule,
|
||||
TuiTextAreaModule,
|
||||
TuiTextareaModule,
|
||||
TuiToggleModule,
|
||||
} from '@taiga-ui/kit'
|
||||
|
||||
@@ -60,7 +60,7 @@ import { HintPipe } from './hint.pipe'
|
||||
TuiInputModule,
|
||||
TuiInputNumberModule,
|
||||
TuiInputFilesModule,
|
||||
TuiTextAreaModule,
|
||||
TuiTextareaModule,
|
||||
TuiSelectModule,
|
||||
TuiMultiSelectModule,
|
||||
TuiToggleModule,
|
||||
|
||||
@@ -39,7 +39,7 @@ var convert = new Convert({
|
||||
selector: 'logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
providers: [TuiDestroyService, DownloadHTMLService],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class LogsComponent {
|
||||
@ViewChild(IonContent)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<backup-drives
|
||||
type="restore"
|
||||
class="ion-page"
|
||||
(onSelect)="presentModalPassword($event)"
|
||||
(onSelect)="presentModalSelectServer($event)"
|
||||
></backup-drives>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { RestorePage } from './restore.component'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module'
|
||||
import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module'
|
||||
import { BackupServerSelectModule } from 'src/app/modals/backup-server-select/backup-server-select.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -21,7 +21,7 @@ const routes: Routes = [
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
BackupDrivesComponentModule,
|
||||
AppRecoverSelectPageModule,
|
||||
BackupServerSelectModule,
|
||||
],
|
||||
declarations: [RestorePage],
|
||||
})
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ModalController, NavController } from '@ionic/angular'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { PROMPT, PromptOptions } from 'src/app/modals/prompt.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||
import {
|
||||
BackupInfo,
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { AppRecoverSelectPage } from 'src/app/modals/app-recover-select/app-recover-select.page'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { BackupServerSelectModal } from 'src/app/modals/backup-server-select/backup-server-select.page'
|
||||
|
||||
@Component({
|
||||
selector: 'restore',
|
||||
@@ -20,78 +13,15 @@ import * as argon2 from '@start9labs/argon2'
|
||||
styleUrls: ['./restore.component.scss'],
|
||||
})
|
||||
export class RestorePage {
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly loader: LoadingService,
|
||||
) {}
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
|
||||
async presentModalPassword(
|
||||
async presentModalSelectServer(
|
||||
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
|
||||
): Promise<void> {
|
||||
const options: PromptOptions = {
|
||||
message:
|
||||
'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.',
|
||||
label: 'Master Password',
|
||||
placeholder: 'Enter master password',
|
||||
useMask: true,
|
||||
buttonText: 'Next',
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
.open<string>(PROMPT, {
|
||||
label: 'Password Required',
|
||||
data: options,
|
||||
})
|
||||
.pipe(take(1))
|
||||
.subscribe(async (password: string) => {
|
||||
const passwordHash = target.entry.startOs?.passwordHash || ''
|
||||
argon2.verify(passwordHash, password)
|
||||
await this.restoreFromBackup(target, password)
|
||||
})
|
||||
}
|
||||
|
||||
private async restoreFromBackup(
|
||||
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
|
||||
password: string,
|
||||
oldPassword?: string,
|
||||
): Promise<void> {
|
||||
const loader = this.loader.open('Decrypting drive...').subscribe()
|
||||
|
||||
try {
|
||||
const backupInfo = await this.embassyApi.getBackupInfo({
|
||||
targetId: target.id,
|
||||
password,
|
||||
})
|
||||
this.presentModalSelect(target.id, backupInfo, password, oldPassword)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalSelect(
|
||||
id: string,
|
||||
backupInfo: BackupInfo,
|
||||
password: string,
|
||||
oldPassword?: string,
|
||||
): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
id,
|
||||
backupInfo,
|
||||
password,
|
||||
oldPassword,
|
||||
},
|
||||
componentProps: { target },
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: AppRecoverSelectPage,
|
||||
})
|
||||
|
||||
modal.onWillDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
}
|
||||
component: BackupServerSelectModal,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
|
||||
@@ -17,6 +17,7 @@ import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.pag
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { BackupService } from 'src/app/components/backup-drives/backup.service'
|
||||
|
||||
@Component({
|
||||
selector: 'server-backup',
|
||||
@@ -38,6 +39,7 @@ export class ServerBackupPage {
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
private readonly eosService: EOSService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly backupService: BackupService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -86,19 +88,18 @@ export class ServerBackupPage {
|
||||
})
|
||||
.pipe(take(1))
|
||||
.subscribe(async (password: string) => {
|
||||
const { passwordHash, id } = await getServerInfo(this.patch)
|
||||
|
||||
// confirm password matches current master password
|
||||
const { passwordHash } = await getServerInfo(this.patch)
|
||||
argon2.verify(passwordHash, password)
|
||||
|
||||
// first time backup
|
||||
if (!target.hasValidBackup) {
|
||||
if (!this.backupService.hasThisBackup(target.entry, id)) {
|
||||
await this.createBackup(target, password)
|
||||
// existing backup
|
||||
} else {
|
||||
try {
|
||||
const passwordHash = target.entry.startOs?.passwordHash || ''
|
||||
|
||||
argon2.verify(passwordHash, password)
|
||||
argon2.verify(target.entry.startOs[id].passwordHash!, password)
|
||||
} catch {
|
||||
setTimeout(
|
||||
() => this.presentModalOldPassword(target, password),
|
||||
@@ -124,6 +125,8 @@ export class ServerBackupPage {
|
||||
buttonText: 'Create Backup',
|
||||
}
|
||||
|
||||
const { id } = await getServerInfo(this.patch)
|
||||
|
||||
this.dialogs
|
||||
.open<string>(PROMPT, {
|
||||
label: 'Original Password Needed',
|
||||
@@ -131,8 +134,7 @@ export class ServerBackupPage {
|
||||
})
|
||||
.pipe(take(1))
|
||||
.subscribe(async (oldPassword: string) => {
|
||||
const passwordHash = target.entry.startOs?.passwordHash || ''
|
||||
|
||||
const passwordHash = target.entry.startOs[id].passwordHash!
|
||||
argon2.verify(passwordHash, oldPassword)
|
||||
await this.createBackup(target, password, oldPassword)
|
||||
})
|
||||
|
||||
@@ -600,12 +600,15 @@ export module Mock {
|
||||
username: 'TestUser',
|
||||
mountable: false,
|
||||
startOs: {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
passwordHash:
|
||||
// password is asdfasdf
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.3.6',
|
||||
passwordHash:
|
||||
// password is asdfasdf
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
// 'ftcvewdnkemfksdm': {
|
||||
@@ -616,7 +619,7 @@ export module Mock {
|
||||
// used: 0,
|
||||
// model: 'Evo SATA 2.5',
|
||||
// vendor: 'Samsung',
|
||||
// startOs: null,
|
||||
// startOs: {},
|
||||
// },
|
||||
csgashbdjkasnd: {
|
||||
type: 'cifs',
|
||||
@@ -624,7 +627,7 @@ export module Mock {
|
||||
path: '/Desktop/startos-backups-2',
|
||||
username: 'TestUser',
|
||||
mountable: true,
|
||||
startOs: null,
|
||||
startOs: {},
|
||||
},
|
||||
powjefhjbnwhdva: {
|
||||
type: 'disk',
|
||||
@@ -635,30 +638,33 @@ export module Mock {
|
||||
model: null,
|
||||
vendor: 'SSK',
|
||||
startOs: {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
// password is asdfasdf
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.3.6',
|
||||
passwordHash:
|
||||
// password is asdfasdf
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const BackupInfo: RR.GetBackupInfoRes = {
|
||||
version: '0.3.0',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
packageBackups: {
|
||||
bitcoind: {
|
||||
title: 'Bitcoin Core',
|
||||
version: '0.21.0',
|
||||
osVersion: '0.3.0',
|
||||
osVersion: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
version: '0.2.2',
|
||||
osVersion: '0.3.0',
|
||||
osVersion: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -191,7 +191,12 @@ export module RR {
|
||||
export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove
|
||||
export type RemoveBackupTargetRes = null
|
||||
|
||||
export type GetBackupInfoReq = { targetId: string; password: string } // backup.target.info
|
||||
export type GetBackupInfoReq = {
|
||||
// backup.target.info
|
||||
targetId: string
|
||||
serverId: string
|
||||
password: string
|
||||
}
|
||||
export type GetBackupInfoRes = BackupInfo
|
||||
|
||||
export type CreateBackupReq = {
|
||||
@@ -239,7 +244,7 @@ export module RR {
|
||||
// package.backup.restore
|
||||
ids: string[]
|
||||
targetId: string
|
||||
oldPassword: string | null
|
||||
serverId: string
|
||||
password: string
|
||||
}
|
||||
export type RestorePackagesRes = null
|
||||
@@ -403,7 +408,7 @@ export interface DiskBackupTarget {
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: StartOSDiskInfo | null
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
}
|
||||
|
||||
export interface CifsBackupTarget {
|
||||
@@ -412,7 +417,7 @@ export interface CifsBackupTarget {
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
startOs: StartOSDiskInfo | null
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
}
|
||||
|
||||
export type RecoverySource = DiskRecoverySource | CifsRecoverySource
|
||||
|
||||
@@ -582,7 +582,7 @@ export class MockApiService extends ApiService {
|
||||
path: path.replace(/\\/g, '/'),
|
||||
username,
|
||||
mountable: true,
|
||||
startOs: null,
|
||||
startOs: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const mockPatchData: DataModel = {
|
||||
// password is asdfasdf
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
eosVersionCompat: '>=0.3.0 <=0.3.0.1',
|
||||
eosVersionCompat: '>=0.3.0 <=0.3.6',
|
||||
statusInfo: {
|
||||
backupProgress: null,
|
||||
updated: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface MappedBackupTarget<T> {
|
||||
id: string
|
||||
hasValidBackup: boolean
|
||||
hasAnyBackup: boolean
|
||||
entry: T
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user