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:
Aiden McClelland
2021-10-23 22:00:23 -06:00
parent 78dcce7be5
commit 8056285a7f
52 changed files with 2032 additions and 873 deletions

View File

@@ -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)
}

View File

@@ -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() });
}
}
}