[GH-ISSUE #714] Regression in v0.46.0: directory upload via drag-and-drop returns 404 due to guard_root_contained() #8817

Closed
opened 2026-06-03 00:39:39 +03:00 by zhus · 1 comment
Owner

Originally created by @nirvana6 on GitHub (May 27, 2026).
Original GitHub issue: https://github.com/sigoden/dufs/issues/714

Bug Description

Dragging a directory onto the dufs web UI causes all files to queue with status - and then fail with 404 Not Found. Single file uploads to the root directory work fine.

Root Cause

This is a regression introduced in v0.46.0 by PR #670 ("fix: ensure symlink inside serve root").

Before v0.46.0 (v0.45.0)

The root containment check was:

if !self.args.allow_symlink && !is_miss && !self.is_root_contained(path).await {
    status_not_found(&mut res);
    return Ok(res);
}

When is_miss = true (path does not exist yet), !is_miss is false, so the entire condition is false and the request proceeds to handle_upload(), which calls ensure_path_parent() to create the parent directory.

v0.46.0 (current)

PR #670 replaced the above with guard_root_contained():

if self.guard_root_contained(path).await {
    self.handle_not_found(&query_params, headers, head_only, &mut res)
        .await?;
    return Ok(res);
}

This check is unconditional — it runs for all requests regardless of whether the path exists. The function tries to handle non-existent paths by checking the parent:

async fn guard_root_contained(&self, path: &Path) -> bool {
    if self.args.allow_symlink {
        return false;
    }
    let path = if !fs::try_exists(path).await.unwrap_or_default() {
        match path.parent() {
            Some(parent) => parent.to_path_buf(),
            None => return true,
        }
    } else {
        path.to_path_buf()
    };
    !self.is_root_contained(path.as_path()).await
}

But when uploading PUT /subdir/file.txt where subdir/ does not exist yet:

  1. fs::try_exists(path)false (file does not exist)
  2. Takes path.parent()subdir/
  3. is_root_contained(subdir/) calls fs::canonicalize(subdir/) which fails because subdir/ does not exist yet
  4. .ok().map(...).unwrap_or_default()false
  5. guard_root_contained returns !falsetrue
  6. Server returns 404 before handle_upload() is reached
  7. ensure_path_parent() inside handle_upload() — which would create subdir/ — is never called

Reproduction

  1. Start dufs v0.46.0 with --allow-upload
  2. Open the web UI in a browser
  3. Drag a directory (containing files) from the file manager onto the page
  4. Files appear in the upload queue with status -, then show with tooltip "404 Not Found"

Single file uploads to the root directory work correctly because the parent (.) already exists.

Suggested Fix

In guard_root_contained(), walk up the directory tree until an existing ancestor is found, then canonicalize that:

async fn guard_root_contained(&self, path: &Path) -> bool {
    if self.args.allow_symlink {
        return false;
    }
    let mut check_path = path.to_path_buf();
    while !fs::try_exists(&check_path).await.unwrap_or_default() {
        match check_path.parent() {
            Some(parent) => check_path = parent.to_path_buf(),
            None => return true,
        }
    }
    !self.is_root_contained(check_path.as_path()).await
}

This way, for PUT /subdir/file.txt where neither the file nor subdir/ exists, it walks up to . (the serve root), which does exist and canonicalizes correctly.

Workarounds

  • Set allow-symlink: true (bypasses the guard entirely — safe for personal use since resolve_path() still blocks .. traversal)
  • Downgrade to v0.45.0
  • Create the target directory first via the "New Folder" button, then drag files into it

Environment

  • dufs v0.46.0
  • Linux
  • Browser: any (tested with drag-and-drop)
Originally created by @nirvana6 on GitHub (May 27, 2026). Original GitHub issue: https://github.com/sigoden/dufs/issues/714 ## Bug Description Dragging a directory onto the dufs web UI causes all files to queue with status `-` and then fail with **404 Not Found**. Single file uploads to the root directory work fine. ## Root Cause This is a regression introduced in v0.46.0 by PR #670 ("fix: ensure symlink inside serve root"). ### Before v0.46.0 (v0.45.0) The root containment check was: ```rust if !self.args.allow_symlink && !is_miss && !self.is_root_contained(path).await { status_not_found(&mut res); return Ok(res); } ``` When `is_miss = true` (path does not exist yet), `!is_miss` is `false`, so the entire condition is `false` and the request proceeds to `handle_upload()`, which calls `ensure_path_parent()` to create the parent directory. ### v0.46.0 (current) PR #670 replaced the above with `guard_root_contained()`: ```rust if self.guard_root_contained(path).await { self.handle_not_found(&query_params, headers, head_only, &mut res) .await?; return Ok(res); } ``` This check is **unconditional** — it runs for all requests regardless of whether the path exists. The function tries to handle non-existent paths by checking the parent: ```rust async fn guard_root_contained(&self, path: &Path) -> bool { if self.args.allow_symlink { return false; } let path = if !fs::try_exists(path).await.unwrap_or_default() { match path.parent() { Some(parent) => parent.to_path_buf(), None => return true, } } else { path.to_path_buf() }; !self.is_root_contained(path.as_path()).await } ``` But when uploading `PUT /subdir/file.txt` where `subdir/` does not exist yet: 1. `fs::try_exists(path)` → `false` (file does not exist) 2. Takes `path.parent()` → `subdir/` 3. `is_root_contained(subdir/)` calls `fs::canonicalize(subdir/)` which **fails** because `subdir/` does not exist yet 4. `.ok().map(...).unwrap_or_default()` → `false` 5. `guard_root_contained` returns `!false` → `true` 6. Server returns 404 **before** `handle_upload()` is reached 7. `ensure_path_parent()` inside `handle_upload()` — which would create `subdir/` — is never called ## Reproduction 1. Start dufs v0.46.0 with `--allow-upload` 2. Open the web UI in a browser 3. Drag a directory (containing files) from the file manager onto the page 4. Files appear in the upload queue with status `-`, then show `✗` with tooltip "404 Not Found" Single file uploads to the root directory work correctly because the parent (`.`) already exists. ## Suggested Fix In `guard_root_contained()`, walk up the directory tree until an existing ancestor is found, then canonicalize that: ```rust async fn guard_root_contained(&self, path: &Path) -> bool { if self.args.allow_symlink { return false; } let mut check_path = path.to_path_buf(); while !fs::try_exists(&check_path).await.unwrap_or_default() { match check_path.parent() { Some(parent) => check_path = parent.to_path_buf(), None => return true, } } !self.is_root_contained(check_path.as_path()).await } ``` This way, for `PUT /subdir/file.txt` where neither the file nor `subdir/` exists, it walks up to `.` (the serve root), which does exist and canonicalizes correctly. ## Workarounds - Set `allow-symlink: true` (bypasses the guard entirely — safe for personal use since `resolve_path()` still blocks `..` traversal) - Downgrade to v0.45.0 - Create the target directory first via the "New Folder" button, then drag files into it ## Environment - dufs v0.46.0 - Linux - Browser: any (tested with drag-and-drop)
zhus closed this issue 2026-06-03 00:39:39 +03:00
Author
Owner

@florinm03 commented on GitHub (May 30, 2026):

Why close this? 👀 Or has it been fixed? @sigoden

<!-- gh-comment-id:4583468600 --> @florinm03 commented on GitHub (May 30, 2026): Why close this? 👀 Or has it been fixed? @sigoden
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sigoden/dufs#8817