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