Skip to main content

aster_forge_webdav/
backend.rs

1//! Product adapter ports consumed by the WebDAV protocol layer.
2
3use std::collections::HashMap;
4use std::future::Future;
5use std::io::SeekFrom;
6use std::pin::Pin;
7use std::time::{Duration, SystemTime};
8
9use async_trait::async_trait;
10use bytes::{Buf, Bytes};
11use futures::Stream;
12use http::StatusCode;
13
14use crate::{DavPath, DavXmlElement};
15
16/// Stream used for product-independent WebDAV content transfer.
17pub type DavContentStream =
18    Pin<Box<dyn Stream<Item = Result<Bytes, DavBackendError>> + Send + 'static>>;
19
20/// Stable backend failure categories mapped by the protocol layer.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum DavBackendErrorKind {
23    NotFound,
24    Forbidden,
25    Conflict,
26    AlreadyExists,
27    InsufficientStorage,
28    PayloadTooLarge,
29    Locked,
30    InvalidInput,
31    Unsupported,
32    Internal,
33}
34
35/// Product-neutral failure returned by a product adapter.
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37#[error("WebDAV backend operation failed: {kind:?}")]
38pub struct DavBackendError {
39    pub kind: DavBackendErrorKind,
40}
41
42impl DavBackendError {
43    #[must_use]
44    pub const fn new(kind: DavBackendErrorKind) -> Self {
45        Self { kind }
46    }
47}
48
49/// Low-level file-system failure exposed by the product adapter.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
51pub enum FsError {
52    #[error("not found")]
53    NotFound,
54    #[error("forbidden")]
55    Forbidden,
56    #[error("general failure")]
57    GeneralFailure,
58    #[error("already exists")]
59    Exists,
60    #[error("insufficient storage")]
61    InsufficientStorage,
62    #[error("too large")]
63    TooLarge,
64    #[error("bad request")]
65    BadRequest,
66}
67
68impl From<FsError> for DavBackendError {
69    fn from(error: FsError) -> Self {
70        let kind = match error {
71            FsError::NotFound => DavBackendErrorKind::NotFound,
72            FsError::Forbidden => DavBackendErrorKind::Forbidden,
73            FsError::GeneralFailure => DavBackendErrorKind::Internal,
74            FsError::Exists => DavBackendErrorKind::AlreadyExists,
75            FsError::InsufficientStorage => DavBackendErrorKind::InsufficientStorage,
76            FsError::TooLarge => DavBackendErrorKind::PayloadTooLarge,
77            FsError::BadRequest => DavBackendErrorKind::InvalidInput,
78        };
79        Self::new(kind)
80    }
81}
82
83pub type FsResult<T> = Result<T, FsError>;
84pub type FsFuture<'a, T> = Pin<Box<dyn Future<Output = FsResult<T>> + Send + 'a>>;
85pub type FsStream<T> = Pin<Box<dyn Stream<Item = FsResult<T>> + Send>>;
86
87/// WebDAV resource type.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub enum DavResourceKind {
90    File,
91    Collection,
92}
93
94/// Protocol-visible state used to evaluate one resource referenced by an `If` header.
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
96pub struct DavIfResourceState {
97    pub etag: Option<String>,
98    pub lock_tokens: Vec<String>,
99}
100
101/// Product adapter used while evaluating WebDAV `If` conditions.
102#[async_trait]
103pub trait DavIfStateResolver: Send + Sync {
104    async fn resolve_if_state(&self, path: &DavPath)
105    -> Result<DavIfResourceState, DavBackendError>;
106}
107
108/// Metadata loading mode for directory entries.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum ReadDirMeta {
111    Data,
112}
113
114/// File open contract selected by the protocol planner.
115#[derive(Debug, Clone, Default, PartialEq, Eq)]
116pub struct OpenOptions {
117    pub read: bool,
118    pub write: bool,
119    pub append: bool,
120    pub truncate: bool,
121    pub create: bool,
122    pub create_new: bool,
123    pub size: Option<u64>,
124    pub checksum: Option<String>,
125}
126
127impl OpenOptions {
128    #[must_use]
129    pub fn read() -> Self {
130        Self {
131            read: true,
132            ..Self::default()
133        }
134    }
135
136    #[must_use]
137    pub fn write() -> Self {
138        Self {
139            write: true,
140            ..Self::default()
141        }
142    }
143}
144
145/// Protocol-visible resource metadata supplied by the product adapter.
146pub trait DavMetaData: Send + Sync {
147    fn len(&self) -> u64;
148    fn modified(&self) -> FsResult<SystemTime>;
149    fn is_dir(&self) -> bool;
150    fn etag(&self) -> Option<String>;
151    fn content_type(&self) -> Option<&str> {
152        None
153    }
154    fn created(&self) -> FsResult<SystemTime>;
155    fn is_empty(&self) -> bool {
156        self.len() == 0
157    }
158    fn is_file(&self) -> bool {
159        !self.is_dir()
160    }
161}
162
163/// One directory entry returned by a product adapter.
164pub trait DavDirEntry: Send {
165    fn name(&self) -> Vec<u8>;
166    fn metadata<'a>(&'a self) -> FsFuture<'a, Box<dyn DavMetaData>>;
167}
168
169/// Open file handle supplied by a product adapter.
170pub trait DavFile: Send {
171    fn metadata<'a>(&'a mut self) -> FsFuture<'a, Box<dyn DavMetaData>>;
172    fn read_bytes(&mut self, count: usize) -> FsFuture<'_, Bytes>;
173    fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()>;
174    fn write_buf(&mut self, buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()>;
175    fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64>;
176    fn flush(&mut self) -> FsFuture<'_, ()>;
177}
178
179/// Stored dead property exchanged with the product adapter.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct DavProp {
182    pub name: String,
183    pub prefix: Option<String>,
184    pub namespace: Option<String>,
185    pub xml: Option<Vec<u8>>,
186}
187
188/// Canonical resource and dead-property backend port.
189pub trait DavFileSystem: Send + Sync {
190    fn open<'a>(
191        &'a self,
192        path: &'a DavPath,
193        options: OpenOptions,
194    ) -> FsFuture<'a, Box<dyn DavFile>>;
195    fn read_dir<'a>(
196        &'a self,
197        path: &'a DavPath,
198        meta: ReadDirMeta,
199    ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>>;
200    fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>>;
201    fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()>;
202    fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()>;
203    fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()>;
204    fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()>;
205    fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()>;
206
207    fn get_quota(&self) -> FsFuture<'_, (u64, Option<u64>)> {
208        Box::pin(async { Ok((0, None)) })
209    }
210
211    fn have_props<'a>(
212        &'a self,
213        _path: &'a DavPath,
214    ) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
215        Box::pin(async { false })
216    }
217
218    fn get_props<'a>(
219        &'a self,
220        _path: &'a DavPath,
221        _do_content: bool,
222    ) -> FsFuture<'a, Vec<DavProp>> {
223        Box::pin(async { Ok(Vec::new()) })
224    }
225
226    /// Serial fallback that calls [`DavFileSystem::get_props`] once per path.
227    ///
228    /// Production adapters should override this method when the persistence layer supports a
229    /// genuine batch query.
230    fn get_props_many<'a>(
231        &'a self,
232        paths: &'a [DavPath],
233        do_content: bool,
234    ) -> FsFuture<'a, HashMap<DavPath, Vec<DavProp>>> {
235        Box::pin(async move {
236            let mut result = HashMap::with_capacity(paths.len());
237            for path in paths {
238                result.insert(path.clone(), self.get_props(path, do_content).await?);
239            }
240            Ok(result)
241        })
242    }
243
244    fn patch_props<'a>(
245        &'a self,
246        _path: &'a DavPath,
247        _patches: Vec<(bool, DavProp)>,
248    ) -> FsFuture<'a, Vec<(StatusCode, DavProp)>> {
249        Box::pin(async { Ok(Vec::new()) })
250    }
251}
252
253/// Protocol-visible lock state persisted by the product adapter.
254#[derive(Debug, Clone)]
255pub struct DavLock {
256    pub token: String,
257    pub path: Box<DavPath>,
258    pub principal: Option<String>,
259    pub owner: Option<Box<DavXmlElement>>,
260    pub timeout_at: Option<SystemTime>,
261    pub timeout: Option<Duration>,
262    pub shared: bool,
263    pub deep: bool,
264}
265
266pub type LsFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum DavLockPreflightError {
270    LimitExceeded,
271    GeneralFailure,
272}
273
274#[derive(Debug, Clone)]
275pub enum DavLockError {
276    Conflict(Box<DavLock>),
277    TokenMismatch,
278    LimitExceeded,
279    Backend,
280}
281
282/// Canonical lock persistence and conflict backend port.
283pub trait DavLockSystem: Send + Sync {
284    fn prepare_lock(&self, _path: &DavPath) -> LsFuture<'_, Result<(), DavLockPreflightError>> {
285        Box::pin(async { Ok(()) })
286    }
287
288    fn lock(
289        &self,
290        path: &DavPath,
291        principal: Option<&str>,
292        owner: Option<&DavXmlElement>,
293        timeout: Option<Duration>,
294        shared: bool,
295        deep: bool,
296    ) -> LsFuture<'_, Result<DavLock, DavLockError>>;
297
298    fn unlock(&self, path: &DavPath, token: &str) -> LsFuture<'_, Result<(), DavLockError>>;
299    fn refresh(
300        &self,
301        path: &DavPath,
302        token: &str,
303        timeout: Option<Duration>,
304    ) -> LsFuture<'_, Result<DavLock, DavLockError>>;
305    fn check(
306        &self,
307        path: &DavPath,
308        principal: Option<&str>,
309        ignore_principal: bool,
310        deep: bool,
311        submitted_tokens: &[String],
312    ) -> LsFuture<'_, Result<(), DavLock>>;
313    fn discover(&self, path: &DavPath) -> LsFuture<'_, Vec<DavLock>>;
314    fn discover_many<'a>(
315        &'a self,
316        paths: &'a [DavPath],
317    ) -> LsFuture<'a, HashMap<DavPath, Vec<DavLock>>> {
318        Box::pin(async move {
319            let mut result = HashMap::with_capacity(paths.len());
320            for path in paths {
321                result.insert(path.clone(), self.discover(path).await);
322            }
323            result
324        })
325    }
326    fn conflicting_locks(&self, path: &DavPath, deep: bool) -> LsFuture<'_, Vec<DavLock>>;
327    fn delete(&self, path: &DavPath) -> LsFuture<'_, Result<(), DavLockError>>;
328}