mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user