diff --git a/backend/src/setup.rs b/backend/src/setup.rs index 479441c6f..4f65a9062 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -1,23 +1,15 @@ -use std::collections::BTreeMap; -use std::os::unix::prelude::MetadataExt; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; use color_eyre::eyre::eyre; -use digest::generic_array::GenericArray; -use digest::OutputSizeUser; use futures::future::BoxFuture; -use futures::{FutureExt, TryFutureExt, TryStreamExt}; +use futures::{StreamExt, TryFutureExt}; use josekit::jwk::Jwk; -use nix::unistd::{Gid, Uid}; use openssl::x509::X509; -use patch_db::{DbHandle, LockType}; +use patch_db::DbHandle; use rpc_toolkit::command; use rpc_toolkit::yajrc::RpcError; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use sqlx::{Connection, Executor, Postgres}; use tokio::fs::File; use tokio::io::AsyncWriteExt; @@ -29,27 +21,20 @@ use crate::backup::target::BackupTargetFS; use crate::context::rpc::RpcContextConfig; use crate::context::setup::SetupResult; use crate::context::SetupContext; -use crate::db::model::RecoveredPackageInfo; use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; -use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::guard::TmpMountGuard; use crate::disk::util::{pvscan, recovery_info, DiskInfo, EmbassyOsRecoveryInfo}; use crate::disk::REPAIR_DISK_PATH; use crate::hostname::{get_hostname, HostNameReceipt, Hostname}; -use crate::id::Id; use crate::init::init; -use crate::install::PKG_PUBLIC_DIR; use crate::middleware::encrypt::EncryptedWire; use crate::net::ssl::SslManager; -use crate::s9pk::manifest::PackageId; use crate::sound::BEETHOVEN; -use crate::util::io::{dir_size, from_yaml_async_reader}; -use crate::util::Version; -use crate::volume::{data_dir, VolumeId}; -use crate::{ensure_code, Error, ErrorKind, ResultExt}; +use crate::util::rsync::{Rsync, RsyncOptions}; +use crate::{Error, ErrorKind, ResultExt}; #[instrument(skip(secrets))] pub async fn password_hash(secrets: &mut Ex) -> Result @@ -92,6 +77,44 @@ pub async fn list_disks(#[context] ctx: SetupContext) -> Result, E crate::disk::util::list(&ctx.os_partitions).await } +async fn setup_init( + ctx: &SetupContext, + password: Option, +) -> Result<(Hostname, OnionAddressV3, X509), Error> { + init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; + let secrets = ctx.secret_store().await?; + let db = ctx.db(&secrets).await?; + let mut secrets_handle = secrets.acquire().await?; + let mut db_handle = db.handle(); + let mut secrets_tx = secrets_handle.begin().await?; + let mut db_tx = db_handle.begin().await?; + + if let Some(password) = password { + let set_password_receipt = crate::auth::SetPasswordReceipt::new(&mut db_tx).await?; + crate::auth::set_password( + &mut db_tx, + &set_password_receipt, + &mut secrets_tx, + &password, + ) + .await?; + } + + let tor_key = crate::net::tor::os_key(&mut secrets_tx).await?; + + db_tx.commit().await?; + secrets_tx.commit().await?; + + let hostname_receipts = HostNameReceipt::new(&mut db_handle).await?; + let hostname = get_hostname(&mut db_handle, &hostname_receipts).await?; + + let (_, root_ca) = SslManager::init(secrets, &mut db_handle) + .await? + .export_root_ca() + .await?; + Ok((hostname, tor_key.public().get_onion_address(), root_ca)) +} + #[command(rpc_only)] pub async fn attach( #[context] ctx: SetupContext, @@ -135,39 +158,9 @@ pub async fn attach( ErrorKind::DiskManagement, )); } - init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; - let secrets = ctx.secret_store().await?; - let db = ctx.db(&secrets).await?; - let mut secrets_handle = secrets.acquire().await?; - let mut db_handle = db.handle(); - let mut secrets_tx = secrets_handle.begin().await?; - let mut db_tx = db_handle.begin().await?; - - if let Some(password) = password { - let set_password_receipt = crate::auth::SetPasswordReceipt::new(&mut db_tx).await?; - crate::auth::set_password( - &mut db_tx, - &set_password_receipt, - &mut secrets_tx, - &password, - ) - .await?; - } - - let tor_key = crate::net::tor::os_key(&mut secrets_tx).await?; - - db_tx.commit().await?; - secrets_tx.commit().await?; - - let hostname_receipts = HostNameReceipt::new(&mut db_handle).await?; - let hostname = get_hostname(&mut db_handle, &hostname_receipts).await?; - - let (_, root_ca) = SslManager::init(secrets, &mut db_handle) - .await? - .export_root_ca() - .await?; + let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; let setup_result = SetupResult { - tor_address: format!("http://{}", tor_key.public().get_onion_address()), + tor_address: format!("http://{}", tor_addr), lan_address: hostname.lan_address(), root_ca: String::from_utf8(root_ca.to_pem()?)?, }; @@ -175,29 +168,11 @@ pub async fn attach( Ok(setup_result) } -#[command(subcommands(v2, recovery_status))] +#[command(subcommands(recovery_status))] pub fn recovery() -> Result<(), Error> { Ok(()) } -#[command(subcommands(set))] -pub fn v2() -> Result<(), Error> { - Ok(()) -} - -#[command(rpc_only, metadata(authenticated = false))] -pub async fn set(#[context] ctx: SetupContext, #[arg] logicalname: PathBuf) -> Result<(), Error> { - let guard = TmpMountGuard::mount(&BlockDev::new(&logicalname), ReadOnly).await?; - let product_key = tokio::fs::read_to_string(guard.as_ref().join("root/agent/product_key")) - .await? - .trim() - .to_owned(); - guard.unmount().await?; - *ctx.cached_product_key.write().await = Some(Arc::new(product_key)); - *ctx.selected_v2_drive.write().await = Some(logicalname); - Ok(()) -} - #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct RecoveryStatus { @@ -253,19 +228,27 @@ pub async fn verify_cifs( embassy_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound)) } +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "kebab-case")] +pub enum RecoverySource { + Migrate { guid: String }, + Backup { target: BackupTargetFS }, +} + #[command(rpc_only)] pub async fn execute( #[context] ctx: SetupContext, #[arg(rename = "embassy-logicalname")] embassy_logicalname: PathBuf, #[arg(rename = "embassy-password")] embassy_password: EncryptedWire, - #[arg(rename = "recovery-source")] mut recovery_source: Option, + #[arg(rename = "recovery-source")] recovery_source: Option, #[arg(rename = "recovery-password")] recovery_password: Option, ) -> Result { let embassy_password = match embassy_password.decrypt(&*ctx) { Some(a) => a, None => { return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode embassy_password"), + color_eyre::eyre::eyre!("Couldn't decode embassy-password"), crate::ErrorKind::Unknown, )) } @@ -275,16 +258,13 @@ pub async fn execute( Some(a) => Some(a), None => { return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode recovery_password"), + color_eyre::eyre::eyre!("Couldn't decode recovery-password"), crate::ErrorKind::Unknown, )) } }, None => None, }; - if let Some(v2_drive) = &*ctx.selected_v2_drive.read().await { - recovery_source = Some(BackupTargetFS::Disk(BlockDev::new(v2_drive.clone()))) - } match execute_inner( ctx.clone(), embassy_logicalname, @@ -343,7 +323,7 @@ pub async fn execute_inner( ctx: SetupContext, embassy_logicalname: PathBuf, embassy_password: String, - recovery_source: Option, + recovery_source: Option, recovery_password: Option, ) -> Result<(Hostname, OnionAddressV3, X509), Error> { if ctx.recovery_status.read().await.is_some() { @@ -369,12 +349,12 @@ pub async fn execute_inner( ) .await?; - let res = if let Some(recovery_source) = recovery_source { + let res = if let Some(RecoverySource::Backup { target }) = recovery_source { let (tor_addr, root_ca, recover_fut) = recover( ctx.clone(), guid.clone(), embassy_password, - recovery_source, + target, recovery_password, ) .await?; @@ -414,6 +394,87 @@ pub async fn execute_inner( } }); res + } else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { + let _ = crate::disk::main::mount_fs( + &old_guid, + "/media/embassy/migrate", + "main", + RepairStrategy::Preen, + DEFAULT_PASSWORD, + ) + .await?; + Rsync::new( + "/media/embassy/migrate/main", + "/embassy-data/main", + RsyncOptions { + delete: true, + force: true, + ignore_existing: false, + }, + )? + .wait() + .await?; + let _ = crate::disk::main::mount_fs( + &old_guid, + "/media/embassy/migrate", + "package-data", + RepairStrategy::Preen, + DEFAULT_PASSWORD, + ) + .await?; + let mut package_data_transfer = Rsync::new( + "/media/embassy/migrate/package-data", + "/embassy-data/package-data", + RsyncOptions { + delete: true, + force: true, + ignore_existing: false, + }, + )?; + *ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { + bytes_transferred: 0, + total_bytes: 100, + complete: false, + })); + let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(embassy_password)).await?; + let res = (hostname.clone(), tor_addr.clone(), root_ca.clone()); + tokio::spawn(async move { + if let Err(e) = async { + while let Some(progress) = package_data_transfer.progress.next().await { + *ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { + bytes_transferred: (progress * 100.0) as u64, + total_bytes: 100, + complete: false, + })); + } + package_data_transfer.wait().await?; + Ok::<_, Error>(()) + } + .and_then(|_| async { + *ctx.setup_result.write().await = Some(( + guid, + SetupResult { + tor_address: format!("http://{}", tor_addr), + lan_address: hostname.lan_address(), + root_ca: String::from_utf8(root_ca.to_pem()?)?, + }, + )); + if let Some(Ok(recovery_status)) = &mut *ctx.recovery_status.write().await { + recovery_status.complete = true; + } + Ok(()) + }) + .await + { + (&BEETHOVEN).play().await.unwrap_or_default(); // ignore error in playing the song + tracing::error!("Error recovering drive!: {}", e); + tracing::debug!("{:?}", e); + *ctx.recovery_status.write().await = Some(Err(e.into())); + } else { + tracing::info!("Recovery Complete!"); + } + }); + res } else { let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?; let db = init(&RpcContextConfig::load(ctx.config_path.clone()).await?) @@ -475,385 +536,12 @@ async fn recover( recovery_password: Option, ) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadOnly).await?; - let recovery_version = recovery_info(&recovery_source) - .await? - .as_ref() - .map(|i| i.version.clone()) - .unwrap_or_else(|| emver::Version::new(0, 2, 0, 0).into()); - let res = if recovery_version.major() == 0 && recovery_version.minor() == 2 { - recover_v2(ctx.clone(), &embassy_password, recovery_source).await? - } else if recovery_version.major() == 0 && recovery_version.minor() == 3 { - recover_full_embassy( - ctx.clone(), - guid.clone(), - embassy_password, - recovery_source, - recovery_password, - ) - .await? - } else { - return Err(Error::new( - eyre!("Unsupported version of embassyOS: {}", recovery_version), - crate::ErrorKind::VersionIncompatible, - )); - }; - - Ok(res) -} - -async fn shasum( - path: impl AsRef, -) -> Result::OutputSize>, Error> { - use tokio::io::AsyncReadExt; - - let mut rdr = tokio::fs::File::open(path).await?; - let mut hasher = Sha256::new(); - let mut buf = [0; 1024]; - let mut read; - while { - read = rdr.read(&mut buf).await?; - read != 0 - } { - hasher.update(&buf[0..read]); - } - Ok(hasher.finalize()) -} - -async fn validated_copy(src: impl AsRef, dst: impl AsRef) -> Result<(), Error> { - let src_path = src.as_ref(); - let dst_path = dst.as_ref(); - tokio::fs::copy(src_path, dst_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("cp {} -> {}", src_path.display(), dst_path.display()), - ) - })?; - let (src_hash, dst_hash) = tokio::try_join!(shasum(src_path), shasum(dst_path))?; - if src_hash != dst_hash { - Err(Error::new( - eyre!( - "source hash does not match destination hash for {}", - dst_path.display() - ), - crate::ErrorKind::Filesystem, - )) - } else { - Ok(()) - } -} - -fn dir_copy<'a, P0: AsRef + 'a + Send + Sync, P1: AsRef + 'a + Send + Sync>( - src: P0, - dst: P1, - ctr: &'a AtomicU64, -) -> BoxFuture<'a, Result<(), Error>> { - async move { - let m = tokio::fs::metadata(&src).await?; - let dst_path = dst.as_ref(); - tokio::fs::create_dir_all(&dst_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("mkdir {}", dst_path.display()), - ) - })?; - tokio::fs::set_permissions(&dst_path, m.permissions()) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("chmod {}", dst_path.display()), - ) - })?; - let tmp_dst_path = dst_path.to_owned(); - tokio::task::spawn_blocking(move || { - nix::unistd::chown( - &tmp_dst_path, - Some(Uid::from_raw(m.uid())), - Some(Gid::from_raw(m.gid())), - ) - }) - .await - .with_kind(crate::ErrorKind::Unknown)? - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("chown {}", dst_path.display()), - ) - })?; - tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(src.as_ref()).await?) - .map_err(|e| Error::new(e, crate::ErrorKind::Filesystem)) - .try_for_each(|e| async move { - let m = e.metadata().await?; - let src_path = e.path(); - let dst_path = dst_path.join(e.file_name()); - if m.is_file() { - let len = m.len(); - let mut cp_res = Ok(()); - for _ in 0..10 { - cp_res = validated_copy(&src_path, &dst_path).await; - if cp_res.is_ok() { - break; - } - } - cp_res?; - let tmp_dst_path = dst_path.clone(); - tokio::task::spawn_blocking(move || { - nix::unistd::chown( - &tmp_dst_path, - Some(Uid::from_raw(m.uid())), - Some(Gid::from_raw(m.gid())), - ) - }) - .await - .with_kind(crate::ErrorKind::Unknown)? - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("chown {}", dst_path.display()), - ) - })?; - ctr.fetch_add(len, Ordering::Relaxed); - } else if m.is_dir() { - dir_copy(src_path, dst_path, ctr).await?; - } else if m.file_type().is_symlink() { - tokio::fs::symlink( - tokio::fs::read_link(&src_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("readlink {}", src_path.display()), - ) - })?, - &dst_path, - ) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!("cp -P {} -> {}", src_path.display(), dst_path.display()), - ) - })?; - // Do not set permissions (see https://unix.stackexchange.com/questions/87200/change-permissions-for-a-symbolic-link) - } - Ok(()) - }) - .await?; - Ok(()) - } - .boxed() -} - -#[instrument(skip(ctx))] -async fn recover_v2( - ctx: SetupContext, - embassy_password: &str, - recovery_source: TmpMountGuard, -) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> { - let secret_store = ctx.secret_store().await?; - - // migrate the root CA - let root_ca_key_path = recovery_source - .as_ref() - .join("root") - .join("agent") - .join("ca") - .join("private") - .join("embassy-root-ca.key.pem"); - let root_ca_cert_path = recovery_source - .as_ref() - .join("root") - .join("agent") - .join("ca") - .join("certs") - .join("embassy-root-ca.cert.pem"); - let (root_ca_key_bytes, root_ca_cert_bytes) = tokio::try_join!( - tokio::fs::read(root_ca_key_path), - tokio::fs::read(root_ca_cert_path) - )?; - let root_ca_key = openssl::pkey::PKey::private_key_from_pem(&root_ca_key_bytes)?; - let root_ca_cert = openssl::x509::X509::from_pem(&root_ca_cert_bytes)?; - crate::net::ssl::SslManager::import_root_ca( - secret_store.clone(), - root_ca_key, - root_ca_cert.clone(), + recover_full_embassy( + ctx.clone(), + guid.clone(), + embassy_password, + recovery_source, + recovery_password, ) - .await?; - - // migrate the tor address - let tor_key_path = recovery_source - .as_ref() - .join("var") - .join("lib") - .join("tor") - .join("agent") - .join("hs_ed25519_secret_key"); - let tor_key_bytes = tokio::fs::read(tor_key_path).await?; - let mut tor_key_array_tmp = [0u8; 64]; - tor_key_array_tmp.clone_from_slice(&tor_key_bytes[32..]); - let tor_key: TorSecretKeyV3 = tor_key_array_tmp.into(); - let key_vec = tor_key.as_bytes().to_vec(); - let password = argon2::hash_encoded( - embassy_password.as_bytes(), - &rand::random::<[u8; 16]>()[..], - &argon2::Config::default(), - ) - .with_kind(crate::ErrorKind::PasswordHashGeneration)?; - let sqlite_pool = ctx.secret_store().await?; - sqlx::query!( - "INSERT INTO account (id, password, tor_key) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3", - 0, - password, - key_vec - ) - .execute(&mut sqlite_pool.acquire().await?) - .await?; - - // rest of migration as future - let fut = async move { - let db = ctx.db(&secret_store).await?; - let mut handle = db.handle(); - // lock everything to avoid issues with renamed packages (bitwarden) - crate::db::DatabaseModel::new() - .lock(&mut handle, LockType::Write) - .await?; - - let apps_yaml_path = recovery_source - .as_ref() - .join("root") - .join("appmgr") - .join("apps.yaml"); - #[derive(Deserialize)] - struct LegacyAppInfo { - title: String, - version: Version, - } - let packages: BTreeMap = - from_yaml_async_reader(File::open(&apps_yaml_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - apps_yaml_path.display().to_string(), - ) - })?) - .await?; - - let volume_path = recovery_source.as_ref().join("root/volumes"); - let mut total_bytes = 0; - for (pkg_id, _) in &packages { - let volume_src_path = volume_path.join(&pkg_id); - total_bytes += dir_size(&volume_src_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - volume_src_path.display().to_string(), - ) - })?; - } - *ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { - bytes_transferred: 0, - total_bytes, - complete: false, - })); - let bytes_transferred = AtomicU64::new(0); - let volume_id = VolumeId::Custom(Id::try_from("main".to_owned())?); - for (pkg_id, info) in packages { - let (src_id, dst_id) = rename_pkg_id(pkg_id); - let volume_src_path = volume_path.join(&src_id); - let volume_dst_path = data_dir(&ctx.datadir, &dst_id, &volume_id); - tokio::select!( - res = dir_copy( - &volume_src_path, - &volume_dst_path, - &bytes_transferred - ) => res?, - _ = async { - loop { - tokio::time::sleep(Duration::from_secs(1)).await; - *ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { - bytes_transferred: bytes_transferred.load(Ordering::Relaxed), - total_bytes, - complete: false - })); - } - } => (), - ); - let tor_src_path = recovery_source - .as_ref() - .join("var/lib/tor") - .join(format!("app-{}", src_id)) - .join("hs_ed25519_secret_key"); - let key_vec = tokio::fs::read(&tor_src_path).await.with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - tor_src_path.display().to_string(), - ) - })?; - ensure_code!( - key_vec.len() == 96, - crate::ErrorKind::Tor, - "{} not 96 bytes", - tor_src_path.display() - ); - let key_vec = key_vec[32..].to_vec(); - sqlx::query!( - "INSERT INTO tor (package, interface, key) VALUES ($1, 'main', $2) ON CONFLICT (package, interface) DO UPDATE SET key = $2", - *dst_id, - key_vec, - ) - .execute(&mut secret_store.acquire().await?) - .await?; - let icon_leaf = AsRef::::as_ref(&dst_id) - .join(info.version.as_str()) - .join("icon.png"); - let icon_src_path = recovery_source - .as_ref() - .join("root/agent/icons") - .join(format!("{}.png", src_id)); - let icon_dst_path = ctx.datadir.join(PKG_PUBLIC_DIR).join(&icon_leaf); - if let Some(parent) = icon_dst_path.parent() { - tokio::fs::create_dir_all(&parent) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?; - } - tokio::fs::copy(&icon_src_path, &icon_dst_path) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - format!( - "cp {} -> {}", - icon_src_path.display(), - icon_dst_path.display() - ), - ) - })?; - let icon_url = Path::new("/public/package-data").join(&icon_leaf); - crate::db::DatabaseModel::new() - .recovered_packages() - .idx_model(&dst_id) - .put( - &mut handle, - &RecoveredPackageInfo { - title: info.title, - icon: icon_url.display().to_string(), - version: info.version, - }, - ) - .await?; - } - - secret_store.close().await; - recovery_source.unmount().await?; - Ok(()) - }; - Ok(( - tor_key.public().get_onion_address(), - root_ca_cert, - fut.boxed(), - )) -} - -fn rename_pkg_id(src_pkg_id: PackageId) -> (PackageId, PackageId) { - if &*src_pkg_id == "bitwarden" { - (src_pkg_id, "vaultwarden".parse().unwrap()) - } else { - (src_pkg_id.clone(), src_pkg_id) - } + .await } diff --git a/frontend/projects/setup-wizard/src/app/app-routing.module.ts b/frontend/projects/setup-wizard/src/app/app-routing.module.ts index c78bc431c..38f320436 100644 --- a/frontend/projects/setup-wizard/src/app/app-routing.module.ts +++ b/frontend/projects/setup-wizard/src/app/app-routing.module.ts @@ -13,6 +13,13 @@ const routes: Routes = [ loadChildren: () => import('./pages/recover/recover.module').then(m => m.RecoverPageModule), }, + { + path: 'transfer', + loadChildren: () => + import('./pages/transfer/transfer.module').then( + m => m.TransferPageModule, + ), + }, { path: 'embassy', loadChildren: () => diff --git a/frontend/projects/setup-wizard/src/app/app.module.ts b/frontend/projects/setup-wizard/src/app/app.module.ts index 4682f4b89..00ae42d96 100644 --- a/frontend/projects/setup-wizard/src/app/app.module.ts +++ b/frontend/projects/setup-wizard/src/app/app.module.ts @@ -16,6 +16,7 @@ import { SuccessPageModule } from './pages/success/success.module' import { HomePageModule } from './pages/home/home.module' import { LoadingPageModule } from './pages/loading/loading.module' import { RecoverPageModule } from './pages/recover/recover.module' +import { TransferPageModule } from './pages/transfer/transfer.module' import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' const { @@ -37,6 +38,7 @@ const { HomePageModule, LoadingPageModule, RecoverPageModule, + TransferPageModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts index 1cac07533..febc606a9 100644 --- a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts @@ -53,18 +53,7 @@ export class CifsModal { await loader.dismiss() - const is02x = embassyOS.version.startsWith('0.2') - - if (is02x) { - this.modalController.dismiss( - { - cifs: this.cifs, - }, - 'success', - ) - } else { - this.presentModalPassword(embassyOS) - } + this.presentModalPassword(embassyOS) } catch (e) { await loader.dismiss() this.presentAlertFailed() diff --git a/frontend/projects/setup-wizard/src/app/modals/password/password.page.html b/frontend/projects/setup-wizard/src/app/modals/password/password.page.html index c8593e02d..4babb25c6 100644 --- a/frontend/projects/setup-wizard/src/app/modals/password/password.page.html +++ b/frontend/projects/setup-wizard/src/app/modals/password/password.page.html @@ -14,9 +14,6 @@ Choose a password for your Embassy. Make it good. Write it down.

