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,11 +18,14 @@ export class MockApiService implements ApiService {
capacity: 73264762332,
used: null,
startOs: {
version: '0.2.17',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
timestamp: new Date().toISOString(),
version: '0.2.17',
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
},
},
guid: null,
},
@@ -41,11 +44,14 @@ export class MockApiService implements ApiService {
capacity: 73264762332,
used: null,
startOs: {
version: '0.3.3',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
timestamp: new Date().toISOString(),
version: '0.2.17',
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
},
},
guid: null,
},
@@ -64,11 +70,14 @@ export class MockApiService implements ApiService {
capacity: 73264762332,
used: null,
startOs: {
version: '0.3.2',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
timestamp: new Date().toISOString(),
version: '0.2.17',
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
},
},
guid: 'guid-guid-guid-guid',
},

View File

@@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { CifsModal } from './cifs-modal.page'
import { ServerBackupSelectModule } from '../server-backup-select/server-backup-select.module'
@NgModule({
declarations: [CifsModal],
imports: [CommonModule, FormsModule, IonicModule],
imports: [CommonModule, FormsModule, IonicModule, ServerBackupSelectModule],
exports: [CifsModal],
})
export class CifsModalModule {}

View File

@@ -4,9 +4,9 @@ import {
LoadingController,
ModalController,
} from '@ionic/angular'
import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service'
import { ApiService } from 'src/app/services/api/api.service'
import { StartOSDiskInfo } from '@start9labs/shared'
import { PasswordPage } from '../password/password.page'
import { ServerBackupSelectModal } from '../server-backup-select/server-backup-select.page'
@Component({
selector: 'cifs-modal',
@@ -50,30 +50,29 @@ export class CifsModal {
await loader.dismiss()
this.presentModalPassword(diskInfo)
this.presentModalSelectServer(diskInfo)
} catch (e) {
await loader.dismiss()
this.presentAlertFailed()
}
}
private async presentModalPassword(diskInfo: StartOSDiskInfo): Promise<void> {
const target: CifsBackupTarget = {
...this.cifs,
mountable: true,
startOs: diskInfo,
}
private async presentModalSelectServer(
servers: Record<string, StartOSDiskInfo>,
): Promise<void> {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
component: ServerBackupSelectModal,
componentProps: {
servers: Object.keys(servers).map(id => ({ id, ...servers[id] })),
},
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
this.modalController.dismiss(
{
cifs: this.cifs,
recoveryPassword: res.data.password,
serverId: res.data.serverId,
recoveryPassword: res.data.recoveryPassword,
},
'success',
)

View File

@@ -1,9 +1,5 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, ModalController } from '@ionic/angular'
import {
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.service'
import * as argon2 from '@start9labs/argon2'
@Component({
@@ -13,7 +9,7 @@ import * as argon2 from '@start9labs/argon2'
})
export class PasswordPage {
@ViewChild('focusInput') elem?: IonInput
@Input() target?: CifsBackupTarget | DiskBackupTarget
@Input() passwordHash = ''
@Input() storageDrive = false
pwError = ''
@@ -31,13 +27,8 @@ export class PasswordPage {
}
async verifyPw() {
if (!this.target || !this.target.startOs)
this.pwError = 'No recovery target' // unreachable
try {
const passwordHash = this.target!.startOs?.passwordHash || ''
argon2.verify(passwordHash, this.password)
argon2.verify(this.passwordHash, this.password)
this.modalController.dismiss({ password: this.password }, 'success')
} catch (e) {
this.pwError = 'Incorrect password provided'
@@ -55,7 +46,7 @@ export class PasswordPage {
}
validate() {
if (!!this.target) return (this.pwError = '')
if (!!this.passwordHash) return (this.pwError = '')
if (this.passwordVer) {
this.checkVer()

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 { ServerBackupSelectModal } from './server-backup-select.page'
import { PasswordPageModule } from '../password/password.module'
@NgModule({
declarations: [ServerBackupSelectModal],
imports: [CommonModule, FormsModule, IonicModule, PasswordPageModule],
exports: [ServerBackupSelectModal],
})
export class ServerBackupSelectModule {}

View File

@@ -0,0 +1,24 @@
<ion-header>
<ion-toolbar>
<ion-title>Select Server to Restore</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item *ngFor="let server of servers" button (click)="select(server)">
<ion-label>
<h2>
<b>Local Hostname</b>
: {{ server.hostname }}.local
</h2>
<h2>
<b>StartOS Version</b>
: {{ server.version }}
</h2>
<h2>
<b>Created</b>
: {{ server.timestamp | date : 'medium' }}
</h2>
</ion-label>
</ion-item>
</ion-content>

View File

@@ -0,0 +1,44 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { StartOSDiskInfoWithId } from 'src/app/services/api/api.service'
import { PasswordPage } from '../password/password.page'
@Component({
selector: 'server-backup-select',
templateUrl: 'server-backup-select.page.html',
styleUrls: ['server-backup-select.page.scss'],
})
export class ServerBackupSelectModal {
@Input() servers: StartOSDiskInfoWithId[] = []
constructor(private readonly modalController: ModalController) {}
cancel() {
this.modalController.dismiss()
}
async select(server: StartOSDiskInfoWithId): Promise<void> {
this.presentModalPassword(server)
}
private async presentModalPassword(
server: StartOSDiskInfoWithId,
): Promise<void> {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { passwordHash: server.passwordHash },
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
this.modalController.dismiss(
{
serverId: server.id,
recoveryPassword: res.data.password,
},
'success',
)
}
})
await modal.present()
}
}

View File

@@ -9,6 +9,7 @@ import { DiskInfo, ErrorService, GuidPipe } from '@start9labs/shared'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { T } from '@start9labs/start-sdk'
@Component({
selector: 'app-embassy',
@@ -50,15 +51,19 @@ export class EmbassyPage {
const disks = await this.apiService.getDrives()
if (this.stateService.setupType === 'fresh') {
this.storageDrives = disks
} else if (this.stateService.setupType === 'restore') {
this.storageDrives = disks.filter(
d =>
this.stateService.recoverySource?.type === 'backup' &&
this.stateService.recoverySource.target?.type === 'disk' &&
!d.partitions
.map(p => p.logicalname)
.includes(this.stateService.recoverySource.target.logicalname),
)
} else if (
this.stateService.setupType === 'restore' &&
this.stateService.recoverySource?.type === 'backup'
) {
if (this.stateService.recoverySource.target.type === 'disk') {
const logicalname =
this.stateService.recoverySource.target.logicalname
this.storageDrives = disks.filter(
d => !d.partitions.map(p => p.logicalname).includes(logicalname),
)
} else {
this.storageDrives = disks
}
} else if (
this.stateService.setupType === 'transfer' &&
this.stateService.recoverySource?.type === 'migrate'
@@ -95,10 +100,10 @@ export class EmbassyPage {
text: 'Continue',
handler: () => {
// for backup recoveries
if (this.stateService.recoveryPassword) {
if (this.stateService.recoverySource?.type === 'backup') {
this.setupEmbassy(
drive.logicalname,
this.stateService.recoveryPassword,
this.stateService.recoverySource.password,
)
} else {
// for migrations and fresh setups
@@ -111,8 +116,11 @@ export class EmbassyPage {
await alert.present()
} else {
// for backup recoveries
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
if (this.stateService.recoverySource?.type === 'backup') {
this.setupEmbassy(
drive.logicalname,
this.stateService.recoverySource.password,
)
} else {
// for migrations and fresh setups
this.presentModalPassword(drive.logicalname)
@@ -154,3 +162,7 @@ export class EmbassyPage {
}
}
}
function isDiskRecovery(source: T.RecoverySource<string>): source is any {
return source.type === 'backup' && source.target.type === 'disk'
}

View File

@@ -1,14 +0,0 @@
<div class="inline">
<!-- has backup -->
<h2 *ngIf="hasValidBackup; else noBackup">
<ion-icon name="cloud-done" color="success"></ion-icon>
StartOS backup detected
</h2>
<!-- no backup -->
<ng-template #noBackup>
<h2>
<ion-icon name="cloud-offline" color="danger"></ion-icon>
No StartOS backup
</h2>
</ng-template>
</div>

View File

@@ -3,13 +3,13 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { DriveStatusComponent, RecoverPage } from './recover.page'
import { RecoverPage } from './recover.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { RecoverPageRoutingModule } from './recover-routing.module'
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
@NgModule({
declarations: [RecoverPage, DriveStatusComponent],
declarations: [RecoverPage],
imports: [
CommonModule,
FormsModule,

View File

@@ -54,29 +54,21 @@
</b>
</div>
<ng-container *ngFor="let mapped of mappedDrives">
<ion-item
button
*ngIf="mapped.drive as drive"
[disabled]="!driveClickable(mapped)"
(click)="select(drive)"
lines="none"
>
<ion-icon
slot="start"
name="save-outline"
size="large"
></ion-icon>
<ng-container *ngFor="let server of servers">
<ion-item button (click)="select(server)" lines="none">
<ion-label>
<h1>{{ drive.label || drive.logicalname }}</h1>
<drive-status
[hasValidBackup]="mapped.hasValidBackup"
></drive-status>
<p>
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model ||
'Unknown Model' }}
</p>
<p>Capacity: {{ drive.capacity | convertBytes }}</p>
<h2>
<b>Local Hostname</b>
: {{ server.hostname }}.local
</h2>
<h2>
<b>StartOS Version</b>
: {{ server.version }}
</h2>
<h2>
<b>Created</b>
: {{ server.timestamp | date : 'medium' }}
</h2>
</ion-label>
</ion-item>
</ng-container>

View File

@@ -1,8 +1,11 @@
import { Component, Input } from '@angular/core'
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { ErrorService } from '@start9labs/shared'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import {
ApiService,
StartOSDiskInfoWithId,
} from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
@@ -13,7 +16,7 @@ import { PasswordPage } from '../../modals/password/password.page'
})
export class RecoverPage {
loading = true
mappedDrives: MappedDisk[] = []
servers: StartOSDiskInfoWithId[] = []
constructor(
private readonly apiService: ApiService,
@@ -34,33 +37,19 @@ export class RecoverPage {
await this.getDrives()
}
driveClickable(mapped: MappedDisk) {
return mapped.drive.startOs?.full
}
async getDrives() {
this.mappedDrives = []
try {
const disks = await this.apiService.getDrives()
disks
.filter(d => d.partitions.length)
.forEach(d => {
d.partitions.forEach(p => {
const drive: DiskBackupTarget = {
vendor: d.vendor,
model: d.model,
logicalname: p.logicalname,
label: p.label,
capacity: p.capacity,
used: p.used,
startOs: p.startOs,
}
this.mappedDrives.push({
hasValidBackup: !!p.startOs?.full,
drive,
})
})
})
const drives = await this.apiService.getDrives()
this.servers = drives.flatMap(drive =>
drive.partitions.flatMap(partition =>
Object.entries(partition.startOs).map(([id, val]) => ({
id,
...val,
partition,
drive,
})),
),
)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -74,65 +63,41 @@ export class RecoverPage {
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
const { hostname, path, username, password } = res.data.cifs
this.stateService.recoverySource = {
type: 'backup',
target: {
type: 'cifs',
hostname,
path,
username,
password,
...res.data.cifs,
},
serverId: res.data.serverId,
password: res.data.recoveryPassword,
}
this.stateService.recoveryPassword = res.data.recoveryPassword
this.navCtrl.navigateForward('/storage')
}
})
await modal.present()
}
async select(target: DiskBackupTarget) {
const { logicalname } = target
if (!logicalname) return
async select(server: StartOSDiskInfoWithId) {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
componentProps: { passwordHash: server.passwordHash },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.password) {
this.selectRecoverySource(logicalname, res.data.password)
if (res.role === 'success') {
this.stateService.recoverySource = {
type: 'backup',
target: {
type: 'disk',
logicalname: res.data.logicalname,
},
serverId: server.id,
password: res.data.password,
}
this.navCtrl.navigateForward(`/storage`)
}
})
await modal.present()
}
private async selectRecoverySource(logicalname: string, password?: string) {
this.stateService.recoverySource = {
type: 'backup',
target: {
type: 'disk',
logicalname,
},
}
this.stateService.recoveryPassword = password
this.navCtrl.navigateForward(`/storage`)
}
}
@Component({
selector: 'drive-status',
templateUrl: './drive-status.component.html',
styleUrls: ['./recover.page.scss'],
})
export class DriveStatusComponent {
@Input() hasValidBackup!: boolean
}
interface MappedDisk {
hasValidBackup: boolean
drive: DiskBackupTarget
}

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ResponsiveColModule } from '@start9labs/shared'
import { SuccessPage } from './success.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { SuccessPageRoutingModule } from './success-routing.module'

View File

@@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service'
selector: 'success',
templateUrl: 'success.page.html',
styleUrls: ['success.page.scss'],
providers: [DownloadHTMLService],
})
export class SuccessPage {
@ViewChild('canvas', { static: true })

View File

@@ -1,5 +1,10 @@
import * as jose from 'node-jose'
import { DiskListResponse, StartOSDiskInfo } from '@start9labs/shared'
import {
DiskInfo,
DiskListResponse,
PartitionInfo,
StartOSDiskInfo,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { Observable } from 'rxjs'
@@ -10,14 +15,16 @@ export abstract class ApiService {
abstract getStatus(): Promise<T.SetupStatusRes | null> // setup.status
abstract getPubKey(): Promise<void> // setup.get-pubkey
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract verifyCifs(cifs: T.VerifyCifsParams): Promise<StartOSDiskInfo> // setup.cifs.verify
abstract verifyCifs(
cifs: T.VerifyCifsParams,
): Promise<Record<string, StartOSDiskInfo>> // setup.cifs.verify
abstract attach(importInfo: T.AttachParams): Promise<T.SetupProgress> // setup.attach
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
abstract complete(): Promise<T.SetupResult> // setup.complete
abstract exit(): Promise<void> // setup.exit
abstract openProgressWebsocket$(guid: string): Observable<T.FullProgress>
async encrypt(toEncrypt: string): Promise<Encrypted> {
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
if (!this.pubkey) throw new Error('No pubkey found!')
const encrypted = await jose.JWE.createEncrypt(this.pubkey!)
.update(toEncrypt)
@@ -28,26 +35,13 @@ export abstract class ApiService {
}
}
type Encrypted = {
encrypted: string
}
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
export type DiskBackupTarget = {
vendor: string | null
model: string | null
logicalname: string | null
label: string | null
capacity: number
used: number | null
startOs: StartOSDiskInfo | null
export type StartOSDiskInfoWithId = StartOSDiskInfo & {
id: string
}
export type CifsBackupTarget = {
hostname: string
path: string
username: string
mountable: boolean
startOs: StartOSDiskInfo | null
export type StartOSDiskInfoFull = StartOSDiskInfoWithId & {
partition: PartitionInfo
drive: DiskInfo
}

View File

@@ -9,7 +9,7 @@ import {
RPCOptions,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { ApiService, WebsocketConfig } from './api.service'
import { ApiService } from './api.service'
import * as jose from 'node-jose'
import { Observable } from 'rxjs'
import { DOCUMENT } from '@angular/common'
@@ -65,9 +65,11 @@ export class LiveApiService extends ApiService {
})
}
async verifyCifs(source: T.VerifyCifsParams): Promise<StartOSDiskInfo> {
async verifyCifs(
source: T.VerifyCifsParams,
): Promise<Record<string, StartOSDiskInfo>> {
source.path = source.path.replace('/\\/g', '/')
return this.rpcRequest<StartOSDiskInfo>({
return this.rpcRequest<Record<string, StartOSDiskInfo>>({
method: 'setup.cifs.verify',
params: source,
})

View File

@@ -175,11 +175,14 @@ export class MockApiService extends ApiService {
capacity: 1979120929996,
used: null,
startOs: {
version: '0.2.17',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
version: '0.2.17',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
},
},
guid: null,
},
@@ -198,11 +201,14 @@ export class MockApiService extends ApiService {
capacity: 73264762332,
used: null,
startOs: {
version: '0.3.3',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
version: '0.2.17',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
},
},
guid: null,
},
@@ -221,11 +227,14 @@ export class MockApiService extends ApiService {
capacity: 73264762332,
used: null,
startOs: {
version: '0.3.2',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
'1234-5678-9876-5432': {
hostname: 'adjective-noun',
version: '0.2.17',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: null,
},
},
guid: 'guid-guid-guid-guid',
},
@@ -236,14 +245,19 @@ export class MockApiService extends ApiService {
]
}
async verifyCifs(params: T.VerifyCifsParams): Promise<StartOSDiskInfo> {
async verifyCifs(
params: T.VerifyCifsParams,
): Promise<Record<string, StartOSDiskInfo>> {
await pauseFor(1000)
return {
version: '0.3.0',
full: true,
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: '',
'9876-5432-1234-5678': {
hostname: 'adjective-noun',
version: '0.3.6',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: '',
},
}
}

View File

@@ -7,8 +7,7 @@ import { T } from '@start9labs/start-sdk'
})
export class StateService {
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
recoverySource?: T.RecoverySource
recoveryPassword?: string
recoverySource?: T.RecoverySource<string>
constructor(private readonly api: ApiService) {}
@@ -26,9 +25,13 @@ export class StateService {
await this.api.execute({
startOsLogicalname: storageLogicalname,
startOsPassword: await this.api.encrypt(password),
recoverySource: this.recoverySource || null,
recoveryPassword: this.recoveryPassword
? await this.api.encrypt(this.recoveryPassword)
recoverySource: this.recoverySource
? this.recoverySource.type === 'migrate'
? this.recoverySource
: {
...this.recoverySource,
password: await this.api.encrypt(this.recoverySource.password),
}
: null,
})
}

View File

@@ -1,7 +1,9 @@
import { DOCUMENT } from '@angular/common'
import { Inject, Injectable } from '@angular/core'
@Injectable()
@Injectable({
providedIn: 'root',
})
export class DownloadHTMLService {
constructor(@Inject(DOCUMENT) private readonly document: Document) {}

View File

@@ -32,13 +32,14 @@ export interface PartitionInfo {
label: string | null
capacity: number
used: number | null
startOs: StartOSDiskInfo | null
startOs: Record<string, StartOSDiskInfo>
guid: string | null
}
export type StartOSDiskInfo = {
hostname: string
version: string
full: boolean
timestamp: string
passwordHash: string | null
wrappedKey: string | null
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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' }} -

View File

@@ -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({

View File

@@ -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
)
}
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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,

View File

@@ -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)

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')
}
}

View File

@@ -1,5 +1,5 @@
<backup-drives
type="restore"
class="ion-page"
(onSelect)="presentModalPassword($event)"
(onSelect)="presentModalSelectServer($event)"
></backup-drives>

View File

@@ -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],
})

View File

@@ -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()

View File

@@ -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)
})

View File

@@ -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(),
},
},

View File

@@ -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

View File

@@ -582,7 +582,7 @@ export class MockApiService extends ApiService {
path: path.replace(/\\/g, '/'),
username,
mountable: true,
startOs: null,
startOs: {},
},
}
}

View File

@@ -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,

View File

@@ -1,5 +1,5 @@
export interface MappedBackupTarget<T> {
id: string
hasValidBackup: boolean
hasAnyBackup: boolean
entry: T
}