From 0fbadf0c9c8f81a21ff4db7fb1715bcd147abacc Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 18 Sep 2026 06:58:06 +0800 Subject: [PATCH] feat: secure web UI and expose runtime settings --- README.md | 2 +- am_deduper/web.py | 29 +++++++++++++++++++++++++---- docker-compose.yml | 2 ++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 309ef53..936c0e4 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ http://:18080/ - 历史:查看 Hard Link 操作,并对仍保持同 inode 的操作执行安全回滚。 - 扫描会把数据库中已经不存在的文件标记为 `missing`,避免脏索引继续计入统计。 -> Web UI 当前面向 NAS 内网使用,未启用账号密码;不要直接暴露到公网。 +> Web UI 支持可选管理 Token。生产环境建议在 NAS 的 Compose 同目录 `.env` 中设置 `AM_DEDUPER_TOKEN=<随机长字符串>`;不要把 Token 提交到 Git。设置后 Web UI 会在首次访问 API 时要求输入 Token。若未设置 Token,则仅适合可信内网环境,不要直接暴露到公网。 更新版本: diff --git a/am_deduper/web.py b/am_deduper/web.py index 054f6cb..73508ec 100644 --- a/am_deduper/web.py +++ b/am_deduper/web.py @@ -5,10 +5,12 @@ from pathlib import Path from urllib.parse import urlparse from .db import connect from .scanner import scan -from .exact import exact_dedupe, hardlink_duplicate +from .exact import exact_dedupe, hardlink_duplicate, choose_winner from .l2 import acoustic_dedupe -ROOT=Path("/music/AllMusic"); DB=Path("/data/dedupe.db") +ROOT=Path(os.environ.get("AM_DEDUPER_ROOT","/music/AllMusic")).resolve() +DB=Path(os.environ.get("AM_DEDUPER_DB","/data/dedupe.db")).resolve() +TOKEN=os.environ.get("AM_DEDUPER_TOKEN","").strip() LOCK=threading.Lock() HTML=r''' @@ -30,11 +32,12 @@ button.primary{background:#111827;color:#fff}button.danger{background:#fee2e2}ta

待人工审核

任务

最近操作

+

设置

加载中...
''' @@ -66,7 +71,11 @@ def duplicates(): for r in rows: groups.setdefault(r["sha256"],[]).append(dict(r)) out=[] for _,items in groups.items(): - if len(items)>1: out.append({"type":"SHA256 完全重复","savings":round(sum(x["file_size"] for x in items[1:])/1024/1024,2),"items":items}) + if len(items)>1: + winner=choose_winner(items) + ordered=[winner]+[x for x in items if x["id"]!=winner["id"]] + savings=round(sum(x["file_size"] for x in ordered[1:])/1024/1024,2) + out.append({"type":"SHA256 完全重复","savings":savings,"winner_id":winner["id"],"items":ordered}) return {"groups":out[:100]} def review_items(): @@ -87,6 +96,11 @@ def jobs(): with connect(DB) as c: rows=c.execute("SELECT * FROM jobs ORDER BY id DESC LIMIT 30").fetchall() return {"items":[dict(r) for r in rows]} +def settings(): + from .l2 import MIN_SIMILARITY, REVIEW_SIMILARITY, DURATION_DELTA + return {"root":str(ROOT),"db":str(DB),"auth":"Token 已启用" if TOKEN else "未启用(仅建议内网)", + "l2_min":MIN_SIMILARITY,"l2_review":REVIEW_SIMILARITY,"duration_delta":DURATION_DELTA} + def run_job(kind): with connect(DB) as c: cur=c.execute("INSERT INTO jobs(kind,status) VALUES(?,?)",(kind,"running")); jid=cur.lastrowid @@ -133,20 +147,27 @@ class Handler(BaseHTTPRequestHandler): def send(self,status,data,ctype="application/json"): raw=data.encode() if isinstance(data,str) else json.dumps(data,ensure_ascii=False).encode() self.send_response(status); self.send_header("Content-Type",ctype+"; charset=utf-8"); self.send_header("Content-Length",str(len(raw))); self.end_headers(); self.wfile.write(raw) + def authorized(self): + if not TOKEN: return True + return self.headers.get("Authorization","") == "Bearer "+TOKEN + def do_GET(self): p=urlparse(self.path).path try: if p=="/": return self.send(200,HTML,"text/html") + if not self.authorized(): return self.send(401,{"error":"unauthorized"}) if p=="/api/stats": return self.send(200,stats()) if p=="/api/duplicates": return self.send(200,duplicates()) if p=="/api/review": return self.send(200,review_items()) if p=="/api/history": return self.send(200,history()) if p=="/api/jobs": return self.send(200,jobs()) + if p=="/api/settings": return self.send(200,settings()) return self.send(404,{"error":"not found"}) except Exception as e: return self.send(500,{"error":str(e)}) def do_POST(self): p=urlparse(self.path).path + if not self.authorized(): return self.send(401,{"error":"unauthorized"}) try: body=json.loads(self.rfile.read(int(self.headers.get("content-length","0"))) or b"{}") except Exception: return self.send(400,{"error":"invalid json"}) try: diff --git a/docker-compose.yml b/docker-compose.yml index e5f4922..31a7859 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: environment: TZ: Asia/Shanghai PYTHONUNBUFFERED: "1" + AM_DEDUPER_TOKEN: ${AM_DEDUPER_TOKEN:-} volumes: - /vol2/1000/Docker/chfs/data/Backup/AllMusic:/music/AllMusic - /vol2/1000/Docker/am-deduper/data:/data @@ -29,6 +30,7 @@ services: environment: TZ: Asia/Shanghai PYTHONUNBUFFERED: "1" + AM_DEDUPER_TOKEN: ${AM_DEDUPER_TOKEN:-} volumes: - /vol2/1000/Docker/chfs/data/Backup/AllMusic:/music/AllMusic - /vol2/1000/Docker/am-deduper/data:/data