-

- Losing your password can result in total loss of data. -

Enter the password that was used to encrypt this drive. diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.html b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.html index bed70c82d..e26c77fd9 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.html @@ -25,8 +25,8 @@ > No drives found Please connect a storage drive to your Embassy and click - "Refresh".Please connect an external storage drive to your Embassy, if + applicable. Next, click "Refresh". diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 76b552513..3cb6f4483 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -7,12 +7,14 @@ import { } from '@ionic/angular' import { ApiService, + BackupRecoverySource, DiskInfo, DiskRecoverySource, } from 'src/app/services/api/api.service' import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' +import { ActivatedRoute } from '@angular/router' @Component({ selector: 'app-embassy', @@ -31,6 +33,7 @@ export class EmbassyPage { private readonly stateService: StateService, private readonly loadingCtrl: LoadingController, private readonly errorToastService: ErrorToastService, + private route: ActivatedRoute, ) {} async ngOnInit() { @@ -55,8 +58,10 @@ export class EmbassyPage { !d.partitions .map(p => p.logicalname) .includes( - (this.stateService.recoverySource as DiskRecoverySource) - ?.logicalname, + ( + (this.stateService.recoverySource as BackupRecoverySource) + ?.target as DiskRecoverySource + )?.logicalname, ), ) } catch (e: any) { @@ -80,12 +85,14 @@ export class EmbassyPage { { text: 'Continue', handler: () => { + // for backup recoveries if (this.stateService.recoveryPassword) { this.setupEmbassy( drive.logicalname, this.stateService.recoveryPassword, ) } else { + // for migrations and fresh setups this.presentModalPassword(drive.logicalname) } }, @@ -94,9 +101,11 @@ export class EmbassyPage { }) await alert.present() } else { + // for backup recoveries if (this.stateService.recoveryPassword) { this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword) } else { + // for migrations and fresh setups this.presentModalPassword(drive.logicalname) } } @@ -129,7 +138,9 @@ export class EmbassyPage { try { await this.stateService.setupEmbassy(logicalname, password) if (!!this.stateService.recoverySource) { - await this.navCtrl.navigateForward(`/loading`) + await this.navCtrl.navigateForward(`/loading`, { + queryParams: { action: this.route.snapshot.paramMap.get('action') }, + }) } else { await this.navCtrl.navigateForward(`/success`) } diff --git a/frontend/projects/setup-wizard/src/app/pages/home/home.page.html b/frontend/projects/setup-wizard/src/app/pages/home/home.page.html index 6d20433a0..5cb8175b1 100644 --- a/frontend/projects/setup-wizard/src/app/pages/home/home.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/home/home.page.html @@ -1,4 +1,4 @@ - + @@ -23,7 +23,7 @@ - +

