1use crate::async_storage::{AsyncAuthorityStore, AsyncObjectStore};
4use crate::facade::Checkout;
5use crate::foundation::{GenerationId, MountId, VolumeId};
6use crate::kernel::{NamespacePath, NamespacePathError};
7use crate::model::{AccessMode, ConsistencyMode};
8use crate::path::{PathError, PortablePath};
9use std::collections::BTreeMap;
10use thiserror::Error;
11
12pub struct MountedCheckout<A, O> {
14 mount_id: MountId,
15 path: PortablePath,
16 checkout: Checkout<A, O>,
17}
18
19impl<A, O> MountedCheckout<A, O> {
20 #[must_use]
22 pub const fn mount_id(&self) -> MountId {
23 self.mount_id
24 }
25
26 #[must_use]
28 pub const fn path(&self) -> &PortablePath {
29 &self.path
30 }
31
32 #[must_use]
34 pub const fn volume_id(&self) -> VolumeId {
35 self.checkout.volume_id()
36 }
37
38 #[must_use]
40 pub const fn checkout(&self) -> &Checkout<A, O> {
41 &self.checkout
42 }
43
44 #[must_use]
46 pub const fn checkout_mut(&mut self) -> &mut Checkout<A, O> {
47 &mut self.checkout
48 }
49}
50
51pub struct RoutedCheckout<'a, A, O> {
53 pub mount_id: MountId,
55 pub path: NamespacePath,
57 pub checkout: &'a mut Checkout<A, O>,
59}
60
61pub struct MountedView<A, O> {
63 bindings: BTreeMap<PortablePath, MountedCheckout<A, O>>,
64}
65
66impl<A, O> MountedView<A, O> {
67 #[must_use]
69 pub fn builder() -> MountedViewBuilder<A, O> {
70 MountedViewBuilder::default()
71 }
72
73 #[must_use]
75 pub fn resolve(&self, path: &PortablePath) -> Option<&MountedCheckout<A, O>> {
76 self.bindings
77 .values()
78 .filter(|binding| path.is_within(&binding.path))
79 .max_by_key(|binding| binding.path.depth())
80 }
81
82 pub fn route_mut(
89 &mut self,
90 path: &PortablePath,
91 ) -> Result<RoutedCheckout<'_, A, O>, MountError> {
92 let selected = self
93 .bindings
94 .keys()
95 .filter(|candidate| path.is_within(candidate))
96 .max_by_key(|candidate| candidate.depth())
97 .cloned()
98 .ok_or(MountError::UnmappedPath)?;
99 let binding = self
100 .bindings
101 .get_mut(&selected)
102 .ok_or(MountError::UnmappedPath)?;
103 let relative = relative_path(path, &selected, binding.checkout.volume_config().limits)?;
104 Ok(RoutedCheckout {
105 mount_id: binding.mount_id,
106 path: NamespacePath::from_portable_in_profile(
107 &relative,
108 binding.checkout.volume_config().profile,
109 binding.checkout.volume_config().limits,
110 )
111 .map_err(MountError::InvalidNamespacePath)?,
112 checkout: &mut binding.checkout,
113 })
114 }
115
116 #[must_use]
118 pub fn snapshot(&self) -> MountedViewSnapshot {
119 MountedViewSnapshot {
120 bindings: self
121 .bindings
122 .values()
123 .map(|binding| MountedGeneration {
124 mount_id: binding.mount_id,
125 path: binding.path.clone(),
126 volume_id: binding.checkout.volume_id(),
127 generation: binding.checkout.generation_id(),
128 access: binding.checkout.mode().access,
129 consistency: binding.checkout.mode().consistency,
130 })
131 .collect(),
132 }
133 }
134
135 pub fn validate_rename(
142 &self,
143 source: &PortablePath,
144 destination: &PortablePath,
145 ) -> Result<(), MountError> {
146 let source_binding = self.resolve(source).ok_or(MountError::UnmappedPath)?;
147 let destination_binding = self.resolve(destination).ok_or(MountError::UnmappedPath)?;
148 if source_binding.mount_id != destination_binding.mount_id {
149 return Err(MountError::CrossVolume);
150 }
151 Ok(())
152 }
153}
154
155pub struct MountedViewBuilder<A, O> {
157 bindings: BTreeMap<PortablePath, MountedCheckout<A, O>>,
158}
159
160impl<A, O> Default for MountedViewBuilder<A, O> {
161 fn default() -> Self {
162 Self {
163 bindings: BTreeMap::new(),
164 }
165 }
166}
167
168impl<A: AsyncAuthorityStore, O: AsyncObjectStore> MountedViewBuilder<A, O> {
169 pub fn mount(mut self, path: &str, checkout: Checkout<A, O>) -> Result<Self, MountError> {
175 let path = PortablePath::parse(path, checkout.volume_config().limits)
176 .map_err(MountError::InvalidPath)?;
177 if self.bindings.contains_key(&path) {
178 return Err(MountError::DuplicatePath);
179 }
180 let mount_id = MountId::new();
181 self.bindings.insert(
182 path.clone(),
183 MountedCheckout {
184 mount_id,
185 path,
186 checkout,
187 },
188 );
189 Ok(self)
190 }
191
192 pub fn build(self) -> Result<MountedView<A, O>, MountError> {
198 if !self
199 .bindings
200 .keys()
201 .any(|path| path.as_str() == PortablePath::ROOT)
202 {
203 return Err(MountError::MissingRoot);
204 }
205 Ok(MountedView {
206 bindings: self.bindings,
207 })
208 }
209}
210
211fn relative_path(
212 path: &PortablePath,
213 mount: &PortablePath,
214 limits: crate::model::VolumeLimits,
215) -> Result<PortablePath, MountError> {
216 if mount.as_str() == PortablePath::ROOT {
217 return Ok(path.clone());
218 }
219 if path == mount {
220 return PortablePath::parse(PortablePath::ROOT, limits).map_err(MountError::InvalidPath);
221 }
222 let relative = path
223 .as_str()
224 .strip_prefix(mount.as_str())
225 .ok_or(MountError::UnmappedPath)?;
226 PortablePath::parse(relative, limits).map_err(MountError::InvalidPath)
227}
228
229#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct MountedViewSnapshot {
232 pub bindings: Vec<MountedGeneration>,
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
238pub struct MountedGeneration {
239 pub mount_id: MountId,
241 pub path: PortablePath,
243 pub volume_id: VolumeId,
245 pub generation: GenerationId,
247 pub access: AccessMode,
249 pub consistency: ConsistencyMode,
251}
252
253#[derive(Clone, Debug, Eq, Error, PartialEq)]
255pub enum MountError {
256 #[error("mount path is already bound")]
258 DuplicatePath,
259 #[error("mounted view has no root binding")]
261 MissingRoot,
262 #[error("path is not mapped to a volume")]
264 UnmappedPath,
265 #[error("cross-volume rename is not supported")]
267 CrossVolume,
268 #[error("invalid mounted path: {0}")]
270 InvalidPath(PathError),
271 #[error("invalid routed namespace path: {0}")]
273 InvalidNamespacePath(NamespacePathError),
274}
275
276#[cfg(test)]
277#[path = "tests/mount.rs"]
278mod tests;