Refactor/project structure (#3085)

* refactor project structure

* environment-based default registry

* fix tests

* update build container

* use docker platform for iso build emulation

* simplify compat

* Fix docker platform spec in run-compat.sh

* handle riscv compat

* fix bug with dep error exists attr

* undo removal of sorting

* use qemu for iso stage

---------

Co-authored-by: Mariusz Kogen <k0gen@pm.me>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2025-12-22 13:39:38 -07:00
committed by GitHub
parent eda08d5b0f
commit 96ae532879
389 changed files with 744 additions and 4005 deletions

View File

@@ -0,0 +1,68 @@
use std::borrow::Cow;
use std::fmt::{self, Display};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use sha2::Sha256;
use super::FileSystem;
use crate::prelude::*;
pub struct BackupFS<DataDir: AsRef<Path>, Password: fmt::Display> {
data_dir: DataDir,
password: Password,
idmapped_root: Vec<(u32, u32)>,
}
impl<DataDir: AsRef<Path>, Password: fmt::Display> BackupFS<DataDir, Password> {
pub fn new(data_dir: DataDir, password: Password, idmapped_root: Vec<(u32, u32)>) -> Self {
BackupFS {
data_dir,
password,
idmapped_root,
}
}
}
impl<DataDir: AsRef<Path> + Send + Sync, Password: fmt::Display + Send + Sync> FileSystem
for BackupFS<DataDir, Password>
{
fn mount_type(&self) -> Option<impl AsRef<str>> {
Some("backup-fs")
}
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
[
Cow::Owned(format!("password={}", self.password)),
Cow::Borrowed("file-size-padding=0.05"),
Cow::Borrowed("allow_other"),
]
.into_iter()
.chain(
self.idmapped_root
.iter()
.map(|(root, range)| Cow::Owned(format!("idmapped-root={root}:{range}"))),
)
}
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(&self.data_dir))
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("BackupFS");
sha.update(
tokio::fs::canonicalize(self.data_dir.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.data_dir.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,100 @@
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use ts_rs::TS;
use super::FileSystem;
use crate::disk::mount::filesystem::MountType;
use crate::prelude::*;
use crate::util::io::create_file;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "kebab-case")]
pub enum FileType {
File,
Directory,
Infer,
}
impl Default for FileType {
fn default() -> Self {
FileType::Directory
}
}
pub struct Bind<Src: AsRef<Path>> {
src: Src,
filetype: FileType,
}
impl<Src: AsRef<Path>> Bind<Src> {
pub fn new(src: Src) -> Self {
Self {
src,
filetype: FileType::Directory,
}
}
pub fn with_type(mut self, filetype: FileType) -> Self {
self.filetype = filetype;
self
}
}
impl<Src: AsRef<Path> + Send + Sync> FileSystem for Bind<Src> {
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(&self.src))
}
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>> {
["--bind"]
}
async fn pre_mount(&self, mountpoint: &Path, mount_type: MountType) -> Result<(), Error> {
let from_meta = tokio::fs::metadata(&self.src).await.ok();
let to_meta = tokio::fs::metadata(&mountpoint).await.ok();
if matches!(self.filetype, FileType::File)
|| (matches!(self.filetype, FileType::Infer)
&& from_meta.as_ref().map_or(false, |m| m.is_file()))
{
if to_meta.as_ref().map_or(false, |m| m.is_dir()) {
tokio::fs::remove_dir(mountpoint).await?;
}
if from_meta.is_none() && mount_type == MountType::ReadWrite {
create_file(self.src.as_ref()).await?.sync_all().await?;
}
if to_meta.is_none() {
create_file(mountpoint).await?.sync_all().await?;
}
} else {
if to_meta.as_ref().map_or(false, |m| m.is_file()) {
tokio::fs::remove_file(mountpoint).await?;
}
if from_meta.is_none() && mount_type == MountType::ReadWrite {
tokio::fs::create_dir_all(self.src.as_ref()).await?;
}
if to_meta.is_none() {
tokio::fs::create_dir_all(mountpoint).await?;
}
}
Ok(())
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("Bind");
sha.update(
tokio::fs::canonicalize(self.src.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.src.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,47 @@
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use ts_rs::TS;
use super::FileSystem;
use crate::prelude::*;
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(concrete(LogicalName = std::path::PathBuf))]
pub struct BlockDev<LogicalName: AsRef<Path>> {
logicalname: LogicalName,
}
impl<LogicalName: AsRef<Path>> BlockDev<LogicalName> {
pub fn new(logicalname: LogicalName) -> Self {
BlockDev { logicalname }
}
}
impl<LogicalName: AsRef<Path> + Send + Sync> FileSystem for BlockDev<LogicalName> {
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(&self.logicalname))
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("BlockDev");
sha.update(
tokio::fs::canonicalize(self.logicalname.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.logicalname.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,106 @@
use std::net::IpAddr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::process::Command;
use tracing::instrument;
use ts_rs::TS;
use super::{FileSystem, MountType, ReadOnly};
use crate::Error;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::util::Invoke;
async fn resolve_hostname(hostname: &str) -> Result<IpAddr, Error> {
if let Ok(addr) = hostname.parse() {
return Ok(addr);
}
if hostname.ends_with(".local") {
return Ok(IpAddr::V4(crate::net::mdns::resolve_mdns(hostname).await?));
}
Ok(String::from_utf8(
Command::new("nmblookup")
.arg(hostname)
.invoke(crate::ErrorKind::Network)
.await?,
)?
.split(" ")
.next()
.unwrap()
.trim()
.parse()?)
}
#[instrument(skip_all)]
pub async fn mount_cifs(
hostname: &str,
path: impl AsRef<Path>,
username: &str,
password: Option<&str>,
mountpoint: impl AsRef<Path>,
mount_type: MountType,
) -> Result<(), Error> {
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
let ip: IpAddr = resolve_hostname(hostname).await?;
let absolute_path = Path::new("/").join(path.as_ref());
let mut cmd = Command::new("mount");
cmd.arg("-t")
.arg("cifs")
.env("USER", username)
.env("PASSWD", password.unwrap_or_default())
.arg(format!("//{}{}", ip, absolute_path.display()))
.arg(mountpoint.as_ref());
if mount_type == ReadOnly {
cmd.arg("-o").arg("ro,noserverino");
} else {
cmd.arg("-o").arg("noserverino");
}
cmd.invoke(crate::ErrorKind::Filesystem).await?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct Cifs {
pub hostname: String,
pub path: PathBuf,
pub username: String,
pub password: Option<String>,
}
impl Cifs {
pub async fn mountable(&self) -> Result<(), Error> {
let guard = TmpMountGuard::mount(self, ReadOnly).await?;
guard.unmount().await?;
Ok(())
}
}
impl FileSystem for Cifs {
async fn mount<P: AsRef<std::path::Path> + Send>(
&self,
mountpoint: P,
mount_type: MountType,
) -> Result<(), Error> {
mount_cifs(
&self.hostname,
&self.path,
&self.username,
self.password.as_ref().map(|p| p.as_str()),
mountpoint,
mount_type,
)
.await
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("Cifs");
sha.update(self.hostname.as_bytes());
sha.update(self.path.as_os_str().as_bytes());
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,83 @@
use std::fmt::Display;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use lazy_format::lazy_format;
use sha2::Sha256;
use tokio::process::Command;
use super::FileSystem;
use crate::disk::mount::filesystem::default_mount_command;
use crate::prelude::*;
use crate::util::Invoke;
pub struct EcryptFS<EncryptedDir: AsRef<Path>, Key: AsRef<str>> {
encrypted_dir: EncryptedDir,
key: Key,
}
impl<EncryptedDir: AsRef<Path>, Key: AsRef<str>> EcryptFS<EncryptedDir, Key> {
pub fn new(encrypted_dir: EncryptedDir, key: Key) -> Self {
EcryptFS { encrypted_dir, key }
}
}
impl<EncryptedDir: AsRef<Path> + Send + Sync, Key: AsRef<str> + Send + Sync> FileSystem
for EcryptFS<EncryptedDir, Key>
{
fn mount_type(&self) -> Option<impl AsRef<str>> {
Some("ecryptfs")
}
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(&self.encrypted_dir))
}
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
[
Box::new(lazy_format!(
"key=passphrase:passphrase_passwd={}",
self.key.as_ref()
)) as Box<dyn Display>,
Box::new("ecryptfs_cipher=aes"),
Box::new("ecryptfs_key_bytes=32"),
Box::new("ecryptfs_passthrough=n"),
Box::new("ecryptfs_enable_filename_crypto=y"),
Box::new("no_sig_cache"),
]
}
async fn mount<P: AsRef<Path> + Send>(
&self,
mountpoint: P,
mount_type: super::MountType,
) -> Result<(), Error> {
self.pre_mount(mountpoint.as_ref(), mount_type).await?;
Command::new("mount")
.args(
default_mount_command(self, mountpoint, mount_type)
.await?
.get_args(),
)
.input(Some(&mut std::io::Cursor::new(b"\n")))
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok(())
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("EcryptFS");
sha.update(
tokio::fs::canonicalize(self.encrypted_dir.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.encrypted_dir.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,25 @@
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use sha2::Sha256;
use super::FileSystem;
use crate::prelude::*;
pub struct EfiVarFs;
impl FileSystem for EfiVarFs {
fn mount_type(&self) -> Option<impl AsRef<str>> {
Some("efivarfs")
}
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some("efivarfs"))
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("EfiVarFs");
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,50 @@
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use super::{FileSystem, MountType};
use crate::Error;
use crate::util::Invoke;
pub async fn mount_httpdirfs(url: &Url, mountpoint: impl AsRef<Path>) -> Result<(), Error> {
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
let mut cmd = tokio::process::Command::new("httpdirfs");
cmd.arg("--cache")
.arg("--single-file-mode")
.arg(url.as_str())
.arg(mountpoint.as_ref());
cmd.invoke(crate::ErrorKind::Filesystem).await?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpDirFS {
url: Url,
}
impl HttpDirFS {
pub fn new(url: Url) -> Self {
HttpDirFS { url }
}
}
impl FileSystem for HttpDirFS {
async fn mount<P: AsRef<Path> + Send>(
&self,
mountpoint: P,
_mount_type: MountType,
) -> Result<(), Error> {
mount_httpdirfs(&self.url, mountpoint).await
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("HttpDirFS");
sha.update(self.url.as_str());
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,156 @@
use std::ffi::OsStr;
use std::fmt::Display;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::str::FromStr;
use clap::Parser;
use clap::builder::ValueParserFactory;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::process::Command;
use ts_rs::TS;
use super::FileSystem;
use crate::disk::mount::filesystem::MountType;
use crate::prelude::*;
use crate::util::{FromStrParser, Invoke};
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct IdMap {
pub from_id: u32,
pub to_id: u32,
pub range: u32,
}
impl IdMap {
pub fn stack(a: Vec<IdMap>, b: Vec<IdMap>) -> Vec<IdMap> {
let mut res = Vec::with_capacity(a.len() + b.len());
res.extend_from_slice(&a);
for mut b in b {
for a in &a {
if a.from_id <= b.to_id && a.from_id + a.range > b.to_id {
b.to_id += a.to_id;
}
}
res.push(b);
}
res
}
}
impl FromStr for IdMap {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let split = s.splitn(3, ":").collect::<Vec<_>>();
if let Some([u, k, r]) = split.get(0..3) {
Ok(Self {
from_id: u.parse()?,
to_id: k.parse()?,
range: r.parse()?,
})
} else if let Some([u, k]) = split.get(0..2) {
Ok(Self {
from_id: u.parse()?,
to_id: k.parse()?,
range: 1,
})
} else {
Err(Error::new(
eyre!("{s} is not a valid idmap"),
ErrorKind::ParseNumber,
))
}
}
}
impl ValueParserFactory for IdMap {
type Parser = FromStrParser<IdMap>;
fn value_parser() -> Self::Parser {
<Self::Parser>::new()
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IdMapped<Fs: FileSystem> {
filesystem: Fs,
idmap: Vec<IdMap>,
}
impl<Fs: FileSystem> IdMapped<Fs> {
pub fn new(filesystem: Fs, idmap: Vec<IdMap>) -> Self {
Self { filesystem, idmap }
}
}
impl<Fs: FileSystem> FileSystem for IdMapped<Fs> {
fn mount_type(&self) -> Option<impl AsRef<str>> {
self.filesystem.mount_type()
}
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<OsStr>> {
self.filesystem.extra_args()
}
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
self.filesystem
.mount_options()
.into_iter()
.map(|a| Box::new(a) as Box<dyn Display>)
.chain(if self.idmap.is_empty() {
None
} else {
use std::fmt::Write;
let mut option = "X-mount.idmap=".to_owned();
for i in &self.idmap {
write!(&mut option, "b:{}:{}:{} ", i.from_id, i.to_id, i.range).unwrap();
}
Some(Box::new(option) as Box<dyn Display>)
})
}
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
self.filesystem.source().await
}
async fn pre_mount(&self, mountpoint: &Path, mount_type: MountType) -> Result<(), Error> {
self.filesystem.pre_mount(mountpoint, mount_type).await?;
let info = tokio::fs::metadata(mountpoint).await?;
for i in &self.idmap {
let uid_in_range = i.from_id <= info.uid() && i.from_id + i.range > info.uid();
let gid_in_range = i.from_id <= info.gid() && i.from_id + i.range > info.gid();
if uid_in_range || gid_in_range {
Command::new("chown")
.arg(format!(
"{uid}:{gid}",
uid = if uid_in_range {
i.to_id + info.uid() - i.from_id
} else {
info.uid()
},
gid = if gid_in_range {
i.to_id + info.gid() - i.from_id
} else {
info.gid()
},
))
.arg(&mountpoint)
.invoke(crate::ErrorKind::Filesystem)
.await?;
}
}
Ok(())
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("IdMapped");
sha.update(self.filesystem.source_hash().await?);
sha.update(usize::to_be_bytes(self.idmap.len()));
for i in &self.idmap {
sha.update(u32::to_be_bytes(i.from_id));
sha.update(u32::to_be_bytes(i.to_id));
sha.update(u32::to_be_bytes(i.range));
}
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,33 @@
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use sha2::Sha256;
use super::FileSystem;
use crate::prelude::*;
pub struct Label<S: AsRef<str>> {
label: S,
}
impl<S: AsRef<str>> Label<S> {
pub fn new(label: S) -> Self {
Label { label }
}
}
impl<S: AsRef<str> + Send + Sync> FileSystem for Label<S> {
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>> {
["-L", self.label.as_ref()]
}
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(None::<&Path>)
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("Label");
sha.update(self.label.as_ref().as_bytes());
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,63 @@
use std::fmt::Display;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use lazy_format::lazy_format;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use super::FileSystem;
use crate::prelude::*;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoopDev<LogicalName: AsRef<Path>> {
logicalname: LogicalName,
offset: u64,
size: u64,
}
impl<LogicalName: AsRef<Path>> LoopDev<LogicalName> {
pub fn new(logicalname: LogicalName, offset: u64, size: u64) -> Self {
Self {
logicalname,
offset,
size,
}
}
}
impl<LogicalName: AsRef<Path> + Send + Sync> FileSystem for LoopDev<LogicalName> {
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(
tokio::fs::canonicalize(self.logicalname.as_ref()).await?,
))
}
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
[
Box::new("loop") as Box<dyn Display>,
Box::new(lazy_format!("offset={}", self.offset)),
Box::new(lazy_format!("sizelimit={}", self.size)),
]
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
let mut sha = Sha256::new();
sha.update("LoopDev");
sha.update(
tokio::fs::canonicalize(self.logicalname.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.logicalname.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
sha.update(&u64::to_be_bytes(self.offset)[..]);
Ok(sha.finalize())
}
}

View File

@@ -0,0 +1,116 @@
use std::ffi::OsStr;
use std::fmt::{Display, Write};
use std::path::Path;
use digest::OutputSizeUser;
use digest::generic_array::GenericArray;
use futures::Future;
use sha2::Sha256;
use tokio::process::Command;
use crate::prelude::*;
use crate::util::Invoke;
pub mod backupfs;
pub mod bind;
pub mod block_dev;
pub mod cifs;
pub mod ecryptfs;
pub mod efivarfs;
pub mod httpdirfs;
pub mod idmapped;
pub mod label;
pub mod loop_dev;
pub mod overlayfs;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MountType {
ReadOnly,
ReadWrite,
}
pub use MountType::*;
pub(self) async fn default_mount_command(
fs: &(impl FileSystem + ?Sized),
mountpoint: impl AsRef<Path> + Send,
mount_type: MountType,
) -> Result<std::process::Command, Error> {
let mut cmd = std::process::Command::new("mount");
if mount_type == ReadOnly {
cmd.arg("-r");
}
cmd.args(fs.extra_args());
if let Some(ty) = fs.mount_type() {
cmd.arg("-t").arg(ty.as_ref());
}
if let Some(options) = fs
.mount_options()
.into_iter()
.fold(None, |acc: Option<String>, x| match acc {
Some(mut s) => {
write!(s, ",{}", x).unwrap();
Some(s)
}
None => Some(x.to_string()),
})
{
cmd.arg("-o").arg(options);
}
if let Some(source) = fs.source().await? {
cmd.arg(source.as_ref());
}
cmd.arg(mountpoint.as_ref());
Ok(cmd)
}
pub(self) async fn default_mount_impl(
fs: &(impl FileSystem + ?Sized),
mountpoint: impl AsRef<Path> + Send,
mount_type: MountType,
) -> Result<(), Error> {
fs.pre_mount(mountpoint.as_ref(), mount_type).await?;
Command::from(default_mount_command(fs, mountpoint, mount_type).await?)
.capture(false)
.invoke(ErrorKind::Filesystem)
.await?;
Ok(())
}
pub trait FileSystem: Send + Sync {
fn mount_type(&self) -> Option<impl AsRef<str>> {
None::<&str>
}
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<OsStr>> {
[] as [&str; 0]
}
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
[] as [&str; 0]
}
fn source(&self) -> impl Future<Output = Result<Option<impl AsRef<Path>>, Error>> + Send {
async { Ok(None::<&Path>) }
}
fn pre_mount(
&self,
mountpoint: &Path,
#[allow(unused_variables)] mount_type: MountType,
) -> impl Future<Output = Result<(), Error>> + Send {
async move {
tokio::fs::create_dir_all(mountpoint).await?;
Ok(())
}
}
fn mount<P: AsRef<Path> + Send>(
&self,
mountpoint: P,
mount_type: MountType,
) -> impl Future<Output = Result<(), Error>> + Send {
default_mount_impl(self, mountpoint, mount_type)
}
fn source_hash(
&self,
) -> impl Future<
Output = Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error>,
> + Send;
}

View File

@@ -0,0 +1,162 @@
use std::fmt::Display;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use sha2::Sha256;
use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite};
use crate::disk::mount::guard::{GenericMountGuard, MountGuard};
use crate::prelude::*;
use crate::util::io::TmpDir;
pub struct OverlayFs<P0: AsRef<Path>, P1: AsRef<Path>, P2: AsRef<Path>> {
lower: P0,
upper: P1,
work: P2,
}
impl<P0: AsRef<Path>, P1: AsRef<Path>, P2: AsRef<Path>> OverlayFs<P0, P1, P2> {
pub fn new(lower: P0, upper: P1, work: P2) -> Self {
Self { lower, upper, work }
}
}
impl<P0: AsRef<Path> + Send + Sync, P1: AsRef<Path> + Send + Sync, P2: AsRef<Path> + Send + Sync>
FileSystem for OverlayFs<P0, P1, P2>
{
fn mount_type(&self) -> Option<impl AsRef<str>> {
Some("overlay")
}
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some("overlay"))
}
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
[
Box::new(lazy_format!("lowerdir={}", self.lower.as_ref().display()))
as Box<dyn Display>,
Box::new(lazy_format!("upperdir={}", self.upper.as_ref().display())),
Box::new(lazy_format!("workdir={}", self.work.as_ref().display())),
]
}
async fn pre_mount(&self, mountpoint: &Path, _: MountType) -> Result<(), Error> {
tokio::fs::create_dir_all(self.upper.as_ref()).await?;
tokio::fs::create_dir_all(self.work.as_ref()).await?;
tokio::fs::create_dir_all(mountpoint).await?;
Ok(())
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
tokio::fs::create_dir_all(self.upper.as_ref()).await?;
tokio::fs::create_dir_all(self.work.as_ref()).await?;
let mut sha = Sha256::new();
sha.update("OverlayFs");
sha.update(
tokio::fs::canonicalize(self.lower.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.lower.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
sha.update(
tokio::fs::canonicalize(self.upper.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.upper.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
sha.update(
tokio::fs::canonicalize(self.work.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.upper.as_ref().display().to_string(),
)
})?
.as_os_str()
.as_bytes(),
);
Ok(sha.finalize())
}
}
#[derive(Debug)]
pub struct OverlayGuard<G: GenericMountGuard> {
lower: Option<G>,
upper: Option<TmpDir>,
inner_guard: MountGuard,
}
impl<G: GenericMountGuard> OverlayGuard<G> {
pub async fn mount(lower: G, mountpoint: impl AsRef<Path>) -> Result<Self, Error> {
let upper = TmpDir::new().await?;
let inner_guard = MountGuard::mount(
&OverlayFs::new(
lower.path(),
upper.as_ref().join("upper"),
upper.as_ref().join("work"),
),
mountpoint,
ReadWrite,
)
.await?;
Ok(Self {
lower: Some(lower),
upper: Some(upper),
inner_guard,
})
}
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
self.inner_guard.take().unmount(delete_mountpoint).await?;
if let Some(lower) = self.lower.take() {
lower.unmount().await?;
}
if let Some(upper) = self.upper.take() {
upper.delete().await?;
}
Ok(())
}
pub fn take(&mut self) -> Self {
Self {
lower: self.lower.take(),
upper: self.upper.take(),
inner_guard: self.inner_guard.take(),
}
}
}
impl<G: GenericMountGuard> GenericMountGuard for OverlayGuard<G> {
fn path(&self) -> &Path {
self.inner_guard.path()
}
async fn unmount(self) -> Result<(), Error> {
self.unmount(false).await
}
}
impl<G: GenericMountGuard> Drop for OverlayGuard<G> {
fn drop(&mut self) {
let lower = self.lower.take();
let upper = self.upper.take();
let guard = self.inner_guard.take();
if lower.is_some() || upper.is_some() || guard.mounted {
tokio::spawn(async move {
guard.unmount(false).await.log_err();
if let Some(lower) = lower {
lower.unmount().await.log_err();
}
if let Some(upper) = upper {
upper.delete().await.log_err();
}
});
}
}
}