mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Feature/cloud backups (#889)
* cifs for cloud backups on lan * password spelling fix * fix spelling and fix rpc method * fix other methods * remove old code and rename method * add support for cifs backup targets wip cifs api simplify idiom add doc comment wip wip should work™ * add password hash to server info * fix type * fix types for cifs * minor fixes for cifs feature * fix rpc structure * fix copy, address some TODOs * add subcommand * backup path and navigation * wizard edits * rebased success page * wiz conflicts resolved * current change actually * only unsub if done * no fileter if necessary * fix copy for cifs old password * setup complete (#913) * setup complete * minor fixes * setup.complete * complete bool * setup-wizard: complete boolean Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { SuccessPageModule } from './pages/success/success.module'
|
||||
import { InitPageModule } from './pages/init/init.module'
|
||||
import { HomePageModule } from './pages/home/home.module'
|
||||
import { LoadingPageModule } from './pages/loading/loading.module'
|
||||
import { ProdKeyModalModule } from './pages/prod-key-modal/prod-key-modal.module'
|
||||
import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
|
||||
import { ProductKeyPageModule } from './pages/product-key/product-key.module'
|
||||
import { RecoverPageModule } from './pages/recover/recover.module'
|
||||
|
||||
|
||||
20
setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts
Normal file
20
setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { CifsModal } from './cifs-modal.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CifsModal,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
CifsModal,
|
||||
],
|
||||
})
|
||||
export class CifsModalModule { }
|
||||
81
setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html
Normal file
81
setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Connect Shared Folder
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<form (ngSubmit)="submit()" #cifsForm="ngForm">
|
||||
<p>Hostname *</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="hostname"
|
||||
required
|
||||
[(ngModel)]="cifs.hostname"
|
||||
name="hostname"
|
||||
#hostname="ngModel"
|
||||
placeholder="e.g. My Computer, Bob's Laptop"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<p [hidden]="hostname.valid || hostname.pristine">
|
||||
<ion-text color="danger">Hostname is required</ion-text>
|
||||
</p>
|
||||
|
||||
<p>Path *</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="path"
|
||||
required
|
||||
[(ngModel)]="cifs.path"
|
||||
name="path"
|
||||
#path="ngModel"
|
||||
placeholder="ex. /Desktop/my-folder'"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<p [hidden]="path.valid || path.pristine">
|
||||
<ion-text color="danger">Path is required</ion-text>
|
||||
</p>
|
||||
|
||||
<p>Username *</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="username"
|
||||
required
|
||||
[(ngModel)]="cifs.username"
|
||||
name="username"
|
||||
#username="ngModel"
|
||||
placeholder="Enter username"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<p [hidden]="username.valid || username.pristine">
|
||||
<ion-text color="danger">Username is required</ion-text>
|
||||
</p>
|
||||
|
||||
<p>Password</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="password"
|
||||
type="password"
|
||||
[(ngModel)]="cifs.password"
|
||||
name="password"
|
||||
#password="ngModel"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<button hidden type="submit"></button>
|
||||
</form>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" [disabled]="!cifsForm.form.valid" (click)="submit()">
|
||||
Verify
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
85
setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts
Normal file
85
setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService, BackupTarget, CifsBackupTarget, EmbassyOSRecoveryInfo } from 'src/app/services/api/api.service'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'cifs-modal',
|
||||
templateUrl: 'cifs-modal.page.html',
|
||||
styleUrls: ['cifs-modal.page.scss'],
|
||||
})
|
||||
export class CifsModal {
|
||||
cifs = {
|
||||
hostname: '',
|
||||
path: '',
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly modalController: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) { }
|
||||
|
||||
cancel () {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
|
||||
async submit (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Connecting to shared folder...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const embassyOS = await this.apiService.verifyCifs(this.cifs)
|
||||
this.presentModalPassword(embassyOS)
|
||||
} catch (e) {
|
||||
this.presentAlertFailed()
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalPassword (embassyOS: EmbassyOSRecoveryInfo): Promise<void> {
|
||||
const target: CifsBackupTarget = {
|
||||
type: 'cifs',
|
||||
...this.cifs,
|
||||
mountable: true,
|
||||
'embassy-os': embassyOS,
|
||||
}
|
||||
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.modalController.dismiss({
|
||||
cifs: this.cifs,
|
||||
recoveryPassword: res.data.password,
|
||||
}, 'success')
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async presentAlertFailed (): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Connection Failed',
|
||||
message: 'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.',
|
||||
buttons: ['OK'],
|
||||
})
|
||||
alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
interface MappedCifs {
|
||||
hasValidBackup: boolean
|
||||
cifs: CifsBackupTarget
|
||||
}
|
||||
@@ -4,16 +4,17 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { PasswordPage } from './password.page'
|
||||
|
||||
import { PasswordPageRoutingModule } from './password-routing.module'
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
PasswordPage,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
PasswordPageRoutingModule,
|
||||
],
|
||||
declarations: [PasswordPage],
|
||||
exports: [
|
||||
PasswordPage,
|
||||
],
|
||||
})
|
||||
export class PasswordPageModule { }
|
||||
@@ -17,13 +17,8 @@
|
||||
</div>
|
||||
|
||||
<form (ngSubmit)="!!storageDrive ? submitPw() : verifyPw()">
|
||||
<p class="input-label">
|
||||
Password:
|
||||
</p>
|
||||
<ion-item
|
||||
color="dark"
|
||||
[class]="pwError ? 'error-border' : password && !!storageDrive ? 'success-border' : ''"
|
||||
>
|
||||
<p>Password</p>
|
||||
<ion-item [class]="pwError ? 'error-border' : password && !!storageDrive ? 'success-border' : ''">
|
||||
<ion-input
|
||||
#focusInput
|
||||
[(ngModel)]="password"
|
||||
@@ -41,10 +36,8 @@
|
||||
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ pwError }}</p>
|
||||
</div>
|
||||
<ng-container *ngIf="!!storageDrive">
|
||||
<p class="input-label">
|
||||
Confirm Password:
|
||||
</p>
|
||||
<ion-item color="dark" [class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''">
|
||||
<p>Confirm Password</p>
|
||||
<ion-item [class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''">
|
||||
<ion-input
|
||||
[(ngModel)]="passwordVer"
|
||||
[ngModelOptions]="{'standalone': true}"
|
||||
3
setup-wizard/src/app/modals/password/password.page.scss
Normal file
3
setup-wizard/src/app/modals/password/password.page.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, ModalController } from '@ionic/angular'
|
||||
import { DiskInfo, PartitionInfo } from 'src/app/services/api/api.service'
|
||||
import { BackupTarget, DiskInfo } from 'src/app/services/api/api.service'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
|
||||
@Component({
|
||||
@@ -10,7 +10,7 @@ import * as argon2 from '@start9labs/argon2'
|
||||
})
|
||||
export class PasswordPage {
|
||||
@ViewChild('focusInput') elem: IonInput
|
||||
@Input() recoveryPartition: PartitionInfo
|
||||
@Input() target: BackupTarget
|
||||
@Input() storageDrive: DiskInfo
|
||||
|
||||
pwError = ''
|
||||
@@ -30,11 +30,11 @@ export class PasswordPage {
|
||||
}
|
||||
|
||||
async verifyPw () {
|
||||
if (!this.recoveryPartition || !this.recoveryPartition['embassy-os']) this.pwError = 'No recovery drive' // unreachable
|
||||
if (!this.target || !this.target['embassy-os']) this.pwError = 'No recovery target' // unreachable
|
||||
|
||||
try {
|
||||
argon2.verify(this.recoveryPartition['embassy-os']['password-hash'], this.password)
|
||||
this.modalController.dismiss({ password: this.password })
|
||||
argon2.verify(this.target['embassy-os']['password-hash'], this.password)
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
} catch (e) {
|
||||
this.pwError = 'Incorrect password provided'
|
||||
}
|
||||
@@ -47,11 +47,11 @@ export class PasswordPage {
|
||||
}
|
||||
|
||||
if (this.pwError || this.verError) return
|
||||
this.modalController.dismiss({ password: this.password })
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
}
|
||||
|
||||
validate () {
|
||||
if (!!this.recoveryPartition) return this.pwError = ''
|
||||
if (!!this.target) return this.pwError = ''
|
||||
|
||||
if (this.passwordVer) {
|
||||
this.checkVer()
|
||||
@@ -4,15 +4,17 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ProdKeyModal } from './prod-key-modal.page'
|
||||
|
||||
import { ProdKeyModalRoutingModule } from './prod-key-modal-routing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ProdKeyModal,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ProdKeyModalRoutingModule,
|
||||
],
|
||||
declarations: [ProdKeyModal],
|
||||
exports: [
|
||||
ProdKeyModal,
|
||||
],
|
||||
})
|
||||
export class ProdKeyModalModule { }
|
||||
@@ -12,7 +12,7 @@
|
||||
<div style="padding-bottom: 16px;">
|
||||
<p>Verify the product key for the chosen recovery drive.</p>
|
||||
</div>
|
||||
<ion-item color="dark">
|
||||
<ion-item>
|
||||
<ion-input
|
||||
#focusInput
|
||||
[(ngModel)]="productKey"
|
||||
@@ -0,0 +1,3 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService, PartitionInfo } from 'src/app/services/api/api.service'
|
||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { HttpService } from 'src/app/services/api/http.service'
|
||||
|
||||
@Component({
|
||||
@@ -10,7 +10,7 @@ import { HttpService } from 'src/app/services/api/http.service'
|
||||
})
|
||||
export class ProdKeyModal {
|
||||
@ViewChild('focusInput') elem: IonInput
|
||||
@Input() recoveryPartition: PartitionInfo
|
||||
@Input() target: DiskBackupTarget
|
||||
|
||||
error = ''
|
||||
productKey = ''
|
||||
@@ -36,10 +36,10 @@ export class ProdKeyModal {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.set02XDrive(this.recoveryPartition.logicalname)
|
||||
await this.apiService.set02XDrive(this.target.logicalname)
|
||||
this.httpService.productKey = this.productKey
|
||||
await this.apiService.verifyProductKey()
|
||||
this.modalController.dismiss({ productKey: this.productKey })
|
||||
this.modalController.dismiss({ productKey: this.productKey }, 'success')
|
||||
} catch (e) {
|
||||
this.httpService.productKey = undefined
|
||||
this.error = 'Invalid Product Key'
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { EmbassyPage } from './embassy.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { EmbassyPageRoutingModule } from './embassy-routing.module'
|
||||
import { PipesModule } from 'src/app/pipes/pipe.module'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content>
|
||||
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
|
||||
<ion-row style="height: 100%;">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
@@ -14,29 +14,19 @@
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<ng-container *ngIf="!loading && !storageDrives.length">
|
||||
<h2>No drives found</h2>
|
||||
<p>Please connect a storage drive to your Embassy and refresh the page.</p>
|
||||
<ion-button style="margin-top: 25px;" (click)="refresh()" color="light">
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
<!-- loading -->
|
||||
<ion-spinner *ngIf="loading; else loaded" class="center-spinner" name="lines"></ion-spinner>
|
||||
|
||||
<ion-item-group>
|
||||
<ng-container *ngIf="loading">
|
||||
<ion-item button lines="none">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text animated></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<ion-skeleton-text style="width: 80%; margin: 13px 0;" animated></ion-skeleton-text>
|
||||
<ion-skeleton-text style="width: 60%; margin: 10px 0;" animated></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- not loading -->
|
||||
<ng-template #loaded>
|
||||
<ng-container *ngIf="!storageDrives.length">
|
||||
<h2>No drives found</h2>
|
||||
<p>Please connect an storage drive to your Embassy and click "Refresh".</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="storageDrives.length">
|
||||
<ion-item (click)="chooseDrive(drive)" class="ion-margin-bottom" button lines="none" *ngFor="let drive of storageDrives" [disabled]="drive.capacity < 34359738368">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
|
||||
<ion-item-group *ngIf="storageDrives.length">
|
||||
<ion-item (click)="chooseDrive(drive)" class="ion-margin-bottom" button lines="none" *ngFor="let drive of storageDrives">
|
||||
<ion-icon slot="start" name="save-outline" size="large" color="light"></ion-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h1>{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }}</h1>
|
||||
<h2>{{ drive.logicalname }} - {{ drive.capacity | convertBytes }}</h2>
|
||||
@@ -47,8 +37,8 @@
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, LoadingController, ModalController, NavController } from '@ionic/angular'
|
||||
import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
|
||||
import { ApiService, DiskInfo, DiskRecoverySource } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-embassy',
|
||||
@@ -11,8 +11,7 @@ import { PasswordPage } from '../password/password.page'
|
||||
styleUrls: ['embassy.page.scss'],
|
||||
})
|
||||
export class EmbassyPage {
|
||||
storageDrives = []
|
||||
selectedDrive: DiskInfo = null
|
||||
storageDrives: DiskInfo[] = []
|
||||
loading = true
|
||||
|
||||
constructor (
|
||||
@@ -30,15 +29,14 @@ export class EmbassyPage {
|
||||
}
|
||||
|
||||
async refresh () {
|
||||
this.storageDrives = []
|
||||
this.selectedDrive = null
|
||||
this.loading = true
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
async getDrives () {
|
||||
try {
|
||||
this.storageDrives = (await this.apiService.getDrives()).filter(d => !d.partitions.map(p => p.logicalname).includes(this.stateService.recoveryPartition?.logicalname))
|
||||
const drives = await this.apiService.getDrives()
|
||||
this.storageDrives = drives.filter(d => !d.partitions.map(p => p.logicalname).includes((this.stateService.recoverySource as DiskRecoverySource)?.logicalname))
|
||||
} catch (e) {
|
||||
this.errorToastService.present(e.message)
|
||||
} finally {
|
||||
@@ -60,14 +58,22 @@ export class EmbassyPage {
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
this.presentModalPassword(drive)
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive, this.stateService.recoveryPassword)
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive, this.stateService.recoveryPassword)
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,31 +86,30 @@ export class EmbassyPage {
|
||||
})
|
||||
modal.onDidDismiss().then(async ret => {
|
||||
if (!ret.data || !ret.data.password) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Transferring encrypted data',
|
||||
})
|
||||
|
||||
await loader.present()
|
||||
|
||||
this.stateService.storageDrive = drive
|
||||
this.stateService.embassyPassword = ret.data.password
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy()
|
||||
if (!!this.stateService.recoveryPartition) {
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} else {
|
||||
await this.navCtrl.navigateForward(`/init`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorToastService.present(`${e.message}: ${e.details}`)
|
||||
console.error(e.message)
|
||||
console.error(e.details)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
this.setupEmbassy(drive, ret.data.password)
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async setupEmbassy (drive: DiskInfo, password: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Transferring encrypted data. This could take a while...',
|
||||
})
|
||||
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy(drive.logicalname, password)
|
||||
if (!!this.stateService.recoverySource) {
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} else {
|
||||
await this.navCtrl.navigateForward(`/init`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorToastService.present(`${e.message}: ${e.details}. Restart Embassy to try again.`)
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
|
||||
import { HomePageRoutingModule } from './home-routing.module'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content>
|
||||
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
|
||||
<ion-row style="height: 100%;">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content>
|
||||
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
|
||||
<ion-row style="height: 100%;">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { interval, Observable, Subscription } from 'rxjs'
|
||||
import { delay, finalize, take, tap } from 'rxjs/operators'
|
||||
import { interval, Subscription } from 'rxjs'
|
||||
import { finalize, take, tap } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
@@ -9,14 +10,18 @@ import { StateService } from 'src/app/services/state.service'
|
||||
styleUrls: ['init.page.scss'],
|
||||
})
|
||||
export class InitPage {
|
||||
progress: number
|
||||
progress = 0
|
||||
sub: Subscription
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
public readonly stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
// call setup.complete to tear down embassy.local and spin up embassy-[id].local
|
||||
this.apiService.setupComplete()
|
||||
|
||||
this.sub = interval(130)
|
||||
.pipe(
|
||||
take(101),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content color="light">
|
||||
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
|
||||
<ion-row style="height: 100%;">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
|
||||
@@ -15,10 +15,10 @@ export class LoadingPage {
|
||||
|
||||
ngOnInit () {
|
||||
this.stateService.pollDataTransferProgress()
|
||||
const progSub = this.stateService.dataProgSubject.subscribe(async progress => {
|
||||
if (progress === 1) {
|
||||
const progSub = this.stateService.dataCompletionSubject.subscribe(async complete => {
|
||||
if (complete) {
|
||||
progSub.unsubscribe()
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
await this.navCtrl.navigateForward(`/init`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { PasswordPage } from './password.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PasswordPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PasswordPageRoutingModule { }
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ProdKeyModal } from './prod-key-modal.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProdKeyModal,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProdKeyModalRoutingModule { }
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ProductKeyPage } from './product-key.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { ProductKeyPageRoutingModule } from './product-key-routing.module'
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content>
|
||||
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
|
||||
<ion-row style="height: 100%;">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
@@ -8,14 +8,14 @@
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-header class="ion-text-center" style="padding-bottom: 8px;">
|
||||
<ion-card-header style="padding-bottom: 8px;">
|
||||
<ion-card-title>Enter Product Key</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<form (submit)="submit()" style="margin-bottom: 12px;">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<p class="input-label">Product Key</p>
|
||||
<p class="ion-text-left">Product Key</p>
|
||||
<ion-item color="dark">
|
||||
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
|
||||
<ion-input
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ion-item {
|
||||
--border-style: solid;
|
||||
--border-width: 1px;
|
||||
--border-color: var(--ion-color-medium);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="inline">
|
||||
<h2 *ngIf="hasValidBackup">
|
||||
<ion-icon name="cloud-done" color="success"></ion-icon>
|
||||
Embassy backup detected
|
||||
</h2>
|
||||
<h2 *ngIf="!hasValidBackup">
|
||||
<ion-icon name="cloud-offline" color="danger"></ion-icon>
|
||||
No Embassy backup
|
||||
</h2>
|
||||
</div>
|
||||
@@ -2,14 +2,15 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { RecoverPage } from './recover.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
import { ProdKeyModalModule } from '../prod-key-modal/prod-key-modal.module'
|
||||
import { DriveStatusComponent, RecoverPage } from './recover.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
|
||||
import { RecoverPageRoutingModule } from './recover-routing.module'
|
||||
import { PipesModule } from 'src/app/pipes/pipe.module'
|
||||
|
||||
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [RecoverPage, DriveStatusComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@@ -18,7 +19,7 @@ import { PipesModule } from 'src/app/pipes/pipe.module'
|
||||
PasswordPageModule,
|
||||
ProdKeyModalModule,
|
||||
PipesModule,
|
||||
CifsModalModule,
|
||||
],
|
||||
declarations: [RecoverPage],
|
||||
})
|
||||
export class RecoverPageModule { }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content>
|
||||
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
|
||||
<ion-row style="height: 100%;">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
|
||||
<div style="padding-bottom: 32px;" class="ion-text-center">
|
||||
@@ -9,66 +9,53 @@
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-header class="ion-text-center">
|
||||
<ion-card-title>Select Recovery Drive</ion-card-title>
|
||||
<ion-card-subtitle>Select the drive containing the Embassy you want to recover.</ion-card-subtitle>
|
||||
<ion-card-title>Restore from Backup</ion-card-title>
|
||||
<ion-card-subtitle>Select the shared folder or physical drive containing your Embassy backup</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<ng-container *ngIf="!loading && !drives.length">
|
||||
<h2>No recovery drives found</h2>
|
||||
<p>Please connect a recovery drive to your Embassy and refresh the page.</p>
|
||||
<ion-button
|
||||
(click)="refresh()"
|
||||
style="text-align:center"
|
||||
class="claim-button"
|
||||
>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
<ion-spinner *ngIf="loading" class="center-spinner" name="lines"></ion-spinner>
|
||||
|
||||
<ion-item-group>
|
||||
<ng-container *ngIf="loading">
|
||||
<ion-skeleton-text animated class="skeleton-header"></ion-skeleton-text>
|
||||
<ion-item color="light" style="padding-bottom: 10px;">
|
||||
<ion-avatar slot="start" style="margin-right: 24px;">
|
||||
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<!-- loaded -->
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<!-- cifs -->
|
||||
<h2 class="target-label">
|
||||
Shared Network Folder
|
||||
</h2>
|
||||
<p class="ion-padding-bottom">
|
||||
Using a shared folder is the recommended way to recover from backup, since it works with all Embassy hardware configurations.
|
||||
To restore from a shared folder, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>.
|
||||
</p>
|
||||
|
||||
<!-- connect -->
|
||||
<ion-item button lines="none" (click)="presentModalCifs()">
|
||||
<ion-icon slot="start" name="folder-open-outline" size="large" color="light"></ion-icon>
|
||||
<ion-label>Open Shared Folder</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<!-- drives -->
|
||||
<h2 class="target-label">
|
||||
Physical Drives
|
||||
</h2>
|
||||
<p class="ion-padding-bottom">
|
||||
Warning! Plugging in more than one physical drive to Embassy can lead to power failure and data corruption.
|
||||
To restore from a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>.
|
||||
</p>
|
||||
|
||||
<ng-container *ngFor="let target of driveTargets">
|
||||
<ion-item button *ngIf="target.drive as drive" [disabled]="!driveClickable(drive)" (click)="select(drive)">
|
||||
<ion-icon slot="start" name="save-outline" size="large" color="light"></ion-icon>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 100px; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 50px; height: 16px; margin-bottom: 16px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 100px;"></ion-skeleton-text>
|
||||
<h1>{{ drive.label || drive.logicalname }}</h1>
|
||||
<drive-status [hasValidBackup]="target.hasValidBackup"></drive-status>
|
||||
<p>{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }}</p>
|
||||
<p>Capacity: {{ drive.capacity | convertBytes }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- loaded -->
|
||||
<div *ngFor="let drive of drives" class="ion-padding-bottom">
|
||||
<h2 class="drive-label">
|
||||
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }} - {{ drive.capacity | convertBytes }}
|
||||
</h2>
|
||||
<ion-item lines="none" button *ngFor="let partition of drive.partitions" [disabled]="!partitionClickable(partition)" (click)="choosePartition(partition)">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partitionClickable(partition)">
|
||||
<ion-text color="success">
|
||||
Embassy backup detected
|
||||
</ion-text>
|
||||
</p>
|
||||
<p *ngIf="!partitionClickable(partition)">
|
||||
<ion-text>
|
||||
No Embassy backup detected
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<div *ngIf="partition['embassy-os'] && partition['embassy-os'].full">
|
||||
<ion-icon *ngIf="partition['embassy-os'].version.startsWith('0.2')" color="success" slot="end" name="lock-open-outline" size="large"></ion-icon>
|
||||
<ion-icon *ngIf="!partition['embassy-os'].version.startsWith('0.2')" color="danger" slot="end" name="lock-closed-outline" size="large"></ion-icon>
|
||||
</div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
@@ -1,20 +1,4 @@
|
||||
.selected {
|
||||
border: 4px solid var(--ion-color-secondary);
|
||||
box-shadow: 4px 4px 16px var(--ion-color-light);
|
||||
}
|
||||
|
||||
.drive-label {
|
||||
.target-label {
|
||||
font-weight: bold;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.skeleton-header {
|
||||
width: 180px;
|
||||
height: 18px;
|
||||
--ion-text-color-rgb: var(--ion-color-light-rgb);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { AlertController, LoadingController, ModalController, NavController } from '@ionic/angular'
|
||||
import { ApiService, DiskInfo, PartitionInfo } from 'src/app/services/api/api.service'
|
||||
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
|
||||
import { ApiService, CifsBackupTarget, DiskBackupTarget, DiskRecoverySource, RecoverySource } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
import { ProdKeyModal } from '../prod-key-modal/prod-key-modal.page'
|
||||
import { MappedDisk } from 'src/app/util/misc.util'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-recover',
|
||||
@@ -12,14 +14,14 @@ import { ProdKeyModal } from '../prod-key-modal/prod-key-modal.page'
|
||||
styleUrls: ['recover.page.scss'],
|
||||
})
|
||||
export class RecoverPage {
|
||||
selectedPartition: PartitionInfo = null
|
||||
loading = true
|
||||
drives: DiskInfo[] = []
|
||||
driveTargets: MappedDisk[] = []
|
||||
hasShownGuidAlert = false
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
@@ -32,25 +34,43 @@ export class RecoverPage {
|
||||
}
|
||||
|
||||
async refresh () {
|
||||
this.selectedPartition = null
|
||||
this.loading = true
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
partitionClickable (partition: PartitionInfo) {
|
||||
return partition['embassy-os']?.full && (this.stateService.hasProductKey || this.is02x(partition))
|
||||
driveClickable (drive: DiskBackupTarget) {
|
||||
return drive['embassy-os']?.full && (this.stateService.hasProductKey || this.is02x(drive))
|
||||
}
|
||||
|
||||
async getDrives () {
|
||||
this.driveTargets = []
|
||||
try {
|
||||
const drives = await this.apiService.getDrives()
|
||||
this.drives = drives.filter(d => d.partitions.length)
|
||||
drives.filter(d => d.partitions.length).forEach(d => {
|
||||
d.partitions.forEach(p => {
|
||||
this.driveTargets.push(
|
||||
{
|
||||
hasValidBackup: p['embassy-os']?.full,
|
||||
drive: {
|
||||
type: 'disk',
|
||||
vendor: d.vendor,
|
||||
model: d.model,
|
||||
logicalname: p.logicalname,
|
||||
label: p.label,
|
||||
capacity: p.capacity,
|
||||
used: p.used,
|
||||
'embassy-os': p['embassy-os'],
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const importableDrive = drives.find(d => !!d.guid)
|
||||
if (!!importableDrive && !this.hasShownGuidAlert) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Embassy Drive Detected',
|
||||
message: 'A valid EmbassyOS data drive has been detected. To use this drive in its current state, simply click "Use Drive" below.',
|
||||
message: 'A valid EmbassyOS data drive has been detected. To use this drive as-is, simply click "Use Drive" below.',
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
@@ -74,14 +94,69 @@ export class RecoverPage {
|
||||
}
|
||||
}
|
||||
|
||||
async importDrive (guid: string) {
|
||||
async presentModalCifs (): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: CifsModal,
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
const { hostname, path, username, password } = res.data.cifs
|
||||
this.stateService.recoverySource = {
|
||||
type: 'cifs',
|
||||
hostname,
|
||||
path,
|
||||
username,
|
||||
password,
|
||||
}
|
||||
this.stateService.recoveryPassword = res.data.recoveryPassword
|
||||
this.navCtrl.navigateForward('/embassy')
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async select (target: DiskBackupTarget) {
|
||||
if (target['embassy-os'].version.startsWith('0.2')) {
|
||||
return this.selectRecoverySource(target.logicalname)
|
||||
}
|
||||
|
||||
if (this.stateService.hasProductKey) {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.selectRecoverySource(target.logicalname, res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
// if no product key, it means they are an upgrade kit user
|
||||
} else {
|
||||
const modal = await this.modalController.create({
|
||||
component: ProdKeyModal,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.productKey) {
|
||||
this.selectRecoverySource(target.logicalname)
|
||||
}
|
||||
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
private async importDrive (guid: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Importing Drive',
|
||||
})
|
||||
await loader.present()
|
||||
try {
|
||||
await this.stateService.importDrive(guid)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
await this.navCtrl.navigateForward(`/init`)
|
||||
} catch (e) {
|
||||
this.errorToastService.present(`${e.message}: ${e.data}`)
|
||||
} finally {
|
||||
@@ -89,60 +164,26 @@ export class RecoverPage {
|
||||
}
|
||||
}
|
||||
|
||||
async choosePartition (partition: PartitionInfo) {
|
||||
this.selectedPartition = partition
|
||||
|
||||
if (partition['embassy-os'].version.startsWith('0.2')) {
|
||||
return this.selectRecoveryPartition()
|
||||
}
|
||||
|
||||
if (this.stateService.hasProductKey) {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: {
|
||||
recoveryPartition: this.selectedPartition,
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(async ret => {
|
||||
if (!ret.data) {
|
||||
this.selectedPartition = null
|
||||
} else if (ret.data.password) {
|
||||
this.selectRecoveryPartition(ret.data.password)
|
||||
}
|
||||
|
||||
})
|
||||
await modal.present()
|
||||
// if no product key, it means they are an upgrade kit user
|
||||
} else {
|
||||
const modal = await this.modalController.create({
|
||||
component: ProdKeyModal,
|
||||
componentProps: {
|
||||
recoveryPartition: this.selectedPartition,
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(async ret => {
|
||||
if (!ret.data) {
|
||||
this.selectedPartition = null
|
||||
} else if (ret.data.productKey) {
|
||||
this.selectRecoveryPartition()
|
||||
}
|
||||
|
||||
})
|
||||
await modal.present()
|
||||
private async selectRecoverySource (logicalname: string, password?: string) {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'disk',
|
||||
logicalname,
|
||||
}
|
||||
this.stateService.recoveryPassword = password
|
||||
this.navCtrl.navigateForward(`/embassy`)
|
||||
}
|
||||
|
||||
async selectRecoveryPartition (password?: string) {
|
||||
this.stateService.recoveryPartition = this.selectedPartition
|
||||
if (password) {
|
||||
this.stateService.recoveryPassword = password
|
||||
}
|
||||
await this.navCtrl.navigateForward(`/embassy`)
|
||||
}
|
||||
|
||||
private is02x (partition: PartitionInfo): boolean {
|
||||
return !this.stateService.hasProductKey && partition['embassy-os']?.version.startsWith('0.2')
|
||||
private is02x (drive: DiskBackupTarget): boolean {
|
||||
return !this.stateService.hasProductKey && drive['embassy-os']?.version.startsWith('0.2')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'drive-status',
|
||||
templateUrl: './drive-status.component.html',
|
||||
styleUrls: ['./recover.page.scss'],
|
||||
})
|
||||
export class DriveStatusComponent {
|
||||
@Input() hasValidBackup: boolean
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { SuccessPage } from './success.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: SuccessPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class SuccessPageRoutingModule { }
|
||||
@@ -3,17 +3,13 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { SuccessPage } from './success.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
|
||||
import { SuccessPageRoutingModule } from './success-routing.module'
|
||||
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
SuccessPageRoutingModule,
|
||||
PasswordPageModule,
|
||||
],
|
||||
declarations: [SuccessPage],
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
For a list of recommended browsers, click <a href="https://docs.start9.com/user-manual/connecting.html" target="_blank" rel="noreferrer"><b>here</b></a>.
|
||||
</p>
|
||||
<br />
|
||||
<p class="input-label">Tor Address</p>
|
||||
<p>Tor Address</p>
|
||||
<ion-item lines="none" color="dark">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<code><ion-text color="light">{{ stateService.torAddress }}</ion-text></code>
|
||||
@@ -81,7 +81,7 @@
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<p class="input-label">LAN Address</p>
|
||||
<p>LAN Address</p>
|
||||
<ion-item lines="none" color="dark">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<code><ion-text color="light">{{ stateService.lanAddress }}</ion-text></code>
|
||||
|
||||
@@ -6,10 +6,11 @@ export abstract class ApiService {
|
||||
abstract getRecoveryStatus (): Promise<RecoveryStatusRes> // setup.recovery.status
|
||||
|
||||
// encrypted
|
||||
abstract verifyCifs (cifs: VerifyCifs): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
|
||||
abstract verifyProductKey (): Promise<void> // echo - throws error if invalid
|
||||
abstract verify03XPassword (logicalname: string, password: string): Promise<boolean> // setup.recovery.test-password
|
||||
abstract importDrive (guid: string): Promise<SetupEmbassyRes> // setup.execute
|
||||
abstract setupEmbassy (setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
||||
abstract setupComplete (): Promise<void> // setup.complete
|
||||
}
|
||||
|
||||
export interface GetStatusRes {
|
||||
@@ -17,11 +18,13 @@ export interface GetStatusRes {
|
||||
migrating: boolean
|
||||
}
|
||||
|
||||
export type VerifyCifs = Omit<CifsRecoverySource, 'type'>
|
||||
|
||||
export interface SetupEmbassyReq {
|
||||
'embassy-logicalname': string
|
||||
'embassy-password': string
|
||||
'recovery-partition'?: PartitionInfo
|
||||
'recovery-password'?: string
|
||||
'recovery-source': RecoverySource | null
|
||||
'recovery-password': string | null
|
||||
}
|
||||
|
||||
export interface SetupEmbassyRes {
|
||||
@@ -30,6 +33,50 @@ export interface SetupEmbassyRes {
|
||||
'root-ca': string
|
||||
}
|
||||
|
||||
export type BackupTarget = DiskBackupTarget | CifsBackupTarget
|
||||
|
||||
export interface EmbassyOSRecoveryInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
'password-hash': string | null
|
||||
'wrapped-key': string | null
|
||||
}
|
||||
|
||||
export interface DiskBackupTarget {
|
||||
type: 'disk'
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string | null
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export interface CifsBackupTarget {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export type RecoverySource = DiskRecoverySource | CifsRecoverySource
|
||||
|
||||
export interface DiskRecoverySource {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export interface CifsRecoverySource {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
|
||||
export interface DiskInfo {
|
||||
logicalname: string,
|
||||
vendor: string | null,
|
||||
@@ -42,6 +89,7 @@ export interface DiskInfo {
|
||||
export interface RecoveryStatusRes {
|
||||
'bytes-transferred': number
|
||||
'total-bytes': number
|
||||
complete: boolean
|
||||
}
|
||||
|
||||
export interface PartitionInfo {
|
||||
@@ -49,11 +97,5 @@ export interface PartitionInfo {
|
||||
label: string | null,
|
||||
capacity: number,
|
||||
used: number | null,
|
||||
'embassy-os': EmbassyOsRecoveryInfo | null,
|
||||
}
|
||||
|
||||
export interface EmbassyOsRecoveryInfo {
|
||||
version: string,
|
||||
full: boolean, // contains full embassy backup
|
||||
'password-hash': string | null, // null for 0.2.x
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null,
|
||||
}
|
||||
|
||||
@@ -151,7 +151,6 @@ export enum Method {
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
// @TODO what are valid params? object, bool?
|
||||
params?: {
|
||||
[param: string]: string | number | boolean | object | string[] | number[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ApiService, DiskInfo, GetStatusRes, RecoveryStatusRes, SetupEmbassyReq, SetupEmbassyRes } from './api.service'
|
||||
import { ApiService, DiskInfo, EmbassyOSRecoveryInfo, GetStatusRes, RecoverySource, RecoveryStatusRes, SetupEmbassyReq, SetupEmbassyRes, VerifyCifs } from './api.service'
|
||||
import { HttpService } from './http.service'
|
||||
|
||||
@Injectable({
|
||||
@@ -43,6 +43,14 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// ** ENCRYPTED **
|
||||
|
||||
async verifyCifs (params: VerifyCifs) {
|
||||
params.path = params.path.replace('/\\/g', '/')
|
||||
return this.http.rpcRequest<EmbassyOSRecoveryInfo>({
|
||||
method: 'setup.cifs.verify',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async verifyProductKey () {
|
||||
return this.http.rpcRequest<void>({
|
||||
method: 'echo',
|
||||
@@ -50,13 +58,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async verify03XPassword (logicalname: string, password: string) {
|
||||
return this.http.rpcRequest<boolean>({
|
||||
method: 'setup.recovery.test-password',
|
||||
params: { logicalname, password },
|
||||
})
|
||||
}
|
||||
|
||||
async importDrive (guid: string) {
|
||||
const res = await this.http.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.attach',
|
||||
@@ -70,6 +71,10 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
async setupEmbassy (setupInfo: SetupEmbassyReq) {
|
||||
if (setupInfo['recovery-source'].type === 'cifs') {
|
||||
setupInfo['recovery-source'].path = setupInfo['recovery-source'].path.replace('/\\/g', '/')
|
||||
}
|
||||
|
||||
const res = await this.http.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.execute',
|
||||
params: setupInfo as any,
|
||||
@@ -80,4 +85,11 @@ export class LiveApiService extends ApiService {
|
||||
'root-ca': btoa(res['root-ca']),
|
||||
}
|
||||
}
|
||||
|
||||
async setupComplete () {
|
||||
await this.http.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.complete',
|
||||
params: { },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { ApiService, SetupEmbassyReq } from './api.service'
|
||||
import { ApiService, RecoverySource, SetupEmbassyReq, VerifyCifs } from './api.service'
|
||||
|
||||
let tries = 0
|
||||
|
||||
@@ -27,8 +27,8 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(1000)
|
||||
return [
|
||||
{
|
||||
vendor: 'Vendor',
|
||||
model: 'Model',
|
||||
vendor: 'Samsung',
|
||||
model: 'SATA',
|
||||
logicalname: '/dev/sda',
|
||||
guid: 'theguid',
|
||||
partitions: [
|
||||
@@ -50,16 +50,16 @@ export class MockApiService extends ApiService {
|
||||
capacity: 150000,
|
||||
},
|
||||
{
|
||||
vendor: 'Vendor',
|
||||
model: 'Model',
|
||||
vendor: 'Samsung',
|
||||
model: null,
|
||||
logicalname: 'dev/sdb',
|
||||
partitions: [],
|
||||
capacity: 34359738369,
|
||||
guid: null,
|
||||
},
|
||||
{
|
||||
vendor: 'Vendor',
|
||||
model: 'Model',
|
||||
vendor: 'Crucial',
|
||||
model: 'MX500',
|
||||
logicalname: 'dev/sdc',
|
||||
guid: null,
|
||||
partitions: [
|
||||
@@ -72,6 +72,7 @@ export class MockApiService extends ApiService {
|
||||
version: '0.3.3',
|
||||
full: true,
|
||||
'password-hash': 'asdfasdfasdf',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,6 +85,7 @@ export class MockApiService extends ApiService {
|
||||
full: true,
|
||||
// password is 'asdfasdf'
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -95,14 +97,15 @@ export class MockApiService extends ApiService {
|
||||
version: '0.3.3',
|
||||
full: false,
|
||||
'password-hash': 'asdfasdfasdf',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
capacity: 100000,
|
||||
},
|
||||
{
|
||||
vendor: 'Vendor',
|
||||
model: 'Model',
|
||||
vendor: 'Sandisk',
|
||||
model: null,
|
||||
logicalname: '/dev/sdd',
|
||||
guid: null,
|
||||
partitions: [
|
||||
@@ -115,6 +118,7 @@ export class MockApiService extends ApiService {
|
||||
version: '0.2.7',
|
||||
full: true,
|
||||
'password-hash': 'asdfasdfasdf',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -133,21 +137,27 @@ export class MockApiService extends ApiService {
|
||||
return {
|
||||
'bytes-transferred': tries,
|
||||
'total-bytes': 4,
|
||||
complete: tries === 4
|
||||
}
|
||||
}
|
||||
|
||||
// ** ENCRYPTED **
|
||||
|
||||
async verifyCifs (params: VerifyCifs) {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': '',
|
||||
}
|
||||
}
|
||||
|
||||
async verifyProductKey () {
|
||||
await pauseFor(1000)
|
||||
return
|
||||
}
|
||||
|
||||
async verify03XPassword (logicalname: string, password: string) {
|
||||
await pauseFor(2000)
|
||||
return password.length > 8
|
||||
}
|
||||
|
||||
async importDrive (guid: string) {
|
||||
await pauseFor(3000)
|
||||
return setupRes
|
||||
@@ -158,25 +168,13 @@ export class MockApiService extends ApiService {
|
||||
return setupRes
|
||||
}
|
||||
|
||||
async getRecoveryDrives () {
|
||||
await pauseFor(2000)
|
||||
return [
|
||||
{
|
||||
logicalname: 'Name1',
|
||||
version: '0.3.3',
|
||||
name: 'My Embassy',
|
||||
},
|
||||
{
|
||||
logicalname: 'Name2',
|
||||
version: '0.2.7',
|
||||
name: 'My Embassy',
|
||||
},
|
||||
]
|
||||
async setupComplete () {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
}
|
||||
|
||||
const rootCA =
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIDpzCCAo+gAwIBAgIRAIIuOarlQETlUQEOZJGZYdIwDQYJKoZIhvcNAQELBQAw
|
||||
bTELMAkGA1UEBhMCVVMxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEOMAwGA1UECwwF
|
||||
U2FsZXMxCzAJBgNVBAgMAldBMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20xEDAO
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ApiService, DiskInfo, PartitionInfo } from './api/api.service'
|
||||
import { ApiService, RecoverySource } from './api/api.service'
|
||||
import { ErrorToastService } from './error-toast.service'
|
||||
import { pauseFor } from '../util/misc.util'
|
||||
|
||||
@@ -14,13 +14,12 @@ export class StateService {
|
||||
polling = false
|
||||
embassyLoaded = false
|
||||
|
||||
storageDrive: DiskInfo
|
||||
embassyPassword: string
|
||||
recoveryPartition: PartitionInfo
|
||||
recoverySource: RecoverySource
|
||||
recoveryPassword: string
|
||||
dataTransferProgress: { bytesTransferred: number; totalBytes: number } | null
|
||||
|
||||
dataTransferProgress: { bytesTransferred: number, totalBytes: number, complete: boolean } | null
|
||||
dataProgress = 0
|
||||
dataProgSubject = new BehaviorSubject(this.dataProgress)
|
||||
dataCompletionSubject = new BehaviorSubject(false)
|
||||
|
||||
torAddress: string
|
||||
lanAddress: string
|
||||
@@ -33,47 +32,48 @@ export class StateService {
|
||||
|
||||
async pollDataTransferProgress () {
|
||||
this.polling = true
|
||||
await pauseFor(1000)
|
||||
await pauseFor(500)
|
||||
|
||||
if (
|
||||
this.dataTransferProgress?.totalBytes &&
|
||||
this.dataTransferProgress.bytesTransferred === this.dataTransferProgress.totalBytes
|
||||
) return
|
||||
this.dataTransferProgress?.complete
|
||||
) {
|
||||
this.dataCompletionSubject.next(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let progress
|
||||
try {
|
||||
progress = await this.apiService.getRecoveryStatus()
|
||||
} catch (e) {
|
||||
this.errorToastService.present(`${e.message}: ${e.details}`)
|
||||
this.errorToastService.present(`${e.message}: ${e.details}.\nRestart Embassy to try again.`)
|
||||
}
|
||||
if (progress) {
|
||||
this.dataTransferProgress = {
|
||||
bytesTransferred: progress['bytes-transferred'],
|
||||
totalBytes: progress['total-bytes'],
|
||||
complete: progress.complete,
|
||||
}
|
||||
if (this.dataTransferProgress.totalBytes) {
|
||||
this.dataProgress = this.dataTransferProgress.bytesTransferred / this.dataTransferProgress.totalBytes
|
||||
this.dataProgSubject.next(this.dataProgress)
|
||||
}
|
||||
}
|
||||
this.pollDataTransferProgress()
|
||||
setTimeout(() => this.pollDataTransferProgress(), 0) // prevent call stack from growing
|
||||
}
|
||||
|
||||
|
||||
async importDrive (guid: string) : Promise<void> {
|
||||
async importDrive (guid: string): Promise<void> {
|
||||
const ret = await this.apiService.importDrive(guid)
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address']
|
||||
this.cert = ret['root-ca']
|
||||
}
|
||||
|
||||
async setupEmbassy () : Promise<void> {
|
||||
async setupEmbassy (storageLogicalname: string, password: string): Promise<void> {
|
||||
const ret = await this.apiService.setupEmbassy({
|
||||
'embassy-logicalname': this.storageDrive.logicalname,
|
||||
'embassy-password': this.embassyPassword,
|
||||
'recovery-partition': this.recoveryPartition,
|
||||
'recovery-password': this.recoveryPassword,
|
||||
'embassy-logicalname': storageLogicalname,
|
||||
'embassy-password': password,
|
||||
'recovery-source': this.recoverySource || null,
|
||||
'recovery-password': this.recoveryPassword || null,
|
||||
})
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address']
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { DiskBackupTarget } from '../services/api/api.service'
|
||||
|
||||
export interface MappedDisk {
|
||||
hasValidBackup: boolean
|
||||
drive: DiskBackupTarget
|
||||
}
|
||||
|
||||
export const pauseFor = (ms: number) => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
Reference in New Issue
Block a user