mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
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:
@@ -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' })
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user