Recover

-

- Restore from backup or use an existing Embassy data drive -

+

Recover, restore, or transfer Embassy data

- +

Restore From Backup

-

Recover an Embassy from encrypted backup

+

Recover an Embassy from an encrypted backup

- +

Use Existing Drive @@ -72,6 +78,27 @@

Attach and use a valid Embassy data drive

+ + + +

+ Transfer +

+

+ Transfer data to a new drive
(e.g. upgrade to a + larger drive or an Embassy Pro) +

+
+
diff --git a/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts b/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts index 4440c1c46..7df9d3423 100644 --- a/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts @@ -23,6 +23,7 @@ export class HomePage { swiper?: Swiper guid?: string | null error = false + loaded = false constructor( private readonly api: ApiService, @@ -46,6 +47,7 @@ export class HomePage { } async ionViewDidEnter() { + this.loaded = true // needed to accomodate autoHight="true" on swiper. Otherwise Swiper height might be 0 when navigatging *to* this page from later page. Happens on refresh. if (this.swiper) { this.swiper.allowTouchMove = false } @@ -100,11 +102,3 @@ export class HomePage { } } } - -function decodeHex(hex: string) { - let str = '' - for (let n = 0; n < hex.length; n += 2) { - str += String.fromCharCode(parseInt(hex.substring(n, 2), 16)) - } - return str -} diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html index b9fd8d817..e94b8b329 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -8,7 +8,10 @@ - Recovering + + Transferring + Recovering + Progress: {{ (stateService.dataProgress * 100).toFixed(0) }}% { diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/drive-status.component.html b/frontend/projects/setup-wizard/src/app/pages/recover/drive-status.component.html index 3a62175a8..8fa441241 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/drive-status.component.html +++ b/frontend/projects/setup-wizard/src/app/pages/recover/drive-status.component.html @@ -2,7 +2,7 @@

- {{ is02x ? 'Embassy 0.2.x detected' : 'Embassy backup detected' }} + Embassy backup detected

@@ -11,4 +11,4 @@ No Embassy backup

- \ No newline at end of file + diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.html b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.html index 247f6a89a..8334de7f9 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.html @@ -8,11 +8,7 @@ - Recover - Select the LAN Shared Folder or physical drive containing your - Embassy backup + Restore from Backup @@ -25,17 +21,10 @@ -

LAN Shared Folder

-

- Using a LAN Shared Folder is the recommended way to recover from - backup, since it works with all Embassy hardware configurations. - View the - instructions. +

Network Folder

+

+ Restore your Embassy from a folder on another computer that is + connected to the same network as your Embassy.

@@ -47,7 +36,7 @@ color="light" > - Open New + Open @@ -55,18 +44,20 @@
-

Physical Drives

-

- Warning! Plugging in more than one physical drive to Embassy can - lead to power failure and data corruption. To recover from a - physical drive, please follow the - instructions. -

+

Physical Drive

+
+

+ Restore your Emabssy from a physcial drive that is plugged + directly into your Embassy. +

+
+ + Warning. Do not use this option if you are using a Raspberry + Pi with an external SSD as your main data drive. The Raspberry + Pi cannot not support more than one external drive without + additional power and can cause data corruption. + +
{{ drive.label || drive.logicalname }}

{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts index b0ef6273c..c5ffe5e0a 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -56,7 +56,6 @@ export class RecoverPage { } this.mappedDrives.push({ hasValidBackup: !!p['embassy-os']?.full, - is02x: !!drive['embassy-os']?.version.startsWith('0.2'), drive, }) }) @@ -76,11 +75,14 @@ export class RecoverPage { if (res.role === 'success') { const { hostname, path, username, password } = res.data.cifs this.stateService.recoverySource = { - type: 'cifs', - hostname, - path, - username, - password, + type: 'backup', + target: { + type: 'cifs', + hostname, + path, + username, + password, + }, } this.stateService.recoveryPassword = res.data.recoveryPassword this.navCtrl.navigateForward('/embassy') @@ -90,35 +92,35 @@ export class RecoverPage { } async select(target: DiskBackupTarget) { - const is02x = target['embassy-os']?.version.startsWith('0.2') const { logicalname } = target if (!logicalname) return - if (is02x) { - this.selectRecoverySource(logicalname) - } else { - const modal = await this.modalController.create({ - component: PasswordPage, - componentProps: { target }, - cssClass: 'alertlike-modal', - }) - modal.onDidDismiss().then(res => { - if (res.data?.password) { - this.selectRecoverySource(logicalname, res.data.password) - } - }) - await modal.present() - } + const modal = await this.modalController.create({ + component: PasswordPage, + componentProps: { target }, + cssClass: 'alertlike-modal', + }) + modal.onDidDismiss().then(res => { + if (res.data?.password) { + this.selectRecoverySource(logicalname, res.data.password) + } + }) + await modal.present() } private async selectRecoverySource(logicalname: string, password?: string) { this.stateService.recoverySource = { - type: 'disk', - logicalname, + type: 'backup', + target: { + type: 'disk', + logicalname, + }, } this.stateService.recoveryPassword = password - this.navCtrl.navigateForward(`/embassy`) + this.navCtrl.navigateForward(`/embassy`, { + queryParams: { action: 'recover' }, + }) } } @@ -129,11 +131,9 @@ export class RecoverPage { }) export class DriveStatusComponent { @Input() hasValidBackup!: boolean - @Input() is02x!: boolean } interface MappedDisk { - is02x: boolean hasValidBackup: boolean drive: DiskBackupTarget } diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.html b/frontend/projects/setup-wizard/src/app/pages/success/success.page.html index bacc5d311..bb88267fa 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.html @@ -11,7 +11,7 @@ Setup Complete You have successully claimed your Embassy!You have successfully claimed your Embassy!
@@ -19,11 +19,13 @@

-

- You can now safely unplug your backup drive. +

+ You can now safely unplug your backup drive. + You can now safely unplug your old drive.

Access your Embassy using the methods below. You should @@ -40,7 +42,7 @@

- Visit the address below when you are conncted to the same WiFi + Visit the address below when you are connected to the same WiFi or Local Area Network (LAN) as your Embassy:

@@ -94,7 +96,7 @@ follow the instructions - to downlaod and trust your Embassy's Root Certificate Authority. + to download and trust your Embassy's Root Certificate Authority.

diff --git a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts new file mode 100644 index 000000000..acee2e7f3 --- /dev/null +++ b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { TransferPage } from './transfer.page' + +const routes: Routes = [ + { + path: '', + component: TransferPage, + }, +] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class TransferPageRoutingModule {} diff --git a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts new file mode 100644 index 000000000..e4169d50a --- /dev/null +++ b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { UnitConversionPipesModule } from '@start9labs/shared' +import { TransferPage } from './transfer.page' +import { TransferPageRoutingModule } from './transfer-routing.module' + +@NgModule({ + declarations: [TransferPage], + imports: [ + CommonModule, + IonicModule, + TransferPageRoutingModule, + UnitConversionPipesModule, + ], +}) +export class TransferPageModule {} diff --git a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.html b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.html new file mode 100644 index 000000000..3b533857e --- /dev/null +++ b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.html @@ -0,0 +1,63 @@ + + + + +
+ +
+ + + + Transfer + Select the physical drive containing your previous Embassy + data + + + + + + + + +

Available Drives

+ + + + + +

{{ drive.logicalname }}

+

+ {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || + 'Unknown Model' }} +

+

Capacity: {{ drive.capacity | convertBytes }}

+
+
+
+ + + Refresh + +
+
+
+
+
+
+
diff --git a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.scss b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.scss new file mode 100644 index 000000000..687b91ecf --- /dev/null +++ b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.scss @@ -0,0 +1,4 @@ +.target-label { + font-weight: bold; + padding-bottom: 6px; +} diff --git a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts new file mode 100644 index 000000000..bf2670772 --- /dev/null +++ b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core' +import { AlertController, NavController } from '@ionic/angular' +import { ApiService, DiskInfo } from 'src/app/services/api/api.service' +import { ErrorToastService } from '@start9labs/shared' +import { StateService } from 'src/app/services/state.service' + +@Component({ + selector: 'app-transfer', + templateUrl: 'transfer.page.html', + styleUrls: ['transfer.page.scss'], +}) +export class TransferPage { + loading = true + drives: DiskInfo[] = [] + + constructor( + private readonly apiService: ApiService, + private readonly navCtrl: NavController, + private readonly alertCtrl: AlertController, + private readonly errToastService: ErrorToastService, + private readonly stateService: StateService, + ) {} + + async ngOnInit() { + await this.getDrives() + } + + async refresh() { + this.loading = true + await this.getDrives() + } + + async getDrives() { + try { + const disks = await this.apiService.getDrives() + this.drives = disks.filter(d => d.partitions.length && d.guid) + } catch (e: any) { + this.errToastService.present(e) + } finally { + this.loading = false + } + } + + async select(target: DiskInfo) { + const { logicalname, guid } = target + + if (!logicalname) return + + const alert = await this.alertCtrl.create({ + header: 'Warning', + message: + 'Data from this drive will not be deleted, but you will not be able to use this drive to run embassyOS after the data is transferred. Attempting to use this drive after data is transferred could cause transferred services to not function properly, and may cause data corruption.', + buttons: [ + { + role: 'cancel', + text: 'Cancel', + }, + { + text: 'Continue', + handler: () => { + this.stateService.recoverySource = { + type: 'migrate', + guid: guid!, + } + this.navCtrl.navigateForward(`/embassy`, { + queryParams: { action: 'transfer' }, + }) + }, + }, + ], + }) + await alert.present() + } +} diff --git a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts index 76ac72f5d..161dc8bad 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts @@ -5,7 +5,6 @@ export abstract class ApiService { abstract getStatus(): Promise // setup.status abstract getPubKey(): Promise // setup.get-pubkey abstract getDrives(): Promise // setup.disk.list - abstract set02XDrive(logicalname: string): Promise // setup.recovery.v2.set abstract getRecoveryStatus(): Promise // setup.recovery.status abstract verifyCifs(cifs: CifsRecoverySource): Promise // setup.cifs.verify abstract importDrive(importInfo: ImportDriveReq): Promise // setup.attach @@ -39,7 +38,7 @@ export type ImportDriveReq = { export type SetupEmbassyReq = { 'embassy-logicalname': string 'embassy-password': Encrypted - 'recovery-source': CifsRecoverySource | DiskRecoverySource | null + 'recovery-source': RecoverySource | null 'recovery-password': Encrypted | null } @@ -81,6 +80,17 @@ export type DiskRecoverySource = { logicalname: string // partition logicalname } +export type BackupRecoverySource = { + type: 'backup' + target: CifsRecoverySource | DiskRecoverySource +} +export type RecoverySource = BackupRecoverySource | DiskMigrateSource + +export type DiskMigrateSource = { + type: 'migrate' + guid: string +} + export type CifsRecoverySource = { type: 'cifs' hostname: string @@ -95,7 +105,7 @@ export type DiskInfo = { model: string | null partitions: PartitionInfo[] capacity: number - guid: string | null // cant back up if guid exists + guid: string | null // cant back up if guid exists, but needed if migrating } export type RecoveryStatusRes = { diff --git a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts index 93b49ae01..6ec6496ff 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -10,6 +10,7 @@ import { ApiService, CifsRecoverySource, DiskListResponse, + DiskMigrateSource, DiskRecoverySource, EmbassyOSRecoveryInfo, GetStatusRes, @@ -58,13 +59,6 @@ export class LiveApiService extends ApiService { }) } - async set02XDrive(logicalname: string) { - return this.rpcRequest({ - method: 'setup.recovery.v2.set', - params: { logicalname }, - }) - } - async getRecoveryStatus() { return this.rpcRequest({ method: 'setup.recovery.status', @@ -93,10 +87,12 @@ export class LiveApiService extends ApiService { } async setupEmbassy(setupInfo: SetupEmbassyReq) { - if (isCifsSource(setupInfo['recovery-source'])) { - setupInfo['recovery-source'].path = setupInfo[ - 'recovery-source' - ].path.replace('/\\/g', '/') + if (setupInfo['recovery-source']?.type === 'backup') { + if (isCifsSource(setupInfo['recovery-source'].target)) { + setupInfo['recovery-source'].target.path = setupInfo[ + 'recovery-source' + ].target.path.replace('/\\/g', '/') + } } const res = await this.rpcRequest({ diff --git a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts index 93983f506..475aa79c0 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -54,7 +54,30 @@ export class MockApiService extends ApiService { 'embassy-os': { version: '0.2.17', full: true, - 'password-hash': null, + 'password-hash': + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + 'wrapped-key': null, + }, + }, + ], + capacity: 123456789123, + guid: 'uuid-uuid-uuid-uuid', + }, + { + logicalname: 'dcba', + vendor: 'Samsung', + model: 'T5', + partitions: [ + { + logicalname: 'pbcba', + label: null, + capacity: 73264762332, + used: null, + 'embassy-os': { + version: '0.3.1', + full: true, + 'password-hash': + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', 'wrapped-key': null, }, }, @@ -65,11 +88,6 @@ export class MockApiService extends ApiService { ] } - async set02XDrive() { - await pauseFor(1000) - return - } - async getRecoveryStatus() { tries = Math.min(tries + 1, 4) return { @@ -134,105 +152,3 @@ const setupRes = { 'lan-address': 'https://embassy-abcdefgh.local', 'root-ca': encodeBase64(rootCA), } - -const disks = [ - { - vendor: 'Samsung', - model: 'SATA', - logicalname: '/dev/sda', - guid: 'theguid', - partitions: [ - { - logicalname: 'sda1', - 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, - }, - { - vendor: 'Samsung', - model: null, - logicalname: 'dev/sdb', - partitions: [], - capacity: 34359738369, - guid: null, - }, - { - vendor: 'Crucial', - model: 'MX500', - logicalname: 'dev/sdc', - guid: null, - partitions: [ - { - logicalname: 'sdc1', - label: 'label 1', - capacity: 0, - used: null, - 'embassy-os': { - version: '0.3.3', - full: true, - 'password-hash': 'asdfasdfasdf', - 'wrapped-key': '', - }, - }, - { - logicalname: 'sdc1MOCKTESTER', - label: 'label 1', - capacity: 0, - used: null, - 'embassy-os': { - version: '0.3.6', - full: true, - // password is 'asdfasdf' - 'password-hash': - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - 'wrapped-key': '', - }, - }, - { - logicalname: 'sdc1', - label: 'label 1', - capacity: 0, - used: null, - 'embassy-os': { - version: '0.3.3', - full: false, - 'password-hash': 'asdfasdfasdf', - 'wrapped-key': '', - }, - }, - ], - capacity: 100000, - }, - { - vendor: 'Sandisk', - model: null, - logicalname: '/dev/sdd', - guid: null, - partitions: [ - { - logicalname: 'sdd1', - label: null, - capacity: 10000, - used: null, - 'embassy-os': { - version: '0.2.7', - full: true, - 'password-hash': 'asdfasdfasdf', - 'wrapped-key': '', - }, - }, - ], - capacity: 10000, - }, -] diff --git a/frontend/projects/setup-wizard/src/app/services/state.service.ts b/frontend/projects/setup-wizard/src/app/services/state.service.ts index 192a10a7f..3b03487bc 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -1,10 +1,6 @@ import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs' -import { - ApiService, - CifsRecoverySource, - DiskRecoverySource, -} from './api/api.service' +import { ApiService, RecoverySource } from './api/api.service' import { pauseFor, ErrorToastService } from '@start9labs/shared' @Injectable({ @@ -14,7 +10,7 @@ export class StateService { polling = false embassyLoaded = false - recoverySource?: CifsRecoverySource | DiskRecoverySource + recoverySource?: RecoverySource recoveryPassword?: string dataTransferProgress?: { 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 1b22ce124..6747397a6 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 @@ -70,14 +70,14 @@ export class BackupDrivesComponent { ): void { if (target.entry.type === 'cifs' && !target.entry.mountable) { const message = - 'Unable to connect to LAN Shared Folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.' + 'Unable to connect to Network Folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.' this.presentAlertError(message) return } if (this.type === 'restore' && !target.hasValidBackup) { const message = `${ - target.entry.type === 'cifs' ? 'LAN Shared Folder' : 'Drive partition' + target.entry.type === 'cifs' ? 'Network Folder' : 'Drive partition' } does not contain a valid Embassy backup.` this.presentAlertError(message) return @@ -90,7 +90,7 @@ export class BackupDrivesComponent { const modal = await this.modalCtrl.create({ component: GenericFormPage, componentProps: { - title: 'New LAN Shared Folder', + title: 'New Network Folder', spec: CifsSpec, buttons: [ {