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::hash::Hash;
6use std::pin::Pin;
7use std::time::{Duration, SystemTime};
8
9use aster_forge_utils::http_range::HttpByteRange;
10use async_trait::async_trait;
11use bytes::Bytes;
12use futures::Stream;
13use http::StatusCode;
14
15use crate::{DavPath, DavXmlElement};
16
17/// Stream used for product-independent `WebDAV` content transfer.
18pub type DavContentStream =
19    Pin<Box<dyn Stream<Item = Result<Bytes, DavBackendError>> + Send + 'static>>;
20
21/// Opened representation stream and the exact number of bytes it is expected to yield.
22pub struct DavOpenedDownload {
23    pub stream: DavContentStream,
24    pub expected_length: u64,
25}
26
27impl DavOpenedDownload {
28    #[must_use]
29    pub const fn new(stream: DavContentStream, expected_length: u64) -> Self {
30        Self {
31            stream,
32            expected_length,
33        }
34    }
35}
36
37/// Failure while opening a planned representation stream.
38#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
39pub enum DavDownloadOpenError {
40    #[error(transparent)]
41    Backend(#[from] DavBackendError),
42    #[error("download source length does not match the protocol plan")]
43    LengthMismatch { planned: u64, opened: u64 },
44}
45
46/// Stable backend failure categories mapped by the protocol layer.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DavBackendErrorKind {
49    NotFound,
50    Forbidden,
51    Conflict,
52    AlreadyExists,
53    InsufficientStorage,
54    PayloadTooLarge,
55    Locked,
56    InvalidInput,
57    Unsupported,
58    Internal,
59}
60
61/// Product-neutral failure returned by a product adapter.
62#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
63#[error("WebDAV backend operation failed: {kind:?}")]
64pub struct DavBackendError {
65    pub kind: DavBackendErrorKind,
66}
67
68impl DavBackendError {
69    #[must_use]
70    pub const fn new(kind: DavBackendErrorKind) -> Self {
71        Self { kind }
72    }
73}
74
75/// Low-level file-system failure exposed by the product adapter.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
77pub enum FsError {
78    #[error("not found")]
79    NotFound,
80    #[error("forbidden")]
81    Forbidden,
82    #[error("general failure")]
83    GeneralFailure,
84    #[error("already exists")]
85    Exists,
86    #[error("insufficient storage")]
87    InsufficientStorage,
88    #[error("too large")]
89    TooLarge,
90    #[error("bad request")]
91    BadRequest,
92}
93
94impl From<FsError> for DavBackendError {
95    fn from(error: FsError) -> Self {
96        let kind = match error {
97            FsError::NotFound => DavBackendErrorKind::NotFound,
98            FsError::Forbidden => DavBackendErrorKind::Forbidden,
99            FsError::GeneralFailure => DavBackendErrorKind::Internal,
100            FsError::Exists => DavBackendErrorKind::AlreadyExists,
101            FsError::InsufficientStorage => DavBackendErrorKind::InsufficientStorage,
102            FsError::TooLarge => DavBackendErrorKind::PayloadTooLarge,
103            FsError::BadRequest => DavBackendErrorKind::InvalidInput,
104        };
105        Self::new(kind)
106    }
107}
108
109pub type FsResult<T> = Result<T, FsError>;
110pub type FsFuture<'a, T> = Pin<Box<dyn Future<Output = FsResult<T>> + Send + 'a>>;
111
112/// `WebDAV` resource type.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum DavResourceKind {
115    File,
116    Collection,
117}
118
119/// Protocol-visible state used to evaluate one resource referenced by an `If` header.
120#[derive(Debug, Clone, Default, PartialEq, Eq)]
121pub struct DavIfResourceState {
122    pub etag: Option<String>,
123    pub lock_tokens: Vec<String>,
124}
125
126/// Product adapter used while evaluating `WebDAV` `If` conditions.
127#[async_trait]
128pub trait DavIfStateResolver: Send + Sync {
129    async fn resolve_if_state(&self, path: &DavPath)
130    -> Result<DavIfResourceState, DavBackendError>;
131}
132
133/// Sequential write contract selected by the protocol planner.
134#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct DavMutationCredentials {
136    /// Positively submitted lock tokens whose URI scope applies to this mutation.
137    pub submitted_lock_tokens: Vec<String>,
138}
139
140impl DavMutationCredentials {
141    /// Merges another validated credential set while preserving a canonical unique token list.
142    pub fn merge(&mut self, other: Self) {
143        self.submitted_lock_tokens
144            .extend(other.submitted_lock_tokens);
145        self.submitted_lock_tokens.sort();
146        self.submitted_lock_tokens.dedup();
147    }
148}
149
150/// Sequential write contract selected by the protocol planner.
151#[derive(Debug, Clone, Default, PartialEq, Eq)]
152pub struct DavWriteOptions {
153    pub truncate: bool,
154    pub create: bool,
155    /// Atomically fail if the target already exists; product adapters must enforce this at open.
156    pub create_new: bool,
157    pub expected_length: Option<u64>,
158    pub checksum: Option<String>,
159    pub credentials: DavMutationCredentials,
160}
161
162/// Protocol-visible resource metadata supplied by the product adapter.
163pub trait DavMetaData: Send + Sync {
164    fn len(&self) -> u64;
165    ///
166    /// # Errors
167    ///
168    /// Returns a backend error when the product adapter cannot read the metadata value.
169    fn modified(&self) -> FsResult<SystemTime>;
170    fn is_dir(&self) -> bool;
171    fn etag(&self) -> Option<String>;
172    fn content_type(&self) -> Option<&str> {
173        None
174    }
175    ///
176    /// # Errors
177    ///
178    /// Returns a backend error when the product adapter cannot read the metadata value.
179    fn created(&self) -> FsResult<SystemTime>;
180    fn is_empty(&self) -> bool {
181        self.len() == 0
182    }
183    fn is_file(&self) -> bool {
184        !self.is_dir()
185    }
186}
187
188/// One entry returned by a bounded directory page.
189///
190/// Metadata is part of the page so product adapters can batch it with enumeration instead of
191/// creating an N+1 lookup contract. `stable_key` must be strictly ordered within and across pages.
192pub trait DavDirectoryEntry: Send {
193    type Metadata: DavMetaData;
194
195    fn name(&self) -> &[u8];
196    fn metadata(&self) -> &Self::Metadata;
197    fn stable_key(&self) -> &[u8] {
198        self.name()
199    }
200}
201
202/// One bounded directory-page request with an opaque product-owned continuation cursor.
203///
204/// [`DavDirectoryEnumerator::read_directory_page`] receives a non-zero `maximum_entries` value.
205/// The backend must return no more than that number of entries.
206#[derive(Debug, Clone, Copy)]
207pub struct DavDirectoryPageRequest<'a, C> {
208    pub path: &'a DavPath,
209    pub cursor: Option<&'a C>,
210    pub maximum_entries: usize,
211}
212
213/// One product-owned directory page.
214///
215/// Entries must be in strictly ascending [`DavDirectoryEntry::stable_key`] order and must not
216/// exceed the request's `maximum_entries`. An empty `entries` collection must use `None` for
217/// `next_cursor` rather than returning an empty continuation page.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct DavDirectoryPage<E, C> {
220    pub entries: Vec<E>,
221    pub next_cursor: Option<C>,
222}
223
224/// Product adapter for stable, bounded directory enumeration.
225pub trait DavDirectoryEnumerator: Send + Sync {
226    type Cursor: Eq + Hash + Send + Sync;
227    type Entry: DavDirectoryEntry;
228
229    /// Returns one page satisfying the bounds and ordering contract on
230    /// [`DavDirectoryPageRequest`] and [`DavDirectoryPage`].
231    fn read_directory_page<'a>(
232        &'a self,
233        request: DavDirectoryPageRequest<'a, Self::Cursor>,
234    ) -> impl Future<Output = Result<DavDirectoryPage<Self::Entry, Self::Cursor>, DavBackendError>>
235    + Send
236    + 'a;
237}
238
239/// Product adapter used by GET/HEAD after the protocol layer selects a representation.
240pub trait DavDownloadSource: Send + Sync {
241    type Metadata: DavMetaData;
242
243    fn metadata<'a>(
244        &'a self,
245        path: &'a DavPath,
246    ) -> impl Future<Output = Result<Self::Metadata, DavBackendError>> + Send + 'a;
247    fn open_full<'a>(
248        &'a self,
249        path: &'a DavPath,
250    ) -> impl Future<Output = Result<DavOpenedDownload, DavBackendError>> + Send + 'a;
251    fn open_range<'a>(
252        &'a self,
253        path: &'a DavPath,
254        range: HttpByteRange,
255    ) -> impl Future<Output = Result<DavOpenedDownload, DavBackendError>> + Send + 'a;
256}
257
258/// Sequential write handle. A successful `finish` is the product adapter's commit boundary.
259///
260/// Dropping a handle before `finish` or `abort` must roll back or clean up the uncommitted write.
261/// It must never commit implicitly or leak staging resources. `abort` is the explicit abandonment
262/// path and must perform the same cleanup before it returns.
263pub trait DavWriteHandle: Send {
264    fn write_bytes(
265        &mut self,
266        buf: Bytes,
267    ) -> impl Future<Output = Result<(), DavBackendError>> + Send + '_;
268    fn finish(self) -> impl Future<Output = Result<(), DavBackendError>> + Send;
269    fn abort(self) -> impl Future<Output = Result<(), DavBackendError>> + Send;
270}
271
272/// Product adapter used for complete, sequential representation writes.
273pub trait DavWriteSystem: Send + Sync {
274    type Handle: DavWriteHandle;
275
276    fn open_write<'a>(
277        &'a self,
278        path: &'a DavPath,
279        options: DavWriteOptions,
280    ) -> impl Future<Output = Result<Self::Handle, DavBackendError>> + Send + 'a;
281}
282
283/// Explicit random-write handle used only by negotiated partial-write capabilities.
284///
285/// Dropping a handle before `finish` or `abort` must roll back or clean up the uncommitted write.
286/// It must never commit implicitly or leak staging resources. `finish` remains the sole commit
287/// boundary, while `abort` is the explicit abandonment and cleanup path.
288pub trait DavRandomWriteHandle: Send {
289    fn write_at(
290        &mut self,
291        offset: u64,
292        buf: Bytes,
293    ) -> impl Future<Output = Result<(), DavBackendError>> + Send + '_;
294    fn finish(self) -> impl Future<Output = Result<(), DavBackendError>> + Send;
295    fn abort(self) -> impl Future<Output = Result<(), DavBackendError>> + Send;
296}
297
298/// Optional product adapter for random writes. Ordinary writers do not implement this port.
299pub trait DavRandomWriteSystem: Send + Sync {
300    type Handle: DavRandomWriteHandle;
301
302    fn open_random_write<'a>(
303        &'a self,
304        path: &'a DavPath,
305        options: DavWriteOptions,
306    ) -> impl Future<Output = Result<Self::Handle, DavBackendError>> + Send + 'a;
307}
308
309/// Stored dead property exchanged with the product adapter.
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct DavProp {
312    pub name: String,
313    pub prefix: Option<String>,
314    pub namespace: Option<String>,
315    pub xml: Option<Vec<u8>>,
316}
317
318/// Canonical resource and dead-property backend port.
319pub trait DavFileSystem: Send + Sync {
320    fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>>;
321    fn create_dir<'a>(
322        &'a self,
323        path: &'a DavPath,
324        credentials: DavMutationCredentials,
325    ) -> FsFuture<'a, ()>;
326    fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()>;
327    fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()>;
328    fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()>;
329    fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()>;
330
331    fn get_quota(&self) -> FsFuture<'_, (u64, Option<u64>)> {
332        Box::pin(async { Ok((0, None)) })
333    }
334
335    fn have_props<'a>(
336        &'a self,
337        _path: &'a DavPath,
338    ) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
339        Box::pin(async { false })
340    }
341
342    fn get_props<'a>(
343        &'a self,
344        _path: &'a DavPath,
345        _do_content: bool,
346    ) -> FsFuture<'a, Vec<DavProp>> {
347        Box::pin(async { Ok(Vec::new()) })
348    }
349
350    /// Serial fallback that calls [`DavFileSystem::get_props`] once per path.
351    ///
352    /// Production adapters should override this method when the persistence layer supports a
353    /// genuine batch query.
354    fn get_props_many<'a>(
355        &'a self,
356        paths: &'a [DavPath],
357        do_content: bool,
358    ) -> FsFuture<'a, HashMap<DavPath, Vec<DavProp>>> {
359        Box::pin(async move {
360            let mut result = HashMap::with_capacity(paths.len());
361            for path in paths {
362                result.insert(path.clone(), self.get_props(path, do_content).await?);
363            }
364            Ok(result)
365        })
366    }
367
368    fn patch_props<'a>(
369        &'a self,
370        _path: &'a DavPath,
371        _patches: Vec<(bool, DavProp)>,
372    ) -> FsFuture<'a, Vec<(StatusCode, DavProp)>> {
373        Box::pin(async { Ok(Vec::new()) })
374    }
375}
376
377/// Protocol-visible lock state persisted by the product adapter.
378#[derive(Debug, Clone)]
379pub struct DavLock {
380    pub token: String,
381    pub path: Box<DavPath>,
382    pub principal: Option<String>,
383    pub owner: Option<Box<DavXmlElement>>,
384    pub timeout_at: Option<SystemTime>,
385    pub timeout: Option<Duration>,
386    pub shared: bool,
387    pub deep: bool,
388}
389
390pub type LsFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub enum DavLockPreflightError {
394    LimitExceeded,
395    GeneralFailure,
396}
397
398#[derive(Debug, Clone)]
399pub enum DavLockError {
400    Conflict(Box<DavLock>),
401    ParentMissing,
402    TokenMismatch,
403    LimitExceeded,
404    NotFound,
405    Backend,
406}
407
408#[derive(Debug, Clone)]
409pub struct DavLockAcquireResult {
410    pub lock: DavLock,
411    pub resource_existed: bool,
412}
413
414/// Product backend input for acquiring a new `WebDAV` lock.
415#[derive(Debug)]
416pub struct DavLockAcquireRequest<'a> {
417    pub path: &'a DavPath,
418    pub principal: Option<&'a str>,
419    pub owner: Option<&'a DavXmlElement>,
420    pub timeout: Option<Duration>,
421    pub shared: bool,
422    pub deep: bool,
423    pub credentials: DavMutationCredentials,
424}
425
426/// Canonical lock persistence and conflict backend port.
427pub trait DavLockSystem: Send + Sync {
428    fn prepare_lock(&self, _path: &DavPath) -> LsFuture<'_, Result<(), DavLockPreflightError>> {
429        Box::pin(async { Ok(()) })
430    }
431
432    fn lock<'a>(
433        &'a self,
434        request: DavLockAcquireRequest<'a>,
435    ) -> LsFuture<'a, Result<DavLockAcquireResult, DavLockError>>;
436
437    fn unlock(&self, path: &DavPath, token: &str) -> LsFuture<'_, Result<(), DavLockError>>;
438    fn refresh(
439        &self,
440        path: &DavPath,
441        token: &str,
442        timeout: Option<Duration>,
443    ) -> LsFuture<'_, Result<DavLock, DavLockError>>;
444    fn check(
445        &self,
446        path: &DavPath,
447        principal: Option<&str>,
448        ignore_principal: bool,
449        deep: bool,
450        submitted_tokens: &[String],
451    ) -> LsFuture<'_, Result<(), DavLockError>>;
452    fn discover(&self, path: &DavPath) -> LsFuture<'_, Result<Vec<DavLock>, DavBackendError>>;
453    fn discover_many<'a>(
454        &'a self,
455        paths: &'a [DavPath],
456    ) -> LsFuture<'a, Result<HashMap<DavPath, Vec<DavLock>>, DavBackendError>> {
457        Box::pin(async move {
458            let mut result = HashMap::with_capacity(paths.len());
459            for path in paths {
460                result.insert(path.clone(), self.discover(path).await?);
461            }
462            Ok(result)
463        })
464    }
465    fn conflicting_locks(
466        &self,
467        path: &DavPath,
468        deep: bool,
469    ) -> LsFuture<'_, Result<Vec<DavLock>, DavBackendError>>;
470    fn delete(&self, path: &DavPath) -> LsFuture<'_, Result<(), DavLockError>>;
471}