PKG_NAME:=dufs
PKG_VERSION:=0.44.0
-PKG_RELEASE:=1
+PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/sigoden/dufs/tar.gz/v$(PKG_VERSION)?
--- /dev/null
+From b2f244a4cfeb492b38ad9b92692e230e04540ea0 Mon Sep 17 00:00:00 2001
+Date: Sat, 16 Aug 2025 07:36:19 +0800
+Subject: [PATCH] feat: make dir urls inherit `?noscript` params (#614)
+
+---
+ src/noscript.rs | 13 ++++++++-----
+ 1 file changed, 8 insertions(+), 5 deletions(-)
+
+--- a/src/noscript.rs
++++ b/src/noscript.rs
+@@ -55,17 +55,20 @@ pub fn generate_noscript_html(data: &Ind
+
+ fn render_parent() -> String {
+ let value = "../";
+- format!("<tr><td><a href=\"{value}\">{value}</a></td><td></td><td></td></tr>")
++ format!("<tr><td><a href=\"{value}?noscript\">{value}</a></td><td></td><td></td></tr>")
+ }
+
+ fn render_path_item(path: &PathItem) -> String {
+- let href = encode_uri(&path.name);
+- let suffix = if path.path_type.is_dir() { "/" } else { "" };
+- let name = escape_str_pcdata(&path.name);
++ let mut href = encode_uri(&path.name);
++ let mut name = escape_str_pcdata(&path.name).to_string();
++ if path.path_type.is_dir() {
++ href.push_str("/?noscript");
++ name.push('/');
++ };
+ let mtime = format_mtime(path.mtime).unwrap_or_default();
+ let size = format_size(path.size, path.path_type);
+
+- format!("<tr><td><a href=\"{href}{suffix}\">{name}{suffix}</a></td><td>{mtime}</td><td>{size}</td></tr>")
++ format!("<tr><td><a href=\"{href}\">{name}</a></td><td>{mtime}</td><td>{size}</td></tr>")
+ }
+
+ fn format_mtime(mtime: u64) -> Option<String> {
--- /dev/null
+From f8a7873582567a85095ca9d2124b185cd3eb2ffd Mon Sep 17 00:00:00 2001
+Date: Tue, 19 Aug 2025 07:51:52 +0800
+Subject: [PATCH] fix: perms on `dufs -A -a @/:ro` (#619)
+
+---
+ src/auth.rs | 9 ++++++---
+ src/server.rs | 4 ++--
+ tests/auth.rs | 18 ++++++++++++++++++
+ 3 files changed, 26 insertions(+), 5 deletions(-)
+
+--- a/src/auth.rs
++++ b/src/auth.rs
+@@ -30,6 +30,7 @@ lazy_static! {
+
+ #[derive(Debug, Clone, PartialEq)]
+ pub struct AccessControl {
++ empty: bool,
+ use_hashed_password: bool,
+ users: IndexMap<String, (String, AccessPaths)>,
+ anonymous: Option<AccessPaths>,
+@@ -38,6 +39,7 @@ pub struct AccessControl {
+ impl Default for AccessControl {
+ fn default() -> Self {
+ AccessControl {
++ empty: true,
+ use_hashed_password: false,
+ users: IndexMap::new(),
+ anonymous: Some(AccessPaths::new(AccessPerm::ReadWrite)),
+@@ -48,7 +50,7 @@ impl Default for AccessControl {
+ impl AccessControl {
+ pub fn new(raw_rules: &[&str]) -> Result<Self> {
+ if raw_rules.is_empty() {
+- return Ok(Default::default());
++ return Ok(Self::default());
+ }
+ let new_raw_rules = split_rules(raw_rules);
+ let mut use_hashed_password = false;
+@@ -93,13 +95,14 @@ impl AccessControl {
+ }
+
+ Ok(Self {
++ empty: false,
+ use_hashed_password,
+ users,
+ anonymous,
+ })
+ }
+
+- pub fn exist(&self) -> bool {
++ pub fn has_users(&self) -> bool {
+ !self.users.is_empty()
+ }
+
+@@ -111,7 +114,7 @@ impl AccessControl {
+ token: Option<&String>,
+ guard_options: bool,
+ ) -> (Option<String>, Option<AccessPaths>) {
+- if self.users.is_empty() {
++ if self.empty {
+ return (None, Some(AccessPaths::new(AccessPerm::ReadWrite)));
+ }
+
+--- a/src/server.rs
++++ b/src/server.rs
+@@ -962,7 +962,7 @@ impl Server {
+ uri_prefix: self.args.uri_prefix.clone(),
+ allow_upload: self.args.allow_upload,
+ allow_delete: self.args.allow_delete,
+- auth: self.args.auth.exist(),
++ auth: self.args.auth.has_users(),
+ user,
+ editable,
+ };
+@@ -1226,7 +1226,7 @@ impl Server {
+ allow_search: self.args.allow_search,
+ allow_archive: self.args.allow_archive,
+ dir_exists: exist,
+- auth: self.args.auth.exist(),
++ auth: self.args.auth.has_users(),
+ user,
+ paths,
+ };
+--- a/tests/auth.rs
++++ b/tests/auth.rs
+@@ -126,6 +126,24 @@ fn auth_skip_if_no_auth_user(server: Tes
+ }
+
+ #[rstest]
++fn auth_no_skip_if_anonymous(
++ #[with(&["--auth", "@/:ro"])] server: TestServer,
++) -> Result<(), Error> {
++ let url = format!("{}index.html", server.url());
++ let resp = fetch!(b"GET", &url)
++ .basic_auth("user", Some("pass"))
++ .send()?;
++ assert_eq!(resp.status(), 401);
++ let resp = fetch!(b"GET", &url).send()?;
++ assert_eq!(resp.status(), 200);
++ let resp = fetch!(b"DELETE", &url)
++ .basic_auth("user", Some("pass"))
++ .send()?;
++ assert_eq!(resp.status(), 401);
++ Ok(())
++}
++
++#[rstest]
+ fn auth_check(
+ #[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer,
+ ) -> Result<(), Error> {
--- /dev/null
+From 4016715187db5bd84c7d15ea6abcd99fd4a0a667 Mon Sep 17 00:00:00 2001
+Date: Tue, 19 Aug 2025 08:58:59 +0800
+Subject: [PATCH] fix: login btn does not work for readonly annoymous (#620)
+
+---
+ assets/index.js | 7 ++++---
+ src/server.rs | 13 ++++++++++++-
+ tests/auth.rs | 16 ++++++++++++++--
+ 3 files changed, 30 insertions(+), 6 deletions(-)
+
+--- a/assets/index.js
++++ b/assets/index.js
+@@ -534,7 +534,7 @@ async function setupAuth() {
+ $loginBtn.classList.remove("hidden");
+ $loginBtn.addEventListener("click", async () => {
+ try {
+- await checkAuth();
++ await checkAuth("login");
+ } catch { }
+ location.reload();
+ });
+@@ -782,9 +782,10 @@ async function saveChange() {
+ }
+ }
+
+-async function checkAuth() {
++async function checkAuth(variant) {
+ if (!DATA.auth) return;
+- const res = await fetch(baseUrl(), {
++ const qs = variant ? `?${variant}` : "";
++ const res = await fetch(baseUrl() + qs, {
+ method: "CHECKAUTH",
+ });
+ await assertResOK(res);
+--- a/src/server.rs
++++ b/src/server.rs
+@@ -211,7 +211,18 @@ impl Server {
+ }
+
+ if method.as_str() == "CHECKAUTH" {
+- *res.body_mut() = body_full(user.clone().unwrap_or_default());
++ match user.clone() {
++ Some(user) => {
++ *res.body_mut() = body_full(user);
++ }
++ None => {
++ if has_query_flag(&query_params, "login") || !access_paths.perm().readwrite() {
++ self.auth_reject(&mut res)?
++ } else {
++ *res.body_mut() = body_full("");
++ }
++ }
++ }
+ return Ok(res);
+ } else if method.as_str() == "LOGOUT" {
+ self.auth_reject(&mut res)?;
+--- a/tests/auth.rs
++++ b/tests/auth.rs
+@@ -147,7 +147,7 @@ fn auth_no_skip_if_anonymous(
+ fn auth_check(
+ #[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer,
+ ) -> Result<(), Error> {
+- let url = format!("{}index.html", server.url());
++ let url = format!("{}", server.url());
+ let resp = fetch!(b"CHECKAUTH", &url).send()?;
+ assert_eq!(resp.status(), 401);
+ let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
+@@ -161,7 +161,7 @@ fn auth_check(
+ fn auth_check2(
+ #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer,
+ ) -> Result<(), Error> {
+- let url = format!("{}index.html", server.url());
++ let url = format!("{}", server.url());
+ let resp = fetch!(b"CHECKAUTH", &url).send()?;
+ assert_eq!(resp.status(), 401);
+ let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
+@@ -171,6 +171,18 @@ fn auth_check2(
+ Ok(())
+ }
+
++#[rstest]
++fn auth_check3(
++ #[with(&["--auth", "user:pass@/:rw", "--auth", "@/dir1:rw", "-A"])] server: TestServer,
++) -> Result<(), Error> {
++ let url = format!("{}dir1/", server.url());
++ let resp = fetch!(b"CHECKAUTH", &url).send()?;
++ assert_eq!(resp.status(), 200);
++ let resp = fetch!(b"CHECKAUTH", format!("{url}?login")).send()?;
++ assert_eq!(resp.status(), 401);
++ Ok(())
++}
++
+ #[rstest]
+ fn auth_logout(
+ #[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer,