use crate::async_storage::{AsyncAuthorityStore, AsyncObjectStore};
use crate::facade::Checkout;
use crate::foundation::{GenerationId, MountId, VolumeId};
use crate::kernel::{NamespacePath, NamespacePathError};
use crate::model::{AccessMode, ConsistencyMode};
use crate::path::{PathError, PortablePath};
use std::collections::BTreeMap;
use thiserror::Error;
pub struct MountedCheckout<A, O> {
mount_id: MountId,
path: PortablePath,
checkout: Checkout<A, O>,
}
impl<A, O> MountedCheckout<A, O> {
#[must_use]
pub const fn mount_id(&self) -> MountId {
self.mount_id
}
#[must_use]
pub const fn path(&self) -> &PortablePath {
&self.path
}
#[must_use]
pub const fn volume_id(&self) -> VolumeId {
self.checkout.volume_id()
}
#[must_use]
pub const fn checkout(&self) -> &Checkout<A, O> {
&self.checkout
}
#[must_use]
pub const fn checkout_mut(&mut self) -> &mut Checkout<A, O> {
&mut self.checkout
}
}
pub struct RoutedCheckout<'a, A, O> {
pub mount_id: MountId,
pub path: NamespacePath,
pub checkout: &'a mut Checkout<A, O>,
}
pub struct MountedView<A, O> {
bindings: BTreeMap<PortablePath, MountedCheckout<A, O>>,
}
impl<A, O> MountedView<A, O> {
#[must_use]
pub fn builder() -> MountedViewBuilder<A, O> {
MountedViewBuilder::default()
}
#[must_use]
pub fn resolve(&self, path: &PortablePath) -> Option<&MountedCheckout<A, O>> {
self.bindings
.values()
.filter(|binding| path.is_within(&binding.path))
.max_by_key(|binding| binding.path.depth())
}
pub fn route_mut(
&mut self,
path: &PortablePath,
) -> Result<RoutedCheckout<'_, A, O>, MountError> {
let selected = self
.bindings
.keys()
.filter(|candidate| path.is_within(candidate))
.max_by_key(|candidate| candidate.depth())
.cloned()
.ok_or(MountError::UnmappedPath)?;
let binding = self
.bindings
.get_mut(&selected)
.ok_or(MountError::UnmappedPath)?;
let relative = relative_path(path, &selected, binding.checkout.volume_config().limits)?;
Ok(RoutedCheckout {
mount_id: binding.mount_id,
path: NamespacePath::from_portable_in_profile(
&relative,
binding.checkout.volume_config().profile,
binding.checkout.volume_config().limits,
)
.map_err(MountError::InvalidNamespacePath)?,
checkout: &mut binding.checkout,
})
}
#[must_use]
pub fn snapshot(&self) -> MountedViewSnapshot {
MountedViewSnapshot {
bindings: self
.bindings
.values()
.map(|binding| MountedGeneration {
mount_id: binding.mount_id,
path: binding.path.clone(),
volume_id: binding.checkout.volume_id(),
generation: binding.checkout.generation_id(),
access: binding.checkout.mode().access,
consistency: binding.checkout.mode().consistency,
})
.collect(),
}
}
pub fn validate_rename(
&self,
source: &PortablePath,
destination: &PortablePath,
) -> Result<(), MountError> {
let source_binding = self.resolve(source).ok_or(MountError::UnmappedPath)?;
let destination_binding = self.resolve(destination).ok_or(MountError::UnmappedPath)?;
if source_binding.mount_id != destination_binding.mount_id {
return Err(MountError::CrossVolume);
}
Ok(())
}
}
pub struct MountedViewBuilder<A, O> {
bindings: BTreeMap<PortablePath, MountedCheckout<A, O>>,
}
impl<A, O> Default for MountedViewBuilder<A, O> {
fn default() -> Self {
Self {
bindings: BTreeMap::new(),
}
}
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> MountedViewBuilder<A, O> {
pub fn mount(mut self, path: &str, checkout: Checkout<A, O>) -> Result<Self, MountError> {
let path = PortablePath::parse(path, checkout.volume_config().limits)
.map_err(MountError::InvalidPath)?;
if self.bindings.contains_key(&path) {
return Err(MountError::DuplicatePath);
}
let mount_id = MountId::new();
self.bindings.insert(
path.clone(),
MountedCheckout {
mount_id,
path,
checkout,
},
);
Ok(self)
}
pub fn build(self) -> Result<MountedView<A, O>, MountError> {
if !self
.bindings
.keys()
.any(|path| path.as_str() == PortablePath::ROOT)
{
return Err(MountError::MissingRoot);
}
Ok(MountedView {
bindings: self.bindings,
})
}
}
fn relative_path(
path: &PortablePath,
mount: &PortablePath,
limits: crate::model::VolumeLimits,
) -> Result<PortablePath, MountError> {
if mount.as_str() == PortablePath::ROOT {
return Ok(path.clone());
}
if path == mount {
return PortablePath::parse(PortablePath::ROOT, limits).map_err(MountError::InvalidPath);
}
let relative = path
.as_str()
.strip_prefix(mount.as_str())
.ok_or(MountError::UnmappedPath)?;
PortablePath::parse(relative, limits).map_err(MountError::InvalidPath)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MountedViewSnapshot {
pub bindings: Vec<MountedGeneration>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MountedGeneration {
pub mount_id: MountId,
pub path: PortablePath,
pub volume_id: VolumeId,
pub generation: GenerationId,
pub access: AccessMode,
pub consistency: ConsistencyMode,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum MountError {
#[error("mount path is already bound")]
DuplicatePath,
#[error("mounted view has no root binding")]
MissingRoot,
#[error("path is not mapped to a volume")]
UnmappedPath,
#[error("cross-volume rename is not supported")]
CrossVolume,
#[error("invalid mounted path: {0}")]
InvalidPath(PathError),
#[error("invalid routed namespace path: {0}")]
InvalidNamespacePath(NamespacePathError),
}
#[cfg(test)]
#[path = "tests/mount.rs"]
mod tests;