use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use chrono::{DateTime, Utc}; use clap::Parser; use clap::builder::ValueParserFactory; use color_eyre::eyre::eyre; use digest::OutputSizeUser; use digest::generic_array::GenericArray; use exver::Version; use imbl_value::InternedString; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use tokio::sync::Mutex; use tracing::instrument; use ts_rs::TS; use self::cifs::CifsBackupTarget; use crate::PackageId; use crate::context::{CliContext, RpcContext}; use crate::db::model::DatabaseModel; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::util::PartitionInfo; use crate::prelude::*; use crate::util::serde::{ HandlerExtSerde, WithIoFormat, deserialize_from_str, display_serializable, serialize_display, }; use crate::util::{FromStrParser, VersionString}; pub mod cifs; #[derive(Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(tag = "type")] #[serde(rename_all = "camelCase")] pub enum BackupTarget { #[serde(rename_all = "camelCase")] Disk { vendor: Option, model: Option, #[serde(flatten)] partition_info: PartitionInfo, }, Cifs(CifsBackupTarget), } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, TS)] #[ts(export, type = "string")] pub enum BackupTargetId { Disk { logicalname: PathBuf }, Cifs { id: u32 }, } impl BackupTargetId { pub fn load(self, db: &DatabaseModel) -> Result { Ok(match self { BackupTargetId::Disk { logicalname } => { BackupTargetFS::Disk(BlockDev::new(logicalname)) } BackupTargetId::Cifs { id } => BackupTargetFS::Cifs(cifs::load(db, id)?), }) } } impl std::fmt::Display for BackupTargetId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BackupTargetId::Disk { logicalname } => write!(f, "disk-{}", logicalname.display()), BackupTargetId::Cifs { id } => write!(f, "cifs-{}", id), } } } impl std::str::FromStr for BackupTargetId { type Err = Error; fn from_str(s: &str) -> Result { match s.split_once('-') { Some(("disk", logicalname)) => Ok(BackupTargetId::Disk { logicalname: Path::new(logicalname).to_owned(), }), Some(("cifs", id)) => Ok(BackupTargetId::Cifs { id: id.parse()? }), _ => Err(Error::new( eyre!("Invalid Backup Target ID"), ErrorKind::InvalidBackupTargetId, )), } } } impl ValueParserFactory for BackupTargetId { type Parser = FromStrParser; fn value_parser() -> Self::Parser { FromStrParser::new() } } impl<'de> Deserialize<'de> for BackupTargetId { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserialize_from_str(deserializer) } } impl Serialize for BackupTargetId { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serialize_display(self, serializer) } } #[derive(Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(tag = "type")] #[serde(rename_all = "camelCase")] pub enum BackupTargetFS { Disk(BlockDev), Cifs(Cifs), } impl FileSystem for BackupTargetFS { async fn mount + Send>( &self, mountpoint: P, mount_type: MountType, ) -> Result<(), Error> { match self { BackupTargetFS::Disk(a) => a.mount(mountpoint, mount_type).await, BackupTargetFS::Cifs(a) => a.mount(mountpoint, mount_type).await, } } async fn source_hash( &self, ) -> Result::OutputSize>, Error> { match self { BackupTargetFS::Disk(a) => a.source_hash().await, BackupTargetFS::Cifs(a) => a.source_hash().await, } } } // #[command(subcommands(cifs::cifs, list, info, mount, umount))] pub fn target() -> ParentHandler { ParentHandler::new() .subcommand( "cifs", cifs::cifs::().with_about("about.add-remove-update-backup-target"), ) .subcommand( "list", from_fn_async(list) .with_display_serializable() .with_about("about.list-existing-backup-targets") .with_call_remote::(), ) .subcommand( "info", from_fn_async(info) .with_display_serializable() .with_custom_display_fn::(|params, info| { display_backup_info(params.params, info) }) .with_about("about.display-package-backup-information") .with_call_remote::(), ) .subcommand( "mount", from_fn_async(mount) .with_about("about.mount-backup-target") .with_call_remote::(), ) .subcommand( "umount", from_fn_async(umount) .no_display() .with_about("about.unmount-backup-target") .with_call_remote::(), ) } // #[command(display(display_serializable))] pub async fn list(ctx: RpcContext) -> Result, Error> { let peek = ctx.db.peek().await; let (disks_res, cifs) = tokio::try_join!( crate::disk::util::list(&ctx.os_partitions), cifs::list(&peek), )?; Ok(disks_res .into_iter() .flat_map(|mut disk| { std::mem::take(&mut disk.partitions) .into_iter() .map(|part| { ( BackupTargetId::Disk { logicalname: part.logicalname.clone(), }, BackupTarget::Disk { vendor: disk.vendor.clone(), model: disk.model.clone(), partition_info: part, }, ) }) .collect::>() }) .chain( cifs.into_iter() .map(|(id, cifs)| (BackupTargetId::Cifs { id }, BackupTarget::Cifs(cifs))), ) .collect()) } #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct BackupInfo { #[ts(type = "string")] pub version: Version, #[ts(type = "string | null")] pub timestamp: Option>, pub package_backups: BTreeMap, } #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct PackageBackupInfo { pub title: InternedString, pub version: VersionString, #[ts(type = "string")] pub os_version: Version, #[ts(type = "string")] pub timestamp: DateTime, } fn display_backup_info(params: WithIoFormat, info: BackupInfo) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { return display_serializable(format, info); } let mut table = Table::new(); table.add_row(row![bc => "ID", "VERSION", "OS VERSION", "TIMESTAMP", ]); table.add_row(row![ "StartOS", &info.version.to_string(), &info.version.to_string(), &if let Some(ts) = &info.timestamp { ts.to_string() } else { "N/A".to_owned() }, ]); for (id, info) in info.package_backups { let row = row![ &*id, info.version.as_str(), &info.os_version.to_string(), &info.timestamp.to_string(), ]; table.add_row(row); } table.print_tty(false)?; Ok(()) } #[derive(Deserialize, Serialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct InfoParams { #[arg(help = "help.arg.backup-target-id")] target_id: BackupTargetId, #[arg(help = "help.arg.server-id")] server_id: String, #[arg(help = "help.arg.backup-password")] password: String, } #[instrument(skip(ctx, password))] pub async fn info( ctx: RpcContext, InfoParams { target_id, server_id, password, }: InfoParams, ) -> Result { let guard = BackupMountGuard::mount( TmpMountGuard::mount(&target_id.load(&ctx.db.peek().await)?, ReadWrite).await?, &server_id, &password, ) .await?; let res = guard.metadata.clone(); guard.unmount().await?; Ok(res) } lazy_static::lazy_static! { static ref USER_MOUNTS: Mutex, TmpMountGuard>>> = Mutex::new(BTreeMap::new()); } #[derive(Deserialize, Serialize, Parser)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct MountParams { #[arg(help = "help.arg.backup-target-id")] target_id: BackupTargetId, #[arg(long, help = "help.arg.server-id")] server_id: Option, #[arg(help = "help.arg.backup-password")] password: String, // TODO: rpassword #[arg(long, help = "help.arg.allow-partial-backup")] allow_partial: bool, } #[instrument(skip_all)] pub async fn mount( ctx: RpcContext, MountParams { target_id, server_id, password, allow_partial, }: MountParams, ) -> Result { let server_id = if let Some(server_id) = server_id { server_id } else { ctx.db .peek() .await .into_public() .into_server_info() .into_id() .de()? }; let mut mounts = USER_MOUNTS.lock().await; let existing = mounts.get(&target_id); let base = match existing { Some(Ok(a)) => return Ok(a.path().display().to_string()), Some(Err(e)) => e.clone(), None => { TmpMountGuard::mount(&target_id.clone().load(&ctx.db.peek().await)?, ReadWrite).await? } }; let guard = match BackupMountGuard::mount(base.clone(), &server_id, &password).await { Ok(a) => a, Err(e) => { if allow_partial { mounts.insert(target_id, Err(base.clone())); let enc_key = BackupMountGuard::::load_metadata( base.path(), &server_id, &password, ) .await .map(|(_, k)| k); return Err(e) .with_ctx(|e| ( e.kind, format!( "\nThe base filesystem did successfully mount at {:?}\nWrapped Key: {:?}", base.path(), enc_key ) )); } else { return Err(e); } } }; let res = guard.path().display().to_string(); mounts.insert(target_id, Ok(guard)); Ok(res) } #[derive(Deserialize, Serialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct UmountParams { #[arg(help = "help.arg.backup-target-id")] target_id: Option, } #[instrument(skip_all)] pub async fn umount(_: RpcContext, UmountParams { target_id }: UmountParams) -> Result<(), Error> { let mut mounts = USER_MOUNTS.lock().await; // TODO: move to context if let Some(target_id) = target_id { if let Some(existing) = mounts.remove(&target_id) { match existing { Ok(e) => e.unmount().await?, Err(e) => e.unmount().await?, } } } else { for (_, existing) in std::mem::take(&mut *mounts) { match existing { Ok(e) => e.unmount().await?, Err(e) => e.unmount().await?, } } } Ok(()) }