Backups Rework (#698)

* wip: Backup al

* wip: Backup

* backup code complete

* wip

* wip

* update types

* wip

* fix errors

* Backups wizard (#699)

* backup adjustments

* fix endpoint arg

* Update prod-key-modal.page.ts

Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>

* build errs addressed

* working

* update backup command input, nix, and apk add

* add ecryptfs-utils

* fix build

* wip

* fixes for macos

* more mac magic

* fix typo

* working

* fixes after rebase

* chore: remove unused imports

Co-authored-by: Justin Miller <dragondef@gmail.com>
Co-authored-by: Drew Ansbacher <drew.ansbacher@gmail.com>
Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2021-10-23 22:00:23 -06:00
parent 78dcce7be5
commit 8056285a7f
52 changed files with 2032 additions and 873 deletions

View File

@@ -38,7 +38,7 @@ export class EmbassyPage {
async getDrives () {
try {
this.storageDrives = (await this.apiService.getDrives()).filter(d => d.logicalname !== this.stateService.recoveryDrive?.logicalname)
this.storageDrives = (await this.apiService.getDrives()).filter(d => !d.partitions.map(p => p.logicalname).includes(this.stateService.recoveryPartition?.logicalname))
} catch (e) {
this.errorToastService.present(e.message)
} finally {
@@ -98,7 +98,7 @@ export class EmbassyPage {
console.error(e.details)
} finally {
loader.dismiss()
if (!!this.stateService.recoveryDrive) {
if (!!this.stateService.recoveryPartition) {
await this.navCtrl.navigateForward(`/loading`, { animationDirection: 'forward' })
} else {
await this.navCtrl.navigateForward(`/success`, { animationDirection: 'forward' })

View File

@@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
import { ApiService, DiskInfo, PartitionInfo } from 'src/app/services/api/api.service'
import * as argon2 from '@start9labs/argon2'
@Component({
selector: 'app-password',
@@ -8,7 +9,7 @@ import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
styleUrls: ['password.page.scss'],
})
export class PasswordPage {
@Input() recoveryDrive: DiskInfo
@Input() recoveryPartition: PartitionInfo
@Input() storageDrive: DiskInfo
pwError = ''
@@ -34,23 +35,13 @@ export class PasswordPage {
}
async verifyPw () {
if (!this.recoveryDrive) this.pwError = 'No recovery drive' // unreachable
const loader = await this.loadingCtrl.create({
message: 'Verifying Password',
})
await loader.present()
if (!this.recoveryPartition || !this.recoveryPartition['embassy-os']) this.pwError = 'No recovery drive' // unreachable
try {
const isCorrectPassword = await this.apiService.verify03XPassword(this.recoveryDrive.logicalname, this.password)
if (isCorrectPassword) {
this.modalController.dismiss({ password: this.password })
} else {
this.pwError = 'Incorrect password provided'
}
argon2.verify( this.recoveryPartition['embassy-os']['password-hash'], this.password)
this.modalController.dismiss({ password: this.password })
} catch (e) {
this.pwError = 'Error connecting to Embassy'
} finally {
loader.dismiss()
this.pwError = 'Incorrect password provided'
}
}
@@ -65,7 +56,7 @@ export class PasswordPage {
}
validate () {
if (!!this.recoveryDrive) return this.pwError = ''
if (!!this.recoveryPartition) return this.pwError = ''
if (this.passwordVer) {
this.checkVer()

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
import { ApiService, PartitionInfo } from 'src/app/services/api/api.service'
import { HttpService } from 'src/app/services/api/http.service'
@Component({
@@ -9,7 +9,7 @@ import { HttpService } from 'src/app/services/api/http.service'
styleUrls: ['prod-key-modal.page.scss'],
})
export class ProdKeyModal {
@Input() recoveryDrive: DiskInfo
@Input() recoveryPartition: PartitionInfo
error = ''
productKey = ''
@@ -31,7 +31,7 @@ export class ProdKeyModal {
await loader.present()
try {
await this.apiService.set02XDrive(this.recoveryDrive.logicalname)
await this.apiService.set02XDrive(this.recoveryPartition.logicalname)
this.httpService.productKey = this.productKey
await this.apiService.verifyProductKey()
this.modalController.dismiss({ productKey: this.productKey })

View File

@@ -15,7 +15,7 @@
<ion-card-content class="ion-margin">
<ng-container *ngIf="!loading && !recoveryDrives.length">
<ng-container *ngIf="!loading && !recoveryPartitions.length">
<h2 color="light">No recovery drives found</h2>
<p color="light">Please connect a recovery drive to your Embassy and refresh the page.</p>
<ion-button
@@ -40,29 +40,29 @@
</ion-label>
</ion-item>
</ng-container>
<ng-container *ngIf="recoveryDrives.length">
<ion-item (click)="chooseDrive(drive)" class="ion-margin-bottom" button color="light" lines="none" *ngFor="let drive of recoveryDrives" [ngClass]="drive.logicalname === selectedDrive?.logicalname ? 'selected' : null">
<ng-container *ngIf="recoveryPartitions.length">
<ion-item (click)="choosePartition(p.partition)" class="ion-margin-bottom" button color="light" lines="none" *ngFor="let p of recoveryPartitions" [ngClass]="p.partition.logicalname === selectedPartition?.logicalname ? 'selected' : null">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label class="ion-text-wrap">
<h1>{{ drive.logicalname }} - {{ drive.capacity | convertBytes }}</h1>
<h2 *ngIf="drive.vendor || drive.model">
{{ drive.vendor }}
<span *ngIf="drive.vendor && drive.model"> - </span>
{{ drive.model }}
<h1>{{ p.partition.logicalname }} <span *ngIf="!!p.partition.label">-</span> {{ p.partition.label }}</h1>
<h2 *ngIf="p.vendor || p.model">
{{ p.vendor }}
<span *ngIf="p.vendor && p.model"> - </span>
{{ p.model }}
</h2>
<h2> Embassy version: {{drive['embassy-os'].version}}</h2>
<h2> Embassy version: {{p.partition['embassy-os'].version}}</h2>
</ion-label>
<ion-icon *ngIf="(drive['embassy-os'].version.startsWith('0.2') && stateService.hasProductKey) || passwords[drive.logicalname] || prodKeys[drive.logicalname]" color="success" slot="end" name="lock-open-outline"></ion-icon>
<ion-icon *ngIf="(drive['embassy-os'].version.startsWith('0.2') && !stateService.hasProductKey && !prodKeys[drive.logicalname]) || (!drive['embassy-os'].version.startsWith('0.2') && !passwords[drive.logicalname])" color="danger" slot="end" name="lock-closed-outline"></ion-icon>
<ion-icon *ngIf="(p.partition['embassy-os'].version.startsWith('0.2') && stateService.hasProductKey) || passwords[p.partition.logicalname] || prodKeys[p.partition.logicalname]" color="success" slot="end" name="lock-open-outline"></ion-icon>
<ion-icon *ngIf="(p.partition['embassy-os'].version.startsWith('0.2') && !stateService.hasProductKey && !prodKeys[p.partition.logicalname]) || (!p.partition['embassy-os'].version.startsWith('0.2') && !passwords[p.partition.logicalname])" color="danger" slot="end" name="lock-closed-outline"></ion-icon>
</ion-item>
</ng-container>
<ion-button
(click)="selectRecoveryDrive()"
(click)="selectRecoveryPartition()"
color="light"
[disabled]="!selectedDrive || (!passwords[selectedDrive.logicalname] && !selectedDrive['embassy-os'].version.startsWith('0.2'))"
[disabled]="!selectedPartition || (!passwords[selectedPartition.logicalname] && !selectedPartition['embassy-os'].version.startsWith('0.2'))"
class="claim-button"
*ngIf="recoveryDrives.length"
*ngIf="recoveryPartitions.length"
>
Next
</ion-button>

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
import { ApiService, PartitionInfo } 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'
@@ -14,8 +14,8 @@ import { ProdKeyModal } from '../prod-key-modal/prod-key-modal.page'
export class RecoverPage {
passwords = { }
prodKeys = { }
recoveryDrives = []
selectedDrive: DiskInfo = null
recoveryPartitions: { partition: PartitionInfo, model: string, vendor: string }[] = []
selectedPartition: PartitionInfo = null
loading = true
constructor (
@@ -27,26 +27,25 @@ export class RecoverPage {
) { }
async ngOnInit () {
await this.getDrives()
await this.getPartitions()
}
async refresh () {
this.recoveryDrives = []
this.selectedDrive = null
this.recoveryPartitions = []
this.selectedPartition = null
this.loading = true
await this.getDrives()
await this.getPartitions()
}
async getDrives () {
async getPartitions () {
try {
let drives = (await this.apiService.getDrives()).filter(d => !!d['embassy-os'])
let drives = (await this.apiService.getDrives())
this.recoveryPartitions = drives.map(d => d.partitions.map(p => ({ partition: p, vendor: d.vendor, model: d.model})).filter(p => p.partition['embassy-os']?.full)).flat()
// if theres no product key, only show 0.2s
if (!this.stateService.hasProductKey) {
drives = drives.filter(d => d['embassy-os'].version.startsWith('0.2'))
this.recoveryPartitions = this.recoveryPartitions.filter(p => p.partition['embassy-os']?.version.startsWith('0.2'))
}
this.recoveryDrives = drives
} catch (e) {
this.errorToastService.present(`${e.message}: ${e.data}`)
} finally {
@@ -54,30 +53,29 @@ export class RecoverPage {
}
}
async chooseDrive (drive: DiskInfo) {
if (this.selectedDrive?.logicalname === drive.logicalname) {
this.selectedDrive = null
async choosePartition (partition: PartitionInfo) {
if (this.selectedPartition?.logicalname === partition.logicalname) {
this.selectedPartition = null
return
} else {
this.selectedDrive = drive
this.selectedPartition = partition
}
if ((drive['embassy-os'].version.startsWith('0.2') && this.stateService.hasProductKey) || this.passwords[drive.logicalname] || this.prodKeys[drive.logicalname]) return
if ((partition['embassy-os'].version.startsWith('0.2') && this.stateService.hasProductKey) || this.passwords[partition.logicalname] || this.prodKeys[partition.logicalname]) return
if (this.stateService.hasProductKey) {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: {
recoveryDrive: this.selectedDrive,
recoveryPartition: this.selectedPartition,
},
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(async ret => {
if (!ret.data) {
this.selectedDrive = null
this.selectedPartition = null
} else if (ret.data.password) {
this.passwords[drive.logicalname] = ret.data.password
this.passwords[partition.logicalname] = ret.data.password
}
})
@@ -86,15 +84,15 @@ export class RecoverPage {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: {
recoveryDrive: this.selectedDrive,
recoveryPartition: this.selectedPartition,
},
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(async ret => {
if (!ret.data) {
this.selectedDrive = null
this.selectedPartition = null
} else if (ret.data.productKey) {
this.prodKeys[drive.logicalname] = ret.data.productKey
this.prodKeys[partition.logicalname] = ret.data.productKey
}
})
@@ -102,9 +100,9 @@ export class RecoverPage {
}
}
async selectRecoveryDrive () {
this.stateService.recoveryDrive = this.selectedDrive
const pw = this.passwords[this.selectedDrive.logicalname]
async selectRecoveryPartition () {
this.stateService.recoveryPartition = this.selectedPartition
const pw = this.passwords[this.selectedPartition.logicalname]
if (pw) {
this.stateService.recoveryPassword = pw
}

View File

@@ -19,7 +19,7 @@ export interface GetStatusRes {
export interface SetupEmbassyReq {
'embassy-logicalname': string
'embassy-password': string
'recovery-drive'?: DiskInfo
'recovery-partition'?: PartitionInfo
'recovery-password'?: string
}
@@ -34,7 +34,6 @@ export interface DiskInfo {
model: string | null,
partitions: PartitionInfo[],
capacity: number,
'embassy-os': EmbassyOsDiskInfo | null,
}
export interface RecoveryStatusRes {
@@ -42,13 +41,16 @@ export interface RecoveryStatusRes {
'total-bytes': number
}
interface PartitionInfo {
export interface PartitionInfo {
logicalname: string,
label: string | null,
capacity: number,
used: number | null,
'embassy-os': EmbassyOsRecoveryInfo | null,
}
interface EmbassyOsDiskInfo {
export interface EmbassyOsRecoveryInfo {
version: string,
}
full: boolean, // contains full embassy backup
'password-hash': string | null, // null for 0.2.x
}

View File

@@ -18,7 +18,7 @@ export class MockApiService extends ApiService {
async getStatus () {
await pauseFor(1000)
return {
'product-key': true,
'product-key': false,
migrating: false,
}
}
@@ -36,31 +36,24 @@ export class MockApiService extends ApiService {
label: 'label 1',
capacity: 100000,
used: 200.1255312,
'embassy-os': null,
},
{
logicalname: 'sda2',
label: 'label 2',
capacity: 50000,
used: 200.1255312,
'embassy-os': null,
},
],
capacity: 150000,
'embassy-os': null,
},
{
vendor: 'Vendor',
model: 'Model',
logicalname: 'dev/sdb',
partitions: [
// {
// logicalname: 'sdb1',
// label: null,
// capacity: 1600.01234,
// used: 0.00,
// }
],
partitions: [],
capacity: 1600.01234,
'embassy-os': null,
},
{
vendor: 'Vendor',
@@ -72,12 +65,36 @@ export class MockApiService extends ApiService {
label: 'label 1',
capacity: null,
used: null,
'embassy-os': {
version: '0.3.3',
full: true,
'password-hash': 'asdfasdfasdf',
},
},
{
logicalname: 'sdc1MOCKTESTER',
label: 'label 1',
capacity: null,
used: null,
'embassy-os': {
version: '0.3.6',
full: true,
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
},
},
{
logicalname: 'sdc1',
label: 'label 1',
capacity: null,
used: null,
'embassy-os': {
version: '0.3.3',
full: false,
'password-hash': 'asdfasdfasdf',
},
},
],
capacity: 100000,
'embassy-os': {
version: '0.3.3',
},
},
{
vendor: 'Vendor',
@@ -89,12 +106,14 @@ export class MockApiService extends ApiService {
label: null,
capacity: 10000,
used: null,
'embassy-os': {
version: '0.2.7',
full: true,
'password-hash': 'asdfasdfasdf',
},
},
],
capacity: 10000,
'embassy-os': {
version: '0.2.7',
},
},
]
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { ApiService, DiskInfo } from './api/api.service'
import { ApiService, DiskInfo, PartitionInfo } from './api/api.service'
import { ErrorToastService } from './error-toast.service'
@Injectable({
@@ -14,7 +14,7 @@ export class StateService {
storageDrive: DiskInfo
embassyPassword: string
recoveryDrive: DiskInfo
recoveryPartition: PartitionInfo
recoveryPassword: string
dataTransferProgress: { bytesTransferred: number; totalBytes: number } | null
dataProgress = 0
@@ -61,7 +61,7 @@ export class StateService {
const ret = await this.apiService.setupEmbassy({
'embassy-logicalname': this.storageDrive.logicalname,
'embassy-password': this.embassyPassword,
'recovery-drive': this.recoveryDrive,
'recovery-partition': this.recoveryPartition,
'recovery-password': this.recoveryPassword,
})
this.torAddress = ret['tor-address']