mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 04:53:40 +00:00
Backups Rework (#698)
* wip: Backup al * wip: Backup * backup code complete * wip * wip * update types * wip * fix errors * Backups wizard (#699) * backup adjustments * fix endpoint arg * Update prod-key-modal.page.ts Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com> Co-authored-by: Aiden McClelland <me@drbonez.dev> * build errs addressed * working * update backup command input, nix, and apk add * add ecryptfs-utils * fix build * wip * fixes for macos * more mac magic * fix typo * working * fixes after rebase * chore: remove unused imports Co-authored-by: Justin Miller <dragondef@gmail.com> Co-authored-by: Drew Ansbacher <drew.ansbacher@gmail.com> Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com> Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::ArgMatches;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use self::util::DiskInfo;
|
||||
use crate::util::{display_serializable, IoFormat};
|
||||
use crate::context::RpcContext;
|
||||
use crate::disk::util::{BackupMountGuard, TmpMountGuard};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::{display_serializable, IoFormat, Version};
|
||||
use crate::Error;
|
||||
|
||||
pub mod main;
|
||||
@@ -34,11 +43,7 @@ fn display_disk_info(info: Vec<DiskInfo>, matches: &ArgMatches<'_>) {
|
||||
"N/A",
|
||||
&format!("{:.2} GiB", disk.capacity as f64 / 1024.0 / 1024.0 / 1024.0),
|
||||
"N/A",
|
||||
if let Some(eos_info) = disk.embassy_os.as_ref() {
|
||||
eos_info.version.as_str()
|
||||
} else {
|
||||
"N/A"
|
||||
}
|
||||
"N/A",
|
||||
];
|
||||
table.add_row(row);
|
||||
for part in disk.partitions {
|
||||
@@ -59,7 +64,11 @@ fn display_disk_info(info: Vec<DiskInfo>, matches: &ArgMatches<'_>) {
|
||||
} else {
|
||||
"N/A"
|
||||
},
|
||||
"N/A",
|
||||
if let Some(eos) = part.embassy_os.as_ref() {
|
||||
eos.version.as_str()
|
||||
} else {
|
||||
"N/A"
|
||||
},
|
||||
];
|
||||
table.add_row(row);
|
||||
}
|
||||
@@ -75,3 +84,73 @@ pub async fn list(
|
||||
) -> Result<Vec<DiskInfo>, Error> {
|
||||
crate::disk::util::list().await
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct BackupInfo {
|
||||
pub version: Version,
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
pub package_backups: BTreeMap<PackageId, PackageBackupInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PackageBackupInfo {
|
||||
pub title: String,
|
||||
pub version: Version,
|
||||
pub os_version: Version,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
fn display_backup_info(info: BackupInfo, matches: &ArgMatches<'_>) {
|
||||
use prettytable::*;
|
||||
|
||||
if matches.is_present("format") {
|
||||
return display_serializable(info, matches);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc =>
|
||||
"ID",
|
||||
"VERSION",
|
||||
"OS VERSION",
|
||||
"TIMESTAMP",
|
||||
]);
|
||||
table.add_row(row![
|
||||
"EMBASSY OS",
|
||||
info.version.as_str(),
|
||||
info.version.as_str(),
|
||||
&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.as_str(),
|
||||
info.version.as_str(),
|
||||
info.os_version.as_str(),
|
||||
&info.timestamp.to_string(),
|
||||
];
|
||||
table.add_row(row);
|
||||
}
|
||||
table.print_tty(false);
|
||||
}
|
||||
|
||||
#[command(rename = "backup-info", display(display_backup_info))]
|
||||
#[instrument(skip(ctx, password))]
|
||||
pub async fn backup_info(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] logicalname: PathBuf,
|
||||
#[arg] password: String,
|
||||
) -> Result<BackupInfo, Error> {
|
||||
let guard =
|
||||
BackupMountGuard::mount(TmpMountGuard::mount(logicalname).await?, &password).await?;
|
||||
|
||||
let res = guard.metadata.clone();
|
||||
|
||||
guard.unmount().await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use color_eyre::eyre::{self, eyre};
|
||||
use digest::Digest;
|
||||
use futures::TryStreamExt;
|
||||
use indexmap::IndexSet;
|
||||
use regex::Regex;
|
||||
@@ -11,11 +13,16 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::BackupInfo;
|
||||
use crate::auth::check_password;
|
||||
use crate::middleware::encrypt::{decrypt_slice, encrypt_slice};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::io::from_yaml_async_reader;
|
||||
use crate::util::{GeneralGuard, Invoke, Version};
|
||||
use crate::util::{AtomicFile, FileLock, GeneralGuard, Invoke, IoFormat, Version};
|
||||
use crate::volume::BACKUP_DIR;
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
pub const TMP_MOUNTPOINT: &'static str = "/media/embassy-os";
|
||||
pub const TMP_MOUNTPOINT: &'static str = "/media/embassy-os/tmp";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -25,7 +32,7 @@ pub struct DiskInfo {
|
||||
pub model: Option<String>,
|
||||
pub partitions: Vec<PartitionInfo>,
|
||||
pub capacity: usize,
|
||||
pub embassy_os: Option<EmbassyOsDiskInfo>,
|
||||
pub internal: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -35,12 +42,16 @@ pub struct PartitionInfo {
|
||||
pub label: Option<String>,
|
||||
pub capacity: usize,
|
||||
pub used: Option<usize>,
|
||||
pub embassy_os: Option<EmbassyOsRecoveryInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct EmbassyOsDiskInfo {
|
||||
pub struct EmbassyOsRecoveryInfo {
|
||||
pub version: Version,
|
||||
pub full: bool,
|
||||
pub password_hash: Option<String>,
|
||||
pub wrapped_key: Option<String>,
|
||||
}
|
||||
|
||||
const DISK_PATH: &'static str = "/dev/disk/by-path";
|
||||
@@ -48,6 +59,7 @@ const SYS_BLOCK_PATH: &'static str = "/sys/block";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PARTITION_REGEX: Regex = Regex::new("-part[0-9]+$").unwrap();
|
||||
static ref ZPOOL_REGEX: Regex = Regex::new("^\\s+([a-z0-9]+)\\s+ONLINE").unwrap();
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
@@ -135,12 +147,19 @@ pub async fn get_used<P: AsRef<Path>>(path: P) -> Result<usize, Error> {
|
||||
|
||||
#[instrument]
|
||||
pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
if tokio::fs::metadata(TMP_MOUNTPOINT).await.is_err() {
|
||||
tokio::fs::create_dir_all(TMP_MOUNTPOINT)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, TMP_MOUNTPOINT))?;
|
||||
}
|
||||
|
||||
let zpool_drives: BTreeSet<PathBuf> = match Command::new("zpool")
|
||||
.arg("status")
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await
|
||||
{
|
||||
Ok(v) => String::from_utf8(v)?
|
||||
.lines()
|
||||
.filter_map(|l| ZPOOL_REGEX.captures(l))
|
||||
.filter_map(|c| c.get(1))
|
||||
.map(|d| Path::new("/dev").join(d.as_str()))
|
||||
.collect(),
|
||||
Err(e) => BTreeSet::new(),
|
||||
};
|
||||
let disks = tokio_stream::wrappers::ReadDirStream::new(
|
||||
tokio::fs::read_dir(DISK_PATH)
|
||||
.await
|
||||
@@ -192,6 +211,7 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
|
||||
let mut res = Vec::with_capacity(disks.len());
|
||||
for (disk, parts) in disks {
|
||||
let mut internal = false;
|
||||
let mut partitions = Vec::with_capacity(parts.len());
|
||||
let vendor = get_vendor(&disk)
|
||||
.await
|
||||
@@ -207,53 +227,95 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
tracing::warn!("Could not get capacity of {}: {}", disk.display(), e.source)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut embassy_os = None;
|
||||
for part in parts {
|
||||
let label = get_label(&part).await?;
|
||||
let capacity = get_capacity(&part)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Could not get capacity of {}: {}", part.display(), e.source)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut used = None;
|
||||
|
||||
let tmp_mountpoint =
|
||||
Path::new(TMP_MOUNTPOINT).join(&part.strip_prefix("/").unwrap_or(&part));
|
||||
if let Err(e) = mount(&part, &tmp_mountpoint).await {
|
||||
tracing::warn!("Could not collect usage information: {}", e.source)
|
||||
} else {
|
||||
let mount_guard = GeneralGuard::new(|| {
|
||||
let path = tmp_mountpoint.clone();
|
||||
tokio::spawn(unmount(path))
|
||||
});
|
||||
used = get_used(&tmp_mountpoint)
|
||||
if zpool_drives.contains(&disk) {
|
||||
internal = true;
|
||||
} else {
|
||||
for part in parts {
|
||||
let mut embassy_os = None;
|
||||
let label = get_label(&part).await?;
|
||||
let capacity = get_capacity(&part)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Could not get usage of {}: {}", part.display(), e.source)
|
||||
tracing::warn!("Could not get capacity of {}: {}", part.display(), e.source)
|
||||
})
|
||||
.ok();
|
||||
if label.as_deref() == Some("rootfs") {
|
||||
let version_path = tmp_mountpoint.join("root").join("appmgr").join("version");
|
||||
if tokio::fs::metadata(&version_path).await.is_ok() {
|
||||
embassy_os = Some(EmbassyOsDiskInfo {
|
||||
version: from_yaml_async_reader(File::open(&version_path).await?)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
mount_guard
|
||||
.drop()
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Unknown)??;
|
||||
}
|
||||
.unwrap_or_default();
|
||||
let mut used = None;
|
||||
|
||||
partitions.push(PartitionInfo {
|
||||
logicalname: part,
|
||||
label,
|
||||
capacity,
|
||||
used,
|
||||
});
|
||||
let tmp_mountpoint =
|
||||
Path::new(TMP_MOUNTPOINT).join(&part.strip_prefix("/").unwrap_or(&part));
|
||||
if let Err(e) = mount(&part, &tmp_mountpoint).await {
|
||||
tracing::warn!("Could not collect usage information: {}", e.source)
|
||||
} else {
|
||||
let mount_guard = GeneralGuard::new(|| {
|
||||
let path = tmp_mountpoint.clone();
|
||||
tokio::spawn(unmount(path))
|
||||
});
|
||||
used = get_used(&tmp_mountpoint)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Could not get usage of {}: {}",
|
||||
part.display(),
|
||||
e.source
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
let backup_unencrypted_metadata_path =
|
||||
tmp_mountpoint.join("EmbassyBackups/unencrypted-metadata.cbor");
|
||||
if tokio::fs::metadata(&backup_unencrypted_metadata_path)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
embassy_os = match (|| async {
|
||||
IoFormat::Cbor.from_slice(
|
||||
&tokio::fs::read(&backup_unencrypted_metadata_path)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
backup_unencrypted_metadata_path.display().to_string(),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
})()
|
||||
.await
|
||||
{
|
||||
Ok(a) => Some(a),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Error fetching unencrypted backup metadata: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
} else if label.as_deref() == Some("rootfs") {
|
||||
let version_path =
|
||||
tmp_mountpoint.join("root").join("appmgr").join("version");
|
||||
if tokio::fs::metadata(&version_path).await.is_ok() {
|
||||
embassy_os = Some(EmbassyOsRecoveryInfo {
|
||||
version: from_yaml_async_reader(File::open(&version_path).await?)
|
||||
.await?,
|
||||
full: true,
|
||||
password_hash: None,
|
||||
wrapped_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
mount_guard
|
||||
.drop()
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Unknown)??;
|
||||
}
|
||||
|
||||
partitions.push(PartitionInfo {
|
||||
logicalname: part,
|
||||
label,
|
||||
capacity,
|
||||
used,
|
||||
embassy_os,
|
||||
});
|
||||
}
|
||||
}
|
||||
res.push(DiskInfo {
|
||||
logicalname: disk,
|
||||
@@ -261,31 +323,31 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
model,
|
||||
partitions,
|
||||
capacity,
|
||||
embassy_os,
|
||||
internal,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument(skip(logicalname, mount_point))]
|
||||
pub async fn mount<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
logicalname: P0,
|
||||
mount_point: P1,
|
||||
#[instrument(skip(logicalname, mountpoint))]
|
||||
pub async fn mount(
|
||||
logicalname: impl AsRef<Path>,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
) -> Result<(), Error> {
|
||||
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
||||
.arg(mount_point.as_ref())
|
||||
.arg(mountpoint.as_ref())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await?;
|
||||
if is_mountpoint.success() {
|
||||
unmount(mount_point.as_ref()).await?;
|
||||
unmount(mountpoint.as_ref()).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(&mount_point).await?;
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mount_output = tokio::process::Command::new("mount")
|
||||
.arg(logicalname.as_ref())
|
||||
.arg(mount_point.as_ref())
|
||||
.arg(mountpoint.as_ref())
|
||||
.output()
|
||||
.await?;
|
||||
crate::ensure_code!(
|
||||
@@ -293,36 +355,47 @@ pub async fn mount<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
crate::ErrorKind::Filesystem,
|
||||
"Error Mounting {} to {}: {}",
|
||||
logicalname.as_ref().display(),
|
||||
mount_point.as_ref().display(),
|
||||
mountpoint.as_ref().display(),
|
||||
std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(src, dst, password))]
|
||||
pub async fn mount_encfs<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
pub async fn mount_ecryptfs<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
src: P0,
|
||||
dst: P1,
|
||||
password: &str,
|
||||
) -> Result<(), Error> {
|
||||
let mut encfs = tokio::process::Command::new("encfs")
|
||||
.arg("--standard")
|
||||
.arg("--public")
|
||||
.arg("-S")
|
||||
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
||||
.arg(dst.as_ref())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await?;
|
||||
if is_mountpoint.success() {
|
||||
unmount(dst.as_ref()).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(dst.as_ref()).await?;
|
||||
let mut ecryptfs = tokio::process::Command::new("mount")
|
||||
.arg("-t")
|
||||
.arg("ecryptfs")
|
||||
.arg(src.as_ref())
|
||||
.arg(dst.as_ref())
|
||||
.arg("-o")
|
||||
.arg(format!("key=passphrase,passwd={},ecryptfs_cipher=aes,ecryptfs_key_bytes=32,ecryptfs_passthrough=n,ecryptfs_enable_filename_crypto=y", password))
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
let mut stdin = encfs.stdin.take().unwrap();
|
||||
let mut stderr = encfs.stderr.take().unwrap();
|
||||
stdin.write_all(password.as_bytes()).await?;
|
||||
let mut stdin = ecryptfs.stdin.take().unwrap();
|
||||
let mut stderr = ecryptfs.stderr.take().unwrap();
|
||||
stdin.write_all(b"\nyes\nno").await?;
|
||||
stdin.flush().await?;
|
||||
stdin.shutdown().await?;
|
||||
drop(stdin);
|
||||
let mut err = String::new();
|
||||
stderr.read_to_string(&mut err).await?;
|
||||
if !encfs.wait().await?.success() {
|
||||
if !ecryptfs.wait().await?.success() {
|
||||
Err(Error::new(eyre!("{}", err), crate::ErrorKind::Filesystem))
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -373,27 +446,334 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(mount_point))]
|
||||
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
|
||||
tracing::info!("Unmounting {}.", mount_point.as_ref().display());
|
||||
#[instrument(skip(mountpoint))]
|
||||
pub async fn unmount<P: AsRef<Path>>(mountpoint: P) -> Result<(), Error> {
|
||||
tracing::debug!("Unmounting {}.", mountpoint.as_ref().display());
|
||||
let umount_output = tokio::process::Command::new("umount")
|
||||
.arg(mount_point.as_ref())
|
||||
.arg(mountpoint.as_ref())
|
||||
.output()
|
||||
.await?;
|
||||
crate::ensure_code!(
|
||||
umount_output.status.success(),
|
||||
crate::ErrorKind::Filesystem,
|
||||
"Error Unmounting Drive: {}: {}",
|
||||
mount_point.as_ref().display(),
|
||||
mountpoint.as_ref().display(),
|
||||
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
|
||||
);
|
||||
tokio::fs::remove_dir_all(mount_point.as_ref())
|
||||
tokio::fs::remove_dir_all(mountpoint.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
format!("rm {}", mount_point.as_ref().display()),
|
||||
format!("rm {}", mountpoint.as_ref().display()),
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GenericMountGuard: AsRef<Path> {
|
||||
async fn unmount(mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub struct MountGuard {
|
||||
mountpoint: PathBuf,
|
||||
mounted: bool,
|
||||
}
|
||||
impl MountGuard {
|
||||
pub async fn mount(
|
||||
logicalname: impl AsRef<Path>,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
) -> Result<Self, Error> {
|
||||
let mountpoint = mountpoint.as_ref().to_owned();
|
||||
mount(logicalname, &mountpoint).await?;
|
||||
Ok(MountGuard {
|
||||
mountpoint,
|
||||
mounted: true,
|
||||
})
|
||||
}
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
if self.mounted {
|
||||
unmount(&self.mountpoint).await?;
|
||||
self.mounted = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for MountGuard {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.mountpoint
|
||||
}
|
||||
}
|
||||
impl Drop for MountGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.mounted {
|
||||
let mountpoint = std::mem::take(&mut self.mountpoint);
|
||||
tokio::spawn(async move { unmount(mountpoint).await.unwrap() });
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl GenericMountGuard for MountGuard {
|
||||
async fn unmount(mut self) -> Result<(), Error> {
|
||||
MountGuard::unmount(self).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn tmp_mountpoint(source: impl AsRef<Path>) -> Result<PathBuf, Error> {
|
||||
Ok(Path::new(TMP_MOUNTPOINT).join(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&sha2::Sha256::digest(
|
||||
tokio::fs::canonicalize(&source)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
source.as_ref().display().to_string(),
|
||||
)
|
||||
})?
|
||||
.as_os_str()
|
||||
.as_bytes(),
|
||||
),
|
||||
)))
|
||||
}
|
||||
|
||||
pub struct TmpMountGuard {
|
||||
guard: MountGuard,
|
||||
lock: FileLock,
|
||||
}
|
||||
impl TmpMountGuard {
|
||||
pub async fn mount(logicalname: impl AsRef<Path>) -> Result<Self, Error> {
|
||||
let mountpoint = tmp_mountpoint(&logicalname).await?;
|
||||
let lock = FileLock::new(mountpoint.with_extension("lock")).await?;
|
||||
let guard = MountGuard::mount(logicalname, &mountpoint).await?;
|
||||
Ok(TmpMountGuard { guard, lock })
|
||||
}
|
||||
pub async fn unmount(self) -> Result<(), Error> {
|
||||
let TmpMountGuard { guard, lock } = self;
|
||||
guard.unmount().await?;
|
||||
lock.unlock().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for TmpMountGuard {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.guard.as_ref()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl GenericMountGuard for TmpMountGuard {
|
||||
async fn unmount(mut self) -> Result<(), Error> {
|
||||
TmpMountGuard::unmount(self).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BackupMountGuard<G: GenericMountGuard> {
|
||||
backup_disk_mount_guard: Option<G>,
|
||||
enc_key: String,
|
||||
pub unencrypted_metadata: EmbassyOsRecoveryInfo,
|
||||
pub metadata: BackupInfo,
|
||||
mountpoint: PathBuf,
|
||||
mounted: bool,
|
||||
}
|
||||
impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
fn backup_disk_path(&self) -> &Path {
|
||||
if let Some(guard) = &self.backup_disk_mount_guard {
|
||||
guard.as_ref()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mount(backup_disk_mount_guard: G, password: &str) -> Result<Self, Error> {
|
||||
let mountpoint = tmp_mountpoint(&backup_disk_mount_guard).await?;
|
||||
let backup_disk_path = backup_disk_mount_guard.as_ref();
|
||||
let unencrypted_metadata_path =
|
||||
backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor");
|
||||
let unencrypted_metadata: EmbassyOsRecoveryInfo =
|
||||
if tokio::fs::metadata(&unencrypted_metadata_path)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
IoFormat::Cbor.from_slice(
|
||||
&tokio::fs::read(&unencrypted_metadata_path)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
unencrypted_metadata_path.display().to_string(),
|
||||
)
|
||||
})?,
|
||||
)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let enc_key = if let (Some(hash), Some(wrapped_key)) = (
|
||||
unencrypted_metadata.password_hash.as_ref(),
|
||||
unencrypted_metadata.wrapped_key.as_ref(),
|
||||
) {
|
||||
let wrapped_key =
|
||||
base32::decode(base32::Alphabet::RFC4648 { padding: false }, wrapped_key)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("failed to decode wrapped key"),
|
||||
crate::ErrorKind::Backup,
|
||||
)
|
||||
})?;
|
||||
check_password(hash, password)?;
|
||||
String::from_utf8(decrypt_slice(wrapped_key, password))?
|
||||
} else {
|
||||
base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&rand::random::<[u8; 32]>()[..],
|
||||
)
|
||||
};
|
||||
|
||||
let crypt_path = backup_disk_path.join("EmbassyBackups/crypt");
|
||||
if tokio::fs::metadata(&crypt_path).await.is_err() {
|
||||
tokio::fs::create_dir_all(&crypt_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
crypt_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
mount_ecryptfs(&crypt_path, &mountpoint, &enc_key).await?;
|
||||
let metadata = match async {
|
||||
let metadata_path = mountpoint.join("metadata.cbor");
|
||||
let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() {
|
||||
IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(
|
||||
|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
metadata_path.display().to_string(),
|
||||
)
|
||||
},
|
||||
)?)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
Ok(metadata)
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
unmount(&mountpoint).await?;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
backup_disk_mount_guard: Some(backup_disk_mount_guard),
|
||||
enc_key,
|
||||
unencrypted_metadata,
|
||||
metadata,
|
||||
mountpoint,
|
||||
mounted: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_password(&mut self, new_password: &str) -> Result<(), Error> {
|
||||
self.unencrypted_metadata.password_hash = Some(
|
||||
argon2::hash_encoded(
|
||||
new_password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::default(),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::PasswordHashGeneration)?,
|
||||
);
|
||||
self.unencrypted_metadata.wrapped_key = Some(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&encrypt_slice(&self.enc_key, new_password),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mount_package_backup(
|
||||
&self,
|
||||
id: &PackageId,
|
||||
) -> Result<PackageBackupMountGuard, Error> {
|
||||
let lock = FileLock::new(Path::new(BACKUP_DIR).join(format!("{}.lock", id))).await?;
|
||||
let mountpoint = Path::new(BACKUP_DIR).join(id);
|
||||
bind(self.mountpoint.join(id), &mountpoint, false).await?;
|
||||
Ok(PackageBackupMountGuard {
|
||||
mountpoint,
|
||||
lock,
|
||||
mounted: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<(), Error> {
|
||||
let metadata_path = self.mountpoint.join("metadata.cbor");
|
||||
let backup_disk_path = self.backup_disk_path();
|
||||
let mut file = AtomicFile::new(&metadata_path).await?;
|
||||
file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?)
|
||||
.await?;
|
||||
file.save().await?;
|
||||
let unencrypted_metadata_path =
|
||||
backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor");
|
||||
let mut file = AtomicFile::new(&unencrypted_metadata_path).await?;
|
||||
file.write_all(&IoFormat::Cbor.to_vec(&self.unencrypted_metadata)?)
|
||||
.await?;
|
||||
file.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
if self.mounted {
|
||||
unmount(&self.mountpoint).await?;
|
||||
self.mounted = false;
|
||||
}
|
||||
if let Some(guard) = self.backup_disk_mount_guard.take() {
|
||||
guard.unmount().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_and_unmount(self) -> Result<(), Error> {
|
||||
self.save().await?;
|
||||
self.unmount().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl<G: GenericMountGuard> AsRef<Path> for BackupMountGuard<G> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.mountpoint
|
||||
}
|
||||
}
|
||||
impl<G: GenericMountGuard> Drop for BackupMountGuard<G> {
|
||||
fn drop(&mut self) {
|
||||
if self.mounted {
|
||||
let mountpoint = std::mem::take(&mut self.mountpoint);
|
||||
tokio::spawn(async move { unmount(mountpoint).await.unwrap() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PackageBackupMountGuard {
|
||||
mountpoint: PathBuf,
|
||||
lock: FileLock,
|
||||
mounted: bool,
|
||||
}
|
||||
impl PackageBackupMountGuard {
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
if self.mounted {
|
||||
unmount(&self.mountpoint).await?;
|
||||
self.mounted = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for PackageBackupMountGuard {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.mountpoint
|
||||
}
|
||||
}
|
||||
impl Drop for PackageBackupMountGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.mounted {
|
||||
let mountpoint = std::mem::take(&mut self.mountpoint);
|
||||
tokio::spawn(async move { unmount(mountpoint).await.unwrap() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user