dav.rs
⎇
Raw
1//! WebDAV endpoint.
2//!
3//! Two mounts, both served by the same [`FbFs`]:
4//!
5//! * `{DAV}` — a signed-in user's roots. Each root is a child collection of a
6//! synthetic top-level directory, so one mount covers every root the user
7//! has.
8//! * `{DAV_SHARE}/{token}` — one public share, mounted at its own root.
9//!
10//! All filesystem access goes through [`crate::fs`], so a mount inherits the
11//! same containment and the same symlink handling the JSON API has.
12//!
13//! The protocol itself (PROPFIND, the 207 multistatus, `Depth`, `Destination`,
14//! `Overwrite`, conditional headers) is `dav-server`'s job. This module only
15//! authenticates the request, decides which roots it may see, and maps dav
16//! paths onto real ones.
17
18use std::collections::HashMap;
19use std::io::SeekFrom;
20use std::path::{Path, PathBuf};
21use std::sync::{Arc, LazyLock, Mutex, Weak};
22use std::time::{Duration, SystemTime, UNIX_EPOCH};
23
24use api_types::{DAV, DAV_SHARE, Mode};
25use axum::body::Body;
26use axum::extract::State;
27use axum::http::header::{HeaderMap, WWW_AUTHENTICATE};
28use axum::http::{Request, Response, StatusCode};
29use axum::response::IntoResponse;
30use bytes::{Buf, Bytes};
31use dav_server::DavConfig;
32use dav_server::davpath::DavPath;
33use dav_server::fs::{
34 DavDirEntry, DavFile, DavMetaData, FsError, FsFuture, FsResult, FsStream, GuardedFileSystem,
35 OpenOptions, ReadDirMeta,
36};
37use dav_server::ls::{DavLock, DavLockSystem, LsFuture};
38use dav_server::memls::MemLs;
39use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
40
41use crate::api::common::{display_name, session_auth};
42use crate::auth;
43use crate::db::RootRow;
44use crate::error::AppState;
45
46/// The `WWW-Authenticate` realm. Clients show it in their password prompt.
47const REALM: &str = "filebrowser-ng";
48
49// ---------------------------------------------------------------------------
50// Routes
51// ---------------------------------------------------------------------------
52
53/// `{DAV}` and everything under it: the signed-in user's roots.
54///
55/// A browser session cookie is accepted, but the usual caller is a mount
56/// client, which only speaks HTTP Basic.
57pub async fn user(State(state): State<Arc<AppState>>, req: Request<Body>) -> Response<Body> {
58 let (principal, roots) = match authenticate(&state, req.headers()).await {
59 Some(v) => v,
60 None => return challenge(),
61 };
62 // The admin pseudo-root (the whole server root, read-only) is deliberately
63 // not mounted: `session_auth` does not add it, and a mount that silently
64 // contained a second copy of every other root would be confusing.
65 let mount = Mount {
66 roots: Arc::new(root_segments(&state, roots)),
67 flat: false,
68 };
69 serve(state, req, DAV.to_string(), principal, mount).await
70}
71
72/// `{DAV_SHARE}/{token}` and everything under it: one public share.
73///
74/// Folder shares only. A file share has no collection to mount, and its one
75/// file is already a plain `GET` away on the share page.
76pub async fn share(State(state): State<Arc<AppState>>, req: Request<Body>) -> Response<Body> {
77 let Some(token) = share_token(req.uri().path()) else {
78 return StatusCode::NOT_FOUND.into_response();
79 };
80 let row = match state.db.share_by_token(&token).await {
81 Ok(Some(row)) => row,
82 Ok(None) => return StatusCode::NOT_FOUND.into_response(),
83 Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
84 };
85 if row.is_expired() {
86 return StatusCode::GONE.into_response();
87 }
88 if row.is_file {
89 return StatusCode::NOT_FOUND.into_response();
90 }
91 // A protected share takes its password over Basic, with the user name
92 // ignored. There is no account behind a share link to name.
93 if let Some(hash) = row.password_hash.clone() {
94 let Some((_, password)) = auth::basic_credentials(req.headers()) else {
95 return challenge();
96 };
97 let (pw, id, tok) = (password.clone(), row.id, token.clone());
98 let ok = auth::verify_cached(row.id, "", &password, move || async move {
99 // Throttled like `POST /api/share/{token}/unlock`, keyed the same
100 // way, so a mount client is not the cheap way to guess.
101 let delay = auth::login_delay(&tok);
102 if !delay.is_zero() {
103 tokio::time::sleep(delay).await;
104 }
105 let ok = auth::verify_password_async(&pw, &hash).await;
106 auth::record_login(&tok, ok);
107 ok.then_some(id)
108 })
109 .await;
110 if ok.is_none() {
111 return challenge();
112 }
113 }
114 let root = RootRow {
115 id: row.id,
116 path: row.target.clone(),
117 mode: row.mode,
118 };
119 let mount = Mount {
120 roots: Arc::new(vec![(String::new(), root)]),
121 flat: true,
122 };
123 let prefix = format!("{DAV_SHARE}/{token}");
124 serve(state, req, prefix, format!("share-{}", row.id), mount).await
125}
126
127/// The token out of the *raw* URL path.
128///
129/// Not axum's decoded wildcard: `DavPath` keeps the raw path, and
130/// `strip_prefix` byte-compares against it. A decoded `<token>%2Fx` would
131/// yield a prefix that the dav path does not start with.
132fn share_token(path: &str) -> Option<String> {
133 let rest = path.strip_prefix(DAV_SHARE)?.strip_prefix('/')?;
134 let token = rest.split('/').next().unwrap_or_default();
135 (!token.is_empty()).then(|| token.to_string())
136}
137
138/// Hand the request to `dav-server` and, afterwards, keep the share table in
139/// step with the filesystem.
140///
141/// The handler is built here rather than once at startup because `prefix`
142/// differs per share mount, and `dav-server` only allows a per-request config
143/// override on the unguarded handler. Building it is cheap: an `Arc::new` and
144/// two `Arc`-backed trait-object clones.
145async fn serve(
146 state: Arc<AppState>,
147 req: Request<Body>,
148 prefix: String,
149 principal: String,
150 mount: Mount,
151) -> Response<Body> {
152 // Resolved *before* the operation, while the item still exists: once
153 // DELETE or MOVE has run there is no path left to look a share up by.
154 let vacating = matches!(req.method().as_str(), "DELETE" | "MOVE");
155 let vacated = if vacating {
156 dav_target(&state, &mount, &prefix, &req)
157 } else {
158 None
159 };
160
161 let handler = DavConfig::<Mount>::new()
162 .filesystem(Box::new(FbFs {
163 state: state.clone(),
164 }))
165 // The handler is rebuilt per request, the lock tree must not be.
166 .locksystem(Box::new(locks_for(&principal)))
167 // Its only effect in `dav-server` is picking the `ReadDirMeta` for
168 // PROPFIND. `false` keeps listings on the followed metadata.
169 .hide_symlinks(false)
170 // Off by default in `dav-server`: without it a plain `GET` of any
171 // collection answers 405, so the mount is unreadable in a browser.
172 .autoindex(true)
173 .strip_prefix(prefix)
174 .build_handler();
175
176 let mut resp = handler.handle_guarded(req, principal, mount).await;
177 crate::api::sandbox_scriptable(&mut resp);
178
179 // One revoke for the whole request. Doing it inside the filesystem would
180 // fire a query per removed item, and a recursive DELETE walks the tree.
181 if let Some(abs) = vacated
182 && resp.status().is_success()
183 {
184 crate::api::files::revoke_shares_at(&state, &abs).await;
185 }
186 resp.map(Body::new)
187}
188
189/// The absolute path a request addresses, if it resolves to one today.
190fn dav_target(
191 state: &AppState,
192 mount: &Mount,
193 prefix: &str,
194 req: &Request<Body>,
195) -> Option<PathBuf> {
196 let mut path = DavPath::from_uri(req.uri()).ok()?;
197 path.set_prefix(prefix).ok()?;
198 let (root, rel) = item(&path, mount).ok()?;
199 // `resolve_entry`, matching the operations this is predicting. Following
200 // the last component would name a symlink's target, so deleting a link
201 // would revoke a share on a file that is still there.
202 crate::fs::resolve_entry(&state.root, &root.path, &rel).ok()
203}
204
205/// 401 with the Basic challenge every mount client needs to see before it
206/// will send credentials at all.
207fn challenge() -> Response<Body> {
208 (
209 StatusCode::UNAUTHORIZED,
210 [(WWW_AUTHENTICATE, format!("Basic realm=\"{REALM}\""))],
211 )
212 .into_response()
213}
214
215// ---------------------------------------------------------------------------
216// Authentication
217// ---------------------------------------------------------------------------
218
219/// Resolve the caller to a principal name and the roots they may mount.
220async fn authenticate(state: &AppState, headers: &HeaderMap) -> Option<(String, Vec<RootRow>)> {
221 // A browser hitting the mount already has a session; take it and skip
222 // Argon2 entirely.
223 if auth::parse_session_cookie(headers).is_some()
224 && let Ok((user, roots)) = session_auth(headers, state).await
225 {
226 return Some((user.name, roots));
227 }
228
229 let (name, password) = auth::basic_credentials(headers)?;
230 let id = auth::verify_cached(0, &name, &password, || {
231 let (state, name, password) = (state, name.clone(), password.clone());
232 async move {
233 // The login route's throttle, keyed the same way, so guessing over
234 // WebDAV is no cheaper than guessing over the login form.
235 let delay = auth::login_delay(&name);
236 if !delay.is_zero() {
237 tokio::time::sleep(delay).await;
238 }
239 let user = state.db.verify_password(&name, &password).await.ok()?;
240 auth::record_login(&name, user.is_some());
241 user.map(|u| u.id)
242 }
243 })
244 .await?;
245 let roots = state.db.user_roots(id).await.ok()?;
246 Some((name, roots))
247}
248
249// ---------------------------------------------------------------------------
250// Locking
251// ---------------------------------------------------------------------------
252
253/// One lock tree per principal.
254///
255/// A lock is keyed by DAV URL, and a URL segment is a root's display name, so a
256/// single shared tree lets two users whose roots are both named `Documents`
257/// reach each other's locks. One could block the other, and `PROPFIND` hands
258/// back the holder's lock token, which is enough to release that lock or write
259/// through it.
260///
261/// The cost is that two principals sharing one physical folder do not
262/// coordinate through WebDAV locks. Byte-level safety does not rest on this:
263/// [`WRITE_LOCKS`] keys on the resolved path and covers every writer.
264///
265/// `MemLs` keeps its state in an `Arc`, so a clone shares that principal's
266/// tree. Locks live in memory only, and a restart drops them all, which is
267/// what a client already sees when a lock times out.
268static LOCKS: LazyLock<Mutex<HashMap<String, ExpiringLs>>> =
269 LazyLock::new(|| Mutex::new(HashMap::new()));
270
271fn locks_for(principal: &str) -> ExpiringLs {
272 let mut g = LOCKS.lock().unwrap_or_else(|e| e.into_inner());
273 g.entry(principal.to_string())
274 .or_insert_with(|| ExpiringLs(*MemLs::new()))
275 .clone()
276}
277
278/// Longest lock handed out, and so the longest an abandoned one blocks a file.
279/// A client that still wants the file refreshes; one that crashed does not.
280///
281/// Matches `dav-server`'s own ceiling for an exclusive lock. It caps only the
282/// two cases that arrive here uncapped, both as `None` meaning "never
283/// expires": a LOCK with no `Timeout` header, and a refresh asking for
284/// `Infinite`.
285const LOCK_TIMEOUT: Duration = Duration::from_secs(600);
286
287/// [`MemLs`] with lock expiry actually applied.
288///
289/// `MemLs` records a lock's `timeout_at` and then never looks at it again, and
290/// it honours an infinite timeout request. Left alone, a client that died
291/// holding an exclusive lock would block that file until the process restarts.
292/// Every call here first drops the expired locks covering the path it touches,
293/// and no lock is granted for longer than [`LOCK_TIMEOUT`].
294#[derive(Debug, Clone)]
295struct ExpiringLs(MemLs);
296
297impl ExpiringLs {
298 /// Drop the expired locks on `path` and its ancestors.
299 ///
300 /// Only the locks that could block an operation *on this path*. A lock on
301 /// a descendant is not swept, so a deep operation can still be refused by
302 /// a stale lock below it until something touches that path directly.
303 async fn sweep(&self, path: &DavPath) {
304 let now = SystemTime::now();
305 for lock in self.0.discover(path).await {
306 if lock.timeout_at.is_some_and(|t| t <= now) {
307 let _ = self.0.unlock(&lock.path, &lock.token).await;
308 }
309 }
310 }
311
312 /// Never `None`, never longer than [`LOCK_TIMEOUT`]. `None` would mean a
313 /// lock that [`sweep`](Self::sweep) can never clear.
314 fn capped(timeout: Option<Duration>) -> Option<Duration> {
315 Some(timeout.unwrap_or(LOCK_TIMEOUT).min(LOCK_TIMEOUT))
316 }
317}
318
319impl DavLockSystem for ExpiringLs {
320 fn lock(
321 &self,
322 path: &DavPath,
323 principal: Option<&str>,
324 owner: Option<&xmltree::Element>,
325 timeout: Option<Duration>,
326 shared: bool,
327 deep: bool,
328 ) -> LsFuture<'_, Result<DavLock, DavLock>> {
329 // The borrows end with the call, not with the future, so clone into it.
330 let (path, principal) = (path.clone(), principal.map(str::to_string));
331 let owner = owner.cloned();
332 Box::pin(async move {
333 self.sweep(&path).await;
334 self.0
335 .lock(
336 &path,
337 principal.as_deref(),
338 owner.as_ref(),
339 Self::capped(timeout),
340 shared,
341 deep,
342 )
343 .await
344 })
345 }
346
347 fn unlock(&self, path: &DavPath, token: &str) -> LsFuture<'_, Result<(), ()>> {
348 let (path, token) = (path.clone(), token.to_string());
349 Box::pin(async move { self.0.unlock(&path, &token).await })
350 }
351
352 fn refresh(
353 &self,
354 path: &DavPath,
355 token: &str,
356 timeout: Option<Duration>,
357 ) -> LsFuture<'_, Result<DavLock, ()>> {
358 let (path, token) = (path.clone(), token.to_string());
359 Box::pin(async move {
360 // Swept first: a client refreshing a lock it let expire must be
361 // told, not silently handed the file back.
362 self.sweep(&path).await;
363 self.0.refresh(&path, &token, Self::capped(timeout)).await
364 })
365 }
366
367 fn check(
368 &self,
369 path: &DavPath,
370 principal: Option<&str>,
371 ignore_principal: bool,
372 deep: bool,
373 submitted_tokens: &[String],
374 ) -> LsFuture<'_, Result<(), DavLock>> {
375 let (path, principal) = (path.clone(), principal.map(str::to_string));
376 let tokens = submitted_tokens.to_vec();
377 Box::pin(async move {
378 self.sweep(&path).await;
379 self.0
380 .check(&path, principal.as_deref(), ignore_principal, deep, &tokens)
381 .await
382 })
383 }
384
385 fn discover(&self, path: &DavPath) -> LsFuture<'_, Vec<DavLock>> {
386 let path = path.clone();
387 Box::pin(async move {
388 self.sweep(&path).await;
389 self.0.discover(&path).await
390 })
391 }
392
393 fn delete(&self, path: &DavPath) -> LsFuture<'_, Result<(), ()>> {
394 let path = path.clone();
395 Box::pin(async move { self.0.delete(&path).await })
396 }
397}
398
399/// One mutex per path with a writer on it.
400///
401/// WebDAV locking does not cover this: a lock is only consulted for a client
402/// that sends LOCK, and a plain PUT never does. Two concurrent PUTs otherwise
403/// interleave into a byte-level splice of both bodies, with both clients told
404/// 2xx. Serializing the write open makes the outcome last-writer-wins.
405///
406/// Keyed by the resolved absolute path, so two mounts onto the same file share
407/// one mutex. The lock tree cannot do that: it keys on the URL, and the same
408/// file has a different URL in a user mount and in a share.
409static WRITE_LOCKS: LazyLock<Mutex<HashMap<PathBuf, Weak<tokio::sync::Mutex<()>>>>> =
410 LazyLock::new(|| Mutex::new(HashMap::new()));
411
412fn write_lock(path: &Path) -> Arc<tokio::sync::Mutex<()>> {
413 let mut map = WRITE_LOCKS.lock().unwrap_or_else(|e| e.into_inner());
414 // Drop entries whose last writer finished, so the map holds in-flight
415 // writes and not every file ever written.
416 map.retain(|_, w| w.strong_count() > 0);
417 if let Some(m) = map.get(path).and_then(Weak::upgrade) {
418 return m;
419 }
420 let m = Arc::new(tokio::sync::Mutex::new(()));
421 map.insert(path.to_path_buf(), Arc::downgrade(&m));
422 m
423}
424
425// ---------------------------------------------------------------------------
426// Mount: which roots a request sees, and where in the URL they live
427// ---------------------------------------------------------------------------
428
429/// The credentials `dav-server` carries through to [`FbFs`]: the roots this
430/// request may touch, and how they are laid out under the mount point.
431#[derive(Clone)]
432pub struct Mount {
433 /// URL segment → root. The segment is empty when `flat`.
434 roots: Arc<Vec<(String, RootRow)>>,
435 /// One root mounted directly at the mount point (a share), rather than as
436 /// a child of a synthetic collection.
437 flat: bool,
438}
439
440/// What a dav path addresses.
441enum Target {
442 /// The synthetic collection at the mount point that lists the roots.
443 Roots,
444 Item {
445 root: RootRow,
446 rel: String,
447 },
448}
449
450/// The URL segment for each root: its display name, disambiguated with the
451/// root id when two roots would otherwise claim the same one.
452fn root_segments(state: &AppState, roots: Vec<RootRow>) -> Vec<(String, RootRow)> {
453 let names: Vec<String> = roots.iter().map(|r| display_name(state, &r.path)).collect();
454 roots
455 .into_iter()
456 .zip(&names)
457 .map(|(r, name)| {
458 let taken = names.iter().filter(|n| *n == name).count() > 1;
459 let seg = match taken {
460 true => format!("{name}-{}", r.id),
461 false => name.clone(),
462 };
463 (seg, r)
464 })
465 .collect()
466}
467
468fn target(path: &DavPath, mount: &Mount) -> FsResult<Target> {
469 let rel = path.as_rel_ospath();
470 if mount.flat {
471 let (_, root) = mount.roots.first().ok_or(FsError::NotFound)?;
472 return Ok(Target::Item {
473 root: root.clone(),
474 rel: rel.to_string_lossy().into_owned(),
475 });
476 }
477 let mut parts = rel.components();
478 let Some(first) = parts.next() else {
479 return Ok(Target::Roots);
480 };
481 let seg = first.as_os_str().to_string_lossy();
482 let (_, root) = mount
483 .roots
484 .iter()
485 .find(|(s, _)| s.as_str() == seg)
486 .ok_or(FsError::NotFound)?;
487 Ok(Target::Item {
488 root: root.clone(),
489 rel: parts.collect::<PathBuf>().to_string_lossy().into_owned(),
490 })
491}
492
493/// [`target`], rejecting the synthetic collection.
494fn item(path: &DavPath, mount: &Mount) -> FsResult<(RootRow, String)> {
495 match target(path, mount)? {
496 Target::Roots => Err(FsError::Forbidden),
497 Target::Item { root, rel } => Ok((root, rel)),
498 }
499}
500
501fn writable(root: &RootRow) -> FsResult<()> {
502 match root.mode {
503 Mode::Rw => Ok(()),
504 Mode::Ro => Err(FsError::Forbidden),
505 }
506}
507
508// ---------------------------------------------------------------------------
509// The filesystem
510// ---------------------------------------------------------------------------
511
512#[derive(Clone)]
513struct FbFs {
514 state: Arc<AppState>,
515}
516
517impl GuardedFileSystem<Mount> for FbFs {
518 fn open<'a>(
519 &'a self,
520 path: &'a DavPath,
521 options: OpenOptions,
522 mount: &'a Mount,
523 ) -> FsFuture<'a, Box<dyn DavFile>> {
524 Box::pin(async move {
525 let (root, rel) = item(path, mount)?;
526 if options.write || options.append || options.truncate || options.create {
527 writable(&root)?;
528 }
529 let creating = options.create || options.create_new;
530 let full = self
531 .resolve(&root, rel, move |server_root, root_rel, rel| {
532 use crate::fs::FsError as E;
533 // Strict first, so a write lands on the file the path
534 // really names.
535 match crate::fs::resolve_path(server_root, root_rel, rel) {
536 Err(E::NotFound) if creating => {
537 let p = crate::fs::resolve_entry(server_root, root_rel, rel)?;
538 // Nothing resolved, yet the name is taken: a
539 // dangling symlink. Opening that with `create`
540 // would write wherever it points, which may be
541 // outside the root.
542 if std::fs::symlink_metadata(&p).is_ok() {
543 return Err(E::Forbidden);
544 }
545 Ok(p)
546 }
547 other => other,
548 }
549 })
550 .await?;
551 // Taken before the open, so the truncate happens under it too,
552 // and held until the `DavFile` is dropped, which is after the last
553 // byte of the body has landed.
554 let writing = options.write || options.append || options.truncate;
555 let _write = match writing {
556 true => Some(write_lock(&full).lock_owned().await),
557 false => None,
558 };
559 let file = tokio::fs::OpenOptions::new()
560 .read(options.read)
561 .write(options.write)
562 .append(options.append)
563 .truncate(options.truncate)
564 .create(options.create)
565 .create_new(options.create_new)
566 .open(&full)
567 .await
568 .map_err(|e| io_error(&e))?;
569 Ok(Box::new(File { file, _write }) as Box<dyn DavFile>)
570 })
571 }
572
573 /// `meta` decides whether a symlink is described as itself or as what it
574 /// points at, and `dav-server` picks it per operation: `Data` for a
575 /// listing, `DataSymlink` for the walk behind a recursive DELETE or COPY.
576 /// Answering both with followed metadata makes a recursive DELETE descend
577 /// into a linked directory and empty it.
578 fn read_dir<'a>(
579 &'a self,
580 path: &'a DavPath,
581 meta: ReadDirMeta,
582 mount: &'a Mount,
583 ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
584 Box::pin(async move {
585 let listing = matches!(meta, ReadDirMeta::Data);
586 let entries = match target(path, mount)? {
587 Target::Roots => {
588 // That walk deletes the children before it asks to remove
589 // the collection, so refusing the mount point at
590 // `remove_dir` would come after every root was emptied.
591 // Refusing the listing stops it before anything is touched.
592 if !listing {
593 return Err(FsError::Forbidden);
594 }
595 self.root_entries(mount).await
596 }
597 Target::Item { root, rel } => {
598 // Same for a root's own top. It is a mount point, not a
599 // folder inside one. A whole root cannot be deleted, moved
600 // onto, or copied through the mount.
601 if rel.is_empty() && !listing {
602 return Err(FsError::Forbidden);
603 }
604 let full = self.resolve(&root, rel, crate::fs::resolve_path).await?;
605 // No `MAX_LIST_ENTRIES` cap here, on purpose. PROPFIND has
606 // no way to say "this listing was cut", so a sync client
607 // would read a truncated listing as "the rest was deleted"
608 // and mirror that.
609 blocking(move || {
610 let rd = std::fs::read_dir(&full).map_err(|e| io_error(&e))?;
611 Ok(rd
612 .flatten()
613 .filter_map(|e| {
614 // A link out of the root is still listed. It
615 // refuses to open.
616 let meta = match listing {
617 true => match std::fs::metadata(e.path()) {
618 Ok(m) => Meta::of(&m),
619 // No target to stat: a dangling link.
620 Err(_) => Meta::broken_link(
621 &std::fs::symlink_metadata(e.path()).ok()?,
622 ),
623 },
624 false => Meta::of(&std::fs::symlink_metadata(e.path()).ok()?),
625 };
626 Some(Entry {
627 name: e.file_name().to_string_lossy().into_owned().into_bytes(),
628 meta,
629 })
630 })
631 .collect())
632 })
633 .await?
634 }
635 };
636 let stream = futures_util::stream::iter(
637 entries
638 .into_iter()
639 .map(|e| Ok(Box::new(e) as Box<dyn DavDirEntry>)),
640 );
641 Ok(Box::pin(stream) as FsStream<Box<dyn DavDirEntry>>)
642 })
643 }
644
645 fn metadata<'a>(
646 &'a self,
647 path: &'a DavPath,
648 mount: &'a Mount,
649 ) -> FsFuture<'a, Box<dyn DavMetaData>> {
650 Box::pin(async move {
651 let (root, rel) = match target(path, mount)? {
652 Target::Roots => return Ok(Box::new(Meta::synthetic_dir()) as Box<dyn DavMetaData>),
653 Target::Item { root, rel } => (root, rel),
654 };
655 let full = self.resolve(&root, rel, crate::fs::resolve_path).await?;
656 let meta = blocking(move || std::fs::metadata(&full).map_err(|e| io_error(&e))).await?;
657 Ok(Box::new(Meta::of(&meta)) as Box<dyn DavMetaData>)
658 })
659 }
660
661 /// Metadata of the entry itself. `dav-server` asks this before a DELETE,
662 /// a MOVE, and before overwriting a destination, precisely so it can act
663 /// on a link rather than on what it names.
664 fn symlink_metadata<'a>(
665 &'a self,
666 path: &'a DavPath,
667 mount: &'a Mount,
668 ) -> FsFuture<'a, Box<dyn DavMetaData>> {
669 Box::pin(async move {
670 let (root, rel) = match target(path, mount)? {
671 Target::Roots => return Ok(Box::new(Meta::synthetic_dir()) as Box<dyn DavMetaData>),
672 Target::Item { root, rel } => (root, rel),
673 };
674 let full = self.resolve(&root, rel, crate::fs::resolve_entry).await?;
675 let meta = blocking(move || std::fs::symlink_metadata(&full).map_err(|e| io_error(&e)))
676 .await?;
677 Ok(Box::new(Meta::of(&meta)) as Box<dyn DavMetaData>)
678 })
679 }
680
681 fn create_dir<'a>(&'a self, path: &'a DavPath, mount: &'a Mount) -> FsFuture<'a, ()> {
682 Box::pin(async move {
683 let (root, rel) = item(path, mount)?;
684 writable(&root)?;
685 let (server_root, root_rel) = (self.state.root.clone(), root.path.clone());
686 blocking(move || crate::fs::mkdir(&server_root, &root_rel, &rel).map_err(fs_error))
687 .await
688 })
689 }
690
691 /// Only ever called on an empty directory: `dav-server` walks a tree
692 /// itself and removes the children first.
693 fn remove_dir<'a>(&'a self, path: &'a DavPath, mount: &'a Mount) -> FsFuture<'a, ()> {
694 Box::pin(async move {
695 let (root, rel) = item(path, mount)?;
696 writable(&root)?;
697 let full = self.resolve(&root, rel, crate::fs::resolve_entry).await?;
698 blocking(move || {
699 // A symlink to a directory is listed as a collection, so this
700 // is where DELETE lands on one. Unlink it rather than letting
701 // `remove_dir` fail on a path that is not a directory.
702 let meta = std::fs::symlink_metadata(&full).map_err(|e| io_error(&e))?;
703 match meta.file_type().is_symlink() {
704 true => std::fs::remove_file(&full),
705 false => std::fs::remove_dir(&full),
706 }
707 .map_err(|e| io_error(&e))
708 })
709 .await
710 })
711 }
712
713 fn remove_file<'a>(&'a self, path: &'a DavPath, mount: &'a Mount) -> FsFuture<'a, ()> {
714 Box::pin(async move {
715 let (root, rel) = item(path, mount)?;
716 writable(&root)?;
717 // Not followed: deleting a symlink removes the link, not the file
718 // it names.
719 let full = self.resolve(&root, rel, crate::fs::resolve_entry).await?;
720 blocking(move || std::fs::remove_file(&full).map_err(|e| io_error(&e))).await
721 })
722 }
723
724 fn rename<'a>(
725 &'a self,
726 from: &'a DavPath,
727 to: &'a DavPath,
728 mount: &'a Mount,
729 ) -> FsFuture<'a, ()> {
730 Box::pin(async move {
731 let (src, dst) = (item(from, mount)?, item(to, mount)?);
732 // A move takes the item out of the source root, so that root has
733 // to be writable too.
734 writable(&src.0)?;
735 writable(&dst.0)?;
736 let server_root = self.state.root.clone();
737 blocking(move || {
738 crate::fs::move_to(&server_root, &src.0.path, &src.1, &dst.0.path, &dst.1)
739 .map_err(fs_error)
740 })
741 .await
742 })
743 }
744
745 /// Files only: `dav-server` walks a directory tree itself.
746 fn copy<'a>(
747 &'a self,
748 from: &'a DavPath,
749 to: &'a DavPath,
750 mount: &'a Mount,
751 ) -> FsFuture<'a, ()> {
752 Box::pin(async move {
753 let (src, dst) = (item(from, mount)?, item(to, mount)?);
754 // Only the destination is written. Copying *out of* a read-only
755 // root is fine, and is how a user gets a read-only folder's
756 // contents into a writable one.
757 writable(&dst.0)?;
758 // The same mutex a PUT to this path would take, or a COPY and a
759 // PUT racing for it interleave. Both resolve to the canonical
760 // parent plus the name, so the keys agree.
761 let full = self
762 .resolve(&dst.0, dst.1.clone(), crate::fs::resolve_entry)
763 .await?;
764 let _write = write_lock(&full).lock_owned().await;
765 let server_root = self.state.root.clone();
766 blocking(move || {
767 crate::fs::copy_file_to(&server_root, &src.0.path, &src.1, &dst.0.path, &dst.1)
768 .map_err(fs_error)
769 })
770 .await
771 })
772 }
773}
774
775impl FbFs {
776 /// Run one of the [`crate::fs`] resolvers on the blocking pool.
777 async fn resolve(
778 &self,
779 root: &RootRow,
780 rel: String,
781 f: impl FnOnce(&std::path::Path, &str, &str) -> Result<PathBuf, crate::fs::FsError>
782 + Send
783 + 'static,
784 ) -> FsResult<PathBuf> {
785 let (server_root, root_rel) = (self.state.root.clone(), root.path.clone());
786 blocking(move || f(&server_root, &root_rel, &rel).map_err(fs_error)).await
787 }
788
789 /// The synthetic top-level listing: one entry per mounted root.
790 async fn root_entries(&self, mount: &Mount) -> Vec<Entry> {
791 let mut out = Vec::with_capacity(mount.roots.len());
792 for (seg, root) in mount.roots.iter() {
793 // A root that no longer resolves is skipped rather than reported
794 // as broken: the JSON API hides it the same way.
795 let (server_root, root_rel) = (self.state.root.clone(), root.path.clone());
796 let Ok(full) = blocking(move || {
797 crate::fs::resolve_root(&server_root, &root_rel).map_err(fs_error)
798 })
799 .await
800 else {
801 continue;
802 };
803 let meta = tokio::fs::metadata(&full)
804 .await
805 .map(|m| Meta::of(&m))
806 .unwrap_or_else(|_| Meta::synthetic_dir());
807 out.push(Entry {
808 name: seg.clone().into_bytes(),
809 meta,
810 });
811 }
812 out
813 }
814}
815
816// ---------------------------------------------------------------------------
817// Filesystem value types
818// ---------------------------------------------------------------------------
819
820#[derive(Debug, Clone)]
821struct Meta {
822 len: u64,
823 modified: SystemTime,
824 is_dir: bool,
825 is_symlink: bool,
826}
827
828impl Meta {
829 fn of(m: &std::fs::Metadata) -> Self {
830 Meta {
831 len: m.len(),
832 modified: m.modified().unwrap_or(UNIX_EPOCH),
833 is_dir: m.is_dir(),
834 is_symlink: m.file_type().is_symlink(),
835 }
836 }
837
838 /// A listing entry whose target could not be stat'd: a dangling symlink.
839 ///
840 /// Described as an empty file, not as a link: `dav-server` drops any entry
841 /// a listing reports as a symlink, and a sync client reads a file missing
842 /// from PROPFIND as a deletion to mirror.
843 fn broken_link(m: &std::fs::Metadata) -> Self {
844 Meta {
845 len: 0,
846 is_dir: false,
847 is_symlink: false,
848 ..Meta::of(m)
849 }
850 }
851
852 /// The mount point itself, which is not a directory on disk.
853 fn synthetic_dir() -> Self {
854 Meta {
855 len: 0,
856 modified: UNIX_EPOCH,
857 is_dir: true,
858 is_symlink: false,
859 }
860 }
861}
862
863impl DavMetaData for Meta {
864 fn len(&self) -> u64 {
865 self.len
866 }
867
868 fn modified(&self) -> FsResult<SystemTime> {
869 Ok(self.modified)
870 }
871
872 fn is_dir(&self) -> bool {
873 self.is_dir
874 }
875
876 fn is_symlink(&self) -> bool {
877 self.is_symlink
878 }
879}
880
881#[derive(Debug)]
882struct Entry {
883 name: Vec<u8>,
884 meta: Meta,
885}
886
887impl DavDirEntry for Entry {
888 fn name(&self) -> Vec<u8> {
889 self.name.clone()
890 }
891
892 fn metadata(&self) -> FsFuture<'_, Box<dyn DavMetaData>> {
893 let meta = self.meta.clone();
894 Box::pin(std::future::ready(Ok(
895 Box::new(meta) as Box<dyn DavMetaData>
896 )))
897 }
898}
899
900/// An open file. Plain async I/O: the path was already resolved and checked,
901/// so nothing here needs the blocking pool.
902#[derive(Debug)]
903struct File {
904 file: tokio::fs::File,
905 /// Held for the life of a writable handle. See [`WRITE_LOCKS`].
906 _write: Option<tokio::sync::OwnedMutexGuard<()>>,
907}
908
909/// Ceiling on one `read_bytes` allocation. `dav-server` asks for its own read
910/// buffer size, but the count reaches us from the request, and a short read is
911/// always a valid answer.
912const MAX_READ: usize = 64 * 1024;
913
914impl DavFile for File {
915 fn metadata(&mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
916 Box::pin(async move {
917 let m = self.file.metadata().await.map_err(|e| io_error(&e))?;
918 Ok(Box::new(Meta::of(&m)) as Box<dyn DavMetaData>)
919 })
920 }
921
922 fn write_buf(&mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
923 Box::pin(async move {
924 while buf.has_remaining() {
925 let n = self
926 .file
927 .write(buf.chunk())
928 .await
929 .map_err(|e| io_error(&e))?;
930 buf.advance(n);
931 }
932 Ok(())
933 })
934 }
935
936 fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
937 Box::pin(async move { self.file.write_all(&buf).await.map_err(|e| io_error(&e)) })
938 }
939
940 fn read_bytes(&mut self, count: usize) -> FsFuture<'_, Bytes> {
941 Box::pin(async move {
942 let mut b = vec![0u8; count.min(MAX_READ)];
943 let n = self.file.read(&mut b).await.map_err(|e| io_error(&e))?;
944 b.truncate(n);
945 Ok(Bytes::from(b))
946 })
947 }
948
949 fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
950 Box::pin(async move { self.file.seek(pos).await.map_err(|e| io_error(&e)) })
951 }
952
953 fn flush(&mut self) -> FsFuture<'_, ()> {
954 Box::pin(async move { self.file.flush().await.map_err(|e| io_error(&e)) })
955 }
956}
957
958// ---------------------------------------------------------------------------
959// Errors and blocking work
960// ---------------------------------------------------------------------------
961
962/// Run blocking filesystem work, mapping a panic or a shut-down runtime onto
963/// a 500.
964async fn blocking<T: Send + 'static>(
965 f: impl FnOnce() -> FsResult<T> + Send + 'static,
966) -> FsResult<T> {
967 tokio::task::spawn_blocking(f)
968 .await
969 .map_err(|_| FsError::GeneralFailure)?
970}
971
972fn fs_error(e: crate::fs::FsError) -> FsError {
973 use crate::fs::FsError as E;
974 match e {
975 E::NotFound | E::RootMissing => FsError::NotFound,
976 E::Conflict => FsError::Exists,
977 // `Invalid` is a rejected name, which is a refusal, not a 400 here:
978 // WebDAV has no status for "that name is not allowed".
979 E::NotADirectory | E::Forbidden | E::Invalid(_) => FsError::Forbidden,
980 }
981}
982
983/// `dav-server` only derives this from `std::io::Error` when its own `localfs`
984/// backend is compiled in, which it is not.
985fn io_error(e: &std::io::Error) -> FsError {
986 use std::io::ErrorKind as K;
987 match e.kind() {
988 K::NotFound => FsError::NotFound,
989 K::PermissionDenied => FsError::Forbidden,
990 K::AlreadyExists => FsError::Exists,
991 K::CrossesDevices => FsError::IsRemote,
992 // `read_dir` on a file. A refusal, not a server fault.
993 K::NotADirectory => FsError::Forbidden,
994 _ => FsError::GeneralFailure,
995 }
996}
997