diff --git a/backend/src/backup/backup_bulk.rs b/backend/src/backup/backup_bulk.rs index d49d2cd4c..1a433829a 100644 --- a/backend/src/backup/backup_bulk.rs +++ b/backend/src/backup/backup_bulk.rs @@ -1,7 +1,8 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; use chrono::Utc; +use clap::ArgMatches; use color_eyre::eyre::eyre; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; @@ -18,6 +19,7 @@ use super::PackageBackupReport; use crate::auth::check_password_against_db; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; +use crate::db::model::BackupProgress; use crate::db::util::WithRevision; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; @@ -112,12 +114,24 @@ impl Serialize for OsBackup { } } +fn parse_comma_separated(arg: &str, _: &ArgMatches<'_>) -> Result, Error> { + arg.split(',') + .map(|s| s.trim().parse().map_err(Error::from)) + .collect() +} + #[command(rename = "create", display(display_none))] #[instrument(skip(ctx, old_password, password))] pub async fn backup_all( #[context] ctx: RpcContext, #[arg(rename = "target-id")] target_id: BackupTargetId, #[arg(rename = "old-password", long = "old-password")] old_password: Option, + #[arg( + rename = "package-ids", + long = "package-ids", + parse(parse_comma_separated) + )] + service_ids: Option>, #[arg] password: String, ) -> Result, Error> { let mut db = ctx.db.handle(); @@ -130,17 +144,27 @@ pub async fn backup_all( old_password.as_ref().unwrap_or(&password), ) .await?; + let all_packages = crate::db::DatabaseModel::new() + .package_data() + .get(&mut db, false) + .await? + .0 + .keys() + .into_iter() + .cloned() + .collect(); + let service_ids = service_ids.unwrap_or(all_packages); if old_password.is_some() { backup_guard.change_password(&password)?; } - let revision = assure_backing_up(&mut db).await?; + let revision = assure_backing_up(&mut db, &service_ids).await?; tokio::task::spawn(async move { let backup_res = perform_backup(&ctx, &mut db, backup_guard).await; - let status_model = crate::db::DatabaseModel::new() + let backup_progress = crate::db::DatabaseModel::new() .server_info() .status_info() - .backing_up(); - status_model + .backup_progress(); + backup_progress .clone() .lock(&mut db, LockType::Write) .await @@ -207,8 +231,8 @@ pub async fn backup_all( .expect("failed to send notification"); } } - status_model - .put(&mut db, &false) + backup_progress + .delete(&mut db) .await .expect("failed to change server status"); }); @@ -218,23 +242,40 @@ pub async fn backup_all( }) } -#[instrument(skip(db))] -async fn assure_backing_up(db: &mut PatchDbHandle) -> Result>, Error> { +#[instrument(skip(db, packages))] +async fn assure_backing_up( + db: &mut PatchDbHandle, + packages: impl IntoIterator, +) -> Result>, Error> { let mut tx = db.begin().await?; let mut backing_up = crate::db::DatabaseModel::new() .server_info() .status_info() - .backing_up() + .backup_progress() .get_mut(&mut tx) .await?; - if *backing_up { + if backing_up + .iter() + .flat_map(|x| x.values()) + .fold(false, |acc, x| { + if !x.complete { + return true; + } + acc + }) + { return Err(Error::new( eyre!("Server is already backing up!"), crate::ErrorKind::InvalidRequest, )); } - *backing_up = true; + *backing_up = Some( + packages + .into_iter() + .map(|x| (x.clone(), BackupProgress { complete: false })) + .collect(), + ); backing_up.save(&mut tx).await?; Ok(tx.commit(None).await?) } @@ -343,7 +384,7 @@ async fn perform_backup( backup_guard .metadata .package_backups - .insert(package_id, pkg_meta); + .insert(package_id.clone(), pkg_meta); } main_status_model @@ -355,6 +396,24 @@ async fn perform_backup( }, ) .await?; + + let mut backup_progress: BTreeMap<_, _> = (crate::db::DatabaseModel::new() + .server_info() + .status_info() + .backup_progress() + .get(&mut tx, true) + .await? + .into_owned()) + .unwrap_or_default(); + if let Some(mut backup_progress) = backup_progress.get_mut(&package_id) { + (*backup_progress).complete = true; + } + crate::db::DatabaseModel::new() + .server_info() + .status_info() + .backup_progress() + .put(&mut tx, &backup_progress) + .await?; tx.save().await?; } @@ -394,6 +453,5 @@ async fn perform_backup( .last_backup() .put(&mut db, ×tamp) .await?; - Ok(backup_report) } diff --git a/backend/src/db/model.rs b/backend/src/db/model.rs index 549f42938..06e37a723 100644 --- a/backend/src/db/model.rs +++ b/backend/src/db/model.rs @@ -51,7 +51,7 @@ impl Database { .parse() .unwrap(), status_info: ServerStatus { - backing_up: false, + backup_progress: None, updated: false, update_progress: None, }, @@ -99,10 +99,16 @@ pub struct ServerInfo { pub password_hash: String, } +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +pub struct BackupProgress { + pub complete: bool, +} + #[derive(Debug, Default, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] pub struct ServerStatus { - pub backing_up: bool, + #[model] + pub backup_progress: Option>, pub updated: bool, #[model] pub update_progress: Option, diff --git a/backend/src/init.rs b/backend/src/init.rs index 181ce2644..eecb81d62 100644 --- a/backend/src/init.rs +++ b/backend/src/init.rs @@ -143,9 +143,9 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error .set( &mut handle, ServerStatus { - backing_up: false, updated: false, update_progress: None, + backup_progress: None, }, ) .await?; diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts index 20a4528cc..7660adf73 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -32,7 +32,7 @@ const ICONS = [ 'desktop-outline', 'download-outline', 'earth-outline', - 'ellipsis-horizontal-outline', + 'ellipsis-horizontal', 'eye-off-outline', 'eye-outline', 'file-tray-stacked-outline', diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.html b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.html index 1e7396e32..ffa097fd1 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.html +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.html @@ -47,11 +47,7 @@ - + Hostname: {{ cifs.hostname }}

Path: {{ cifs.path }}

+ + +
diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss index e69de29bb..36da157ea 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss @@ -0,0 +1,18 @@ +.click-area { + padding: 50px; + + + &:hover { + background-color: var(--ion-color-medium-tint); + } + + ion-icon { + font-size: 27px; + } +} + +@media (max-width: 1000px) { + .click-area { + padding: 18px 0px 10px; + } +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index 926abbba6..3d73d49a6 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -89,9 +89,12 @@ export class BackupDrivesComponent { } async presentActionCifs( + event: Event, target: MappedBackupTarget, index: number, ): Promise { + event.stopPropagation() + const entry = target.entry as CifsBackupTarget const action = await this.actionCtrl.create({ @@ -114,17 +117,6 @@ export class BackupDrivesComponent { this.presentModalEditCifs(target.id, entry, index) }, }, - { - text: - this.type === 'create' ? 'Create Backup' : 'Restore From Backup', - icon: - this.type === 'create' - ? 'cloud-upload-outline' - : 'cloud-download-outline', - handler: () => { - this.select(target) - }, - }, ], }) diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts b/frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts new file mode 100644 index 000000000..be840eff2 --- /dev/null +++ b/frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { BackupSelectPage } from './backup-select.page' +import { FormsModule } from '@angular/forms' + +@NgModule({ + declarations: [BackupSelectPage], + imports: [CommonModule, IonicModule, FormsModule], + exports: [BackupSelectPage], +}) +export class BackupSelectPageModule {} diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html new file mode 100644 index 000000000..8ad5ff221 --- /dev/null +++ b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html @@ -0,0 +1,45 @@ + + + Select Services to Back Up + + + + + + + + + + + + + + + +

{{ pkg.title }}

+
+ +
+
+
+ + + + + + Back Up Selected + + + + diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.scss b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts new file mode 100644 index 000000000..78a2de8e3 --- /dev/null +++ b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core' +import { ModalController, IonicSafeString } from '@ionic/angular' +import { map, take } from 'rxjs/operators' +import { PackageState } from 'src/app/services/patch-db/data-model' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' + +@Component({ + selector: 'backup-select', + templateUrl: './backup-select.page.html', + styleUrls: ['./backup-select.page.scss'], +}) +export class BackupSelectPage { + hasSelection = false + error: string | IonicSafeString = '' + pkgs: { + id: string + title: string + icon: string + disabled: boolean + checked: boolean + }[] = [] + + constructor( + private readonly modalCtrl: ModalController, + private readonly patch: PatchDbService, + ) {} + + ngOnInit() { + this.patch + .watch$('package-data') + .pipe( + map(pkgs => { + return Object.values(pkgs).map(pkg => { + const { id, title } = pkg.manifest + return { + id, + title, + icon: pkg['static-files'].icon, + disabled: pkg.state !== PackageState.Installed, + checked: pkg.state === PackageState.Installed, + } + }) + }), + take(1), + ) + .subscribe(pkgs => (this.pkgs = pkgs)) + } + + dismiss(success = false) { + if (success) { + const ids = this.pkgs.filter(p => p.checked).map(p => p.id) + this.modalCtrl.dismiss(ids) + } else { + this.modalCtrl.dismiss() + } + } + + handleChange() { + this.hasSelection = this.pkgs.some(p => p.checked) + } +} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html index 4fecacf35..e00330448 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html @@ -31,7 +31,7 @@ fill="clear" (click)="presentAction(entry.key, $event)" > - + diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts index b08d56900..992ce934c 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts @@ -5,7 +5,7 @@ import { ModalController, } from '@ionic/angular' import { ActionSheetButton } from '@ionic/core' -import { ErrorToastService } from '@start9labs/shared' +import { DestroyService, ErrorToastService } from '@start9labs/shared' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ValueSpecObject } from 'src/app/pkg-config/config-types' @@ -15,7 +15,12 @@ import { v4 } from 'uuid' import { UIMarketplaceData } from '../../../services/patch-db/data-model' import { ConfigService } from '../../../services/config.service' import { MarketplaceService } from 'src/app/services/marketplace.service' -import { distinctUntilChanged, finalize, first } from 'rxjs/operators' +import { + distinctUntilChanged, + finalize, + first, + takeUntil, +} from 'rxjs/operators' type Marketplaces = { id: string | undefined @@ -27,6 +32,7 @@ type Marketplaces = { selector: 'marketplaces', templateUrl: 'marketplaces.page.html', styleUrls: ['marketplaces.page.scss'], + providers: [DestroyService], }) export class MarketplacesPage { selectedId: string | undefined @@ -42,12 +48,13 @@ export class MarketplacesPage { private readonly marketplaceService: MarketplaceService, private readonly config: ConfigService, public readonly patch: PatchDbService, + private readonly destroy$: DestroyService, ) {} ngOnInit() { this.patch .watch$('ui', 'marketplace') - .pipe(distinctUntilChanged()) + .pipe(distinctUntilChanged(), takeUntil(this.destroy$)) .subscribe((mp: UIMarketplaceData | undefined) => { let marketplaces: Marketplaces = [ { diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html new file mode 100644 index 000000000..536043f69 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html @@ -0,0 +1,63 @@ + + + + + + Backup Progress + + + + + + + + + + + + + + {{ pkg.value.manifest.title }} + + + +   + Complete + + + + + + + + Backing up + + + + Waiting... + + + + + + + + + + diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts new file mode 100644 index 000000000..fb50d09e4 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts @@ -0,0 +1,50 @@ +import { + ChangeDetectionStrategy, + Component, + Pipe, + PipeTransform, +} from '@angular/core' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { take } from 'rxjs/operators' +import { PackageMainStatus } from 'src/app/services/patch-db/data-model' +import { EOSService } from 'src/app/services/eos.service' +import { Observable } from 'rxjs' + +@Component({ + selector: 'backing-up', + templateUrl: './backing-up.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BackingUpComponent { + readonly pkgs$ = this.patch.watch$('package-data').pipe(take(1)) + readonly backupProgress$ = this.patch.watch$( + 'server-info', + 'status-info', + 'backup-progress', + ) + + PackageMainStatus = PackageMainStatus + + constructor( + public readonly eosService: EOSService, + public readonly patch: PatchDbService, + ) {} +} + +@Pipe({ + name: 'pkgMainStatus', +}) +export class PkgMainStatusPipe implements PipeTransform { + transform(pkgId: string): Observable { + return this.patch.watch$( + 'package-data', + pkgId, + 'installed', + 'status', + 'main', + 'status', + ) + } + + constructor(private readonly patch: PatchDbService) {} +} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts index d4262392d..6a1782985 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts @@ -2,9 +2,12 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { ServerBackupPage } from './server-backup.page' +import { BackingUpComponent } from './backing-up/backing-up.component' import { RouterModule, Routes } from '@angular/router' import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module' import { SharedPipesModule } from '@start9labs/shared' +import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module' +import { PkgMainStatusPipe } from './backing-up/backing-up.component' const routes: Routes = [ { @@ -20,7 +23,8 @@ const routes: Routes = [ RouterModule.forChild(routes), SharedPipesModule, BackupDrivesComponentModule, + BackupSelectPageModule, ], - declarations: [ServerBackupPage], + declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe], }) export class ServerBackupPageModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html index 3b201d3e2..c4576f3f1 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html @@ -1,55 +1,16 @@ + + + - + - + - - - - - - - - - - - Backup Progress - - - - - - - - - - - - - - {{ pkg.entry.manifest.title }} - - - - -   - Complete - - - - - Backing up - - - - Waiting... - - - - - - - - - + diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index 559e931b7..1bfd479b7 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -10,78 +10,73 @@ import { GenericInputOptions, } from 'src/app/modals/generic-input/generic-input.component' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' -import { Subscription } from 'rxjs' -import { take } from 'rxjs/operators' +import { skip, takeUntil } from 'rxjs/operators' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' -import { - PackageDataEntry, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' import * as argon2 from '@start9labs/argon2' import { CifsBackupTarget, DiskBackupTarget, } from 'src/app/services/api/api.types' +import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.page' +import { EOSService } from 'src/app/services/eos.service' +import { DestroyService } from '@start9labs/shared' @Component({ selector: 'server-backup', templateUrl: './server-backup.page.html', styleUrls: ['./server-backup.page.scss'], + providers: [DestroyService], }) export class ServerBackupPage { - backingUp = false - pkgs: PkgInfo[] = [] - subs: Subscription[] + target: MappedBackupTarget + serviceIds: string[] = [] + + readonly backingUp$ = this.eosService.backingUp$ constructor( private readonly loadingCtrl: LoadingController, private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, - private readonly patch: PatchDbService, private readonly navCtrl: NavController, + private readonly destroy$: DestroyService, + private readonly eosService: EOSService, + private readonly patch: PatchDbService, ) {} ngOnInit() { - this.subs = [ - this.patch - .watch$('server-info', 'status-info', 'backing-up') - .pipe() - .subscribe(isBackingUp => { - if (isBackingUp) { - if (!this.backingUp) { - this.backingUp = true - this.subscribeToBackup() - } - } else { - if (this.backingUp) { - this.backingUp = false - this.pkgs.forEach(pkg => pkg.sub?.unsubscribe()) - this.navCtrl.navigateRoot('/embassy') - } - } - }), - ] + this.backingUp$ + .pipe(skip(1), takeUntil(this.destroy$)) + .subscribe(isBackingUp => { + if (!isBackingUp) { + this.navCtrl.navigateRoot('/embassy') + } + }) } - ngOnDestroy() { - this.subs.forEach(sub => sub.unsubscribe()) - this.pkgs.forEach(pkg => pkg.sub?.unsubscribe()) - } - - async presentModalPassword( + async presentModalSelect( target: MappedBackupTarget, - ): Promise { - let message = - 'Enter your master password to create an encrypted backup of your Embassy and all its services.' - if (!target.hasValidBackup) { - message = - message + - ' Since this is a fresh backup, it could take a while. Future backups will likely be much faster.' - } + ) { + this.target = target + const modal = await this.modalCtrl.create({ + presentingElement: await this.modalCtrl.getTop(), + component: BackupSelectPage, + }) + + modal.onWillDismiss().then(res => { + if (res.data) { + this.serviceIds = res.data + this.presentModalPassword() + } + }) + + await modal.present() + } + + async presentModalPassword(): Promise { const options: GenericInputOptions = { title: 'Master Password Needed', - message, + message: 'Enter your master password to encrypt this backup.', label: 'Master Password', placeholder: 'Enter master password', useMask: true, @@ -93,23 +88,20 @@ export class ServerBackupPage { argon2.verify(passwordHash, password) // first time backup - if (!target.hasValidBackup) { - await this.createBackup(target.id, password) + if (!this.target.hasValidBackup) { + await this.createBackup(password) // existing backup } else { try { const passwordHash = - target.entry['embassy-os']?.['password-hash'] || '' + this.target.entry['embassy-os']?.['password-hash'] || '' argon2.verify(passwordHash, password) } catch { - setTimeout( - () => this.presentModalOldPassword(target, password), - 500, - ) + setTimeout(() => this.presentModalOldPassword(password), 500) return } - await this.createBackup(target.id, password) + await this.createBackup(password) } }, } @@ -123,10 +115,7 @@ export class ServerBackupPage { await m.present() } - private async presentModalOldPassword( - target: MappedBackupTarget, - password: string, - ): Promise { + private async presentModalOldPassword(password: string): Promise { const options: GenericInputOptions = { title: 'Original Password Needed', message: @@ -136,10 +125,11 @@ export class ServerBackupPage { useMask: true, buttonText: 'Create Backup', submitFn: async (oldPassword: string) => { - const passwordHash = target.entry['embassy-os']?.['password-hash'] || '' + const passwordHash = + this.target.entry['embassy-os']?.['password-hash'] || '' argon2.verify(passwordHash, oldPassword) - await this.createBackup(target.id, password, oldPassword) + await this.createBackup(password, oldPassword) }, } @@ -153,7 +143,6 @@ export class ServerBackupPage { } private async createBackup( - id: string, password: string, oldPassword?: string, ): Promise { @@ -164,7 +153,8 @@ export class ServerBackupPage { try { await this.embassyApi.createBackup({ - 'target-id': id, + 'target-id': this.target.id, + 'package-ids': this.serviceIds, 'old-password': oldPassword || null, password, }) @@ -172,53 +162,4 @@ export class ServerBackupPage { loader.dismiss() } } - - private subscribeToBackup() { - this.patch - .watch$('package-data') - .pipe(take(1)) - .subscribe(pkgs => { - const pkgArr = Object.keys(pkgs) - .sort() - .map(key => pkgs[key]) - const activeIndex = pkgArr.findIndex( - pkg => - pkg.installed?.status.main.status === PackageMainStatus.BackingUp, - ) - - this.pkgs = pkgArr.map((pkg, i) => ({ - entry: pkg, - active: i === activeIndex, - complete: i < activeIndex, - })) - - // subscribe to pkg - this.pkgs.forEach(pkg => { - pkg.sub = this.patch - .watch$( - 'package-data', - pkg.entry.manifest.id, - 'installed', - 'status', - 'main', - 'status', - ) - .subscribe(status => { - if (status === PackageMainStatus.BackingUp) { - pkg.active = true - } else if (pkg.active) { - pkg.active = false - pkg.complete = true - } - }) - }) - }) - } -} - -interface PkgInfo { - entry: PackageDataEntry - active: boolean - complete: boolean - sub?: Subscription } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index 7ddbc3f36..2468a1507 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -52,17 +52,17 @@ Last Backup: {{ server['last-backup'] ? (server['last-backup'] | date: 'short') : 'never' }} - + - Backing up + Backing up

diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index 6dba316b7..703301aa8 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -20,7 +20,7 @@ import { MarketplacePkg } from '@start9labs/marketplace' export module Mock { export const ServerUpdated: ServerStatusInfo = { - 'backing-up': false, + 'backup-progress': null, 'update-progress': null, updated: true, } diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index b1f8a45e1..0166dfc9a 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -148,6 +148,7 @@ export module RR { export type CreateBackupReq = WithExpire<{ // backup.create 'target-id': string + 'package-ids': string[] 'old-password': string | null password: string }> diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 0c859a621..99173f9bf 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -392,12 +392,13 @@ export class MockApiService extends ApiService { params: RR.CreateBackupReq, ): Promise { await pauseFor(2000) - const path = '/server-info/status-info/backing-up' - const ids = ['bitcoind', 'lnd'] + const path = '/server-info/status-info/backup-progress' + const ids = params['package-ids'] setTimeout(async () => { for (let i = 0; i < ids.length; i++) { - const appPath = `/package-data/${ids[i]}/installed/status/main/status` + const id = ids[i] + const appPath = `/package-data/${id}/installed/status/main/status` const appPatch = [ { op: PatchOp.REPLACE, @@ -409,13 +410,19 @@ export class MockApiService extends ApiService { await pauseFor(8000) - const newPatch = [ + this.updateMock([ { ...appPatch[0], value: PackageMainStatus.Stopped, }, - ] - this.updateMock(newPatch) + ]) + this.updateMock([ + { + op: PatchOp.REPLACE, + path: `${path}/${id}/complete`, + value: true, + }, + ]) } await pauseFor(1000) @@ -425,7 +432,7 @@ export class MockApiService extends ApiService { { op: PatchOp.REPLACE, path, - value: false, + value: null, }, ] this.updateMock(lastPatch) @@ -435,7 +442,12 @@ export class MockApiService extends ApiService { { op: PatchOp.REPLACE, path, - value: true, + value: ids.reduce((acc, val) => { + return { + ...acc, + [val]: { complete: false }, + } + }, {}), }, ] diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 26d3a0763..e1954f749 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -27,7 +27,7 @@ export const mockPatchData: DataModel = { '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', 'eos-version-compat': '>=0.3.0 <=0.3.0.1', 'status-info': { - 'backing-up': false, + 'backup-progress': null, updated: false, 'update-progress': null, }, diff --git a/frontend/projects/ui/src/app/services/eos.service.ts b/frontend/projects/ui/src/app/services/eos.service.ts index b429145f6..a1907291d 100644 --- a/frontend/projects/ui/src/app/services/eos.service.ts +++ b/frontend/projects/ui/src/app/services/eos.service.ts @@ -4,7 +4,7 @@ import { MarketplaceEOS } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { Emver } from '@start9labs/shared' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' -import { map } from 'rxjs/operators' +import { distinctUntilChanged, map } from 'rxjs/operators' @Injectable({ providedIn: 'root', @@ -15,15 +15,17 @@ export class EOSService { readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe( map(status => { - return status && (!!status['update-progress'] || status.updated) + return !!status['update-progress'] || status.updated }), + distinctUntilChanged(), ) - readonly backingUp$ = this.patch.watch$( - 'server-info', - 'status-info', - 'backing-up', - ) + readonly backingUp$ = this.patch + .watch$('server-info', 'status-info', 'backup-progress') + .pipe( + map(obj => !!obj), + distinctUntilChanged(), + ) readonly updatingOrBackingUp$ = combineLatest([ this.updating$, diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 367b9c40f..c3a9013bb 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -59,7 +59,11 @@ export interface ServerInfo { } export interface ServerStatusInfo { - 'backing-up': boolean + 'backup-progress': null | { + [packageId: string]: { + complete: boolean + } + } updated: boolean 'update-progress': { size: number | null; downloaded: number } | null }