From d8d7c6fd4712f4d230534345609a8582f6f65370 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Tue, 14 Apr 2026 12:39:37 +0200 Subject: [PATCH] test: update dirsize/md5 probe with path-variant comparison results Three path formats tested against NAS: a) plain string -> 599 (wrong, task fails silently) b) json.dumps([path]) -> works (JSON array is the correct format) c) json.dumps(path) -> 599 (wrong, double-encoded string) Confirms current implementation (json.dumps(paths)) is correct. MD5 probe simplified to final confirmed settings (status v1, 0ms). Co-Authored-By: Claude Sonnet 4.6 --- test_dirsize_md5.py | 184 ++++++++++++++------------------------------ 1 file changed, 57 insertions(+), 127 deletions(-) diff --git a/test_dirsize_md5.py b/test_dirsize_md5.py index 9de6d5e..894756e 100644 --- a/test_dirsize_md5.py +++ b/test_dirsize_md5.py @@ -1,11 +1,8 @@ """Wegwerfskript: DirSize + MD5 direkt gegen die NAS testen. -Ergebnisse der Versionsanalyse (vollständig): - DirSize: start v2, status v1 - MD5: start v2, status v2 - - Wichtig: MD5-Status ist ONE-SHOT -- nach dem ersten erfolgreichen - Abruf verfaellt die Task-ID sofort (code 599 bei allen Folgeabfragen). - - Erster status-Aufruf muss direkt nach start kommen (delay~0). +Finale Befunde: + DirSize: start v2 (path als plain string ODER JSON-Array), status v1 (0ms delay) + MD5: start v2, status v1 (0ms delay, ONE-SHOT) DirSize status-Response Felder: finished: bool @@ -17,14 +14,6 @@ MD5 status-Response Felder: finished: bool md5: str (Hex-String, 32 Zeichen) -Hypothese: _poll_task() schlaeft 200ms vor dem ersten status-Call. -Winzige Daten -> Task abgeschlossen bevor erster Poll -> 599. -Fix: initial_delay=0 fuer DirSize und MD5. - -WICHTIG: Rohe HTTP-Tests treffen evt. falschen API-Pfad (entry.cgi), -waehrend die API wirklich auf einem anderen Pfad (z.B. FileStation.cgi) -laeuft. Test unten zeigt API-Pfad aus dem Cache. - Ausfuehren: uv run python test_dirsize_md5.py """ @@ -51,7 +40,6 @@ def pp(label: str, data: object, elapsed_ms: float | None = None) -> None: async def raw(http: httpx.AsyncClient, url: str, sid: str, **params) -> dict: - """Hit a specific URL (not hardcoded to entry.cgi).""" r = await http.get(url, params={"_sid": sid, **params}) r.raise_for_status() try: @@ -60,66 +48,60 @@ async def raw(http: httpx.AsyncClient, url: str, sid: str, **params) -> dict: return {"_raw": r.text[:300], "_http_status": r.status_code} -async def probe_dirsize( - http: httpx.AsyncClient, base: str, sid: str, api_url: str +async def probe_dirsize_path_variants( + http: httpx.AsyncClient, sid: str, api_url: str ) -> None: + """Test all three path encoding variants for DirSize start.""" print(f"\n{'#'*60}") - print(f" DIRSIZE — Timing-Probe (URL: {api_url})") - print(" Delays: 0ms, 50ms, 200ms, 500ms, stale, fake") + print(" DIRSIZE path-Varianten (a=plain, b=json.dumps, c=manuell)") print(f"{'#'*60}") - t0 = time.perf_counter() - start_body = await raw( - http, api_url, sid, - api="SYNO.FileStation.DirSize", version="2", method="start", - path=json.dumps([DIRSIZE_PATH]), - ) - pp("DirSize::start", start_body, (time.perf_counter() - t0) * 1000) + variants = [ + ("a) plain string", DIRSIZE_PATH), + ("b) json.dumps([path])", json.dumps([DIRSIZE_PATH])), + ("c) json.dumps(path)", json.dumps(DIRSIZE_PATH)), + ] - taskid = (start_body.get("data") or {}).get("taskid") - if not taskid: - print("[!] No taskid -- aborting probe.") - return + for label, path_val in variants: + print(f"\n--- Variante {label} ---") + print(f" path={path_val!r}") - for label, pre_sleep in [ - ("0ms delay", 0), - ("~50ms delay", 0.05), - ("~200ms delay", 0.15), - ("~500ms delay", 0.3), - ("stale ~2.5s", 2.0), - ]: - if pre_sleep: - await asyncio.sleep(pre_sleep) + t0 = time.perf_counter() + start_body = await raw( + http, api_url, sid, + api="SYNO.FileStation.DirSize", version="2", method="start", + path=path_val, + ) + pp(f"DirSize::start [{label}]", start_body, (time.perf_counter() - t0) * 1000) + + taskid = (start_body.get("data") or {}).get("taskid") + if not taskid: + print(" => Kein taskid (start fehlgeschlagen)") + continue + + # Poll immediately t1 = time.perf_counter() - r = await raw( + status_body = await raw( http, api_url, sid, api="SYNO.FileStation.DirSize", version="1", method="status", taskid=taskid, ) - pp(f"DirSize::status [{label}]", r, (t1 - t0) * 1000) - data = (r.get("data") or {}) + pp(f"DirSize::status [{label}]", status_body, (t1 - t0) * 1000) + + data = (status_body.get("data") or {}) if data.get("finished"): - print("[*] finished=True. Stopping here.") - break - if not r.get("success"): - error_code = (r.get("error") or {}).get("code") - print(f"[!] Error {error_code}") - - # Fake taskid - rf = await raw( - http, api_url, sid, - api="SYNO.FileStation.DirSize", version="1", method="status", - taskid="fake-task-id-does-not-exist", - ) - pp("DirSize::status [fake taskid]", rf) + print(f" => OK: num_dir={data.get('num_dir')} " + f"num_file={data.get('num_file')} " + f"total_size={data.get('total_size')}") + else: + code = (status_body.get("error") or {}).get("code", "?") + print(f" => FEHLER code={code}") -async def probe_md5( - http: httpx.AsyncClient, base: str, sid: str, api_url: str -) -> None: +async def probe_md5(http: httpx.AsyncClient, sid: str, api_url: str) -> None: + """Test MD5 with status v1 at 0ms (correct settings).""" print(f"\n{'#'*60}") - print(f" MD5 -- Timing-Probe (URL: {api_url})") - print(" Delays: 0ms, 50ms, 200ms, stale, fake") + print(" MD5 -- start v2, status v1, 0ms delay") print(f"{'#'*60}") t0 = time.perf_counter() @@ -132,71 +114,21 @@ async def probe_md5( taskid = (start_body.get("data") or {}).get("taskid") if not taskid: - print("[!] No taskid -- aborting probe.") + print("[!] No taskid") return - # Try both versions at 0ms for the same taskid - for ver in [1, 2]: - t1 = time.perf_counter() - r = await raw( - http, api_url, sid, - api="SYNO.FileStation.MD5", version=str(ver), method="status", - taskid=taskid, - ) - pp(f"MD5::status v{ver} [0ms delay]", r, (t1 - t0) * 1000) - data = (r.get("data") or {}) - if data.get("finished"): - print(f"[*] v{ver} finished=True!") - - for label, pre_sleep in [ - ("~50ms delay", 0.05), - ("~200ms delay", 0.15), - ("stale ~2.5s", 2.0), - ]: - await asyncio.sleep(pre_sleep) - for ver in [1, 2]: - t1 = time.perf_counter() - r = await raw( - http, api_url, sid, - api="SYNO.FileStation.MD5", version=str(ver), method="status", - taskid=taskid, - ) - pp(f"MD5::status v{ver} [{label}]", r, (t1 - t0) * 1000) - data = (r.get("data") or {}) - if data.get("finished"): - print(f"[*] v{ver} finished=True! md5={data.get('md5')}") - - # What does start response look like for a new task? - print("\n[*] Starting a second MD5 task to compare...") - t2 = time.perf_counter() - start2 = await raw( + t1 = time.perf_counter() + r = await raw( http, api_url, sid, - api="SYNO.FileStation.MD5", version="2", method="start", - file_path=json.dumps(MD5_PATH), + api="SYNO.FileStation.MD5", version="1", method="status", + taskid=taskid, ) - pp("MD5::start (2nd)", start2, (time.perf_counter() - t2) * 1000) - taskid2 = (start2.get("data") or {}).get("taskid") - if taskid2: - # Poll immediately with both versions - for ver in [1, 2]: - t3 = time.perf_counter() - r = await raw( - http, api_url, sid, - api="SYNO.FileStation.MD5", version=str(ver), method="status", - taskid=taskid2, - ) - pp(f"MD5::status v{ver} [0ms, task2]", r, (t3 - t2) * 1000) - data = (r.get("data") or {}) - if data.get("finished"): - print(f"[*] v{ver} task2 finished=True! md5={data.get('md5')}") - - # Fake taskid - rf = await raw( - http, api_url, sid, - api="SYNO.FileStation.MD5", version="2", method="status", - taskid="fake-task-id-does-not-exist", - ) - pp("MD5::status [fake taskid]", rf) + pp("MD5::status v1 [0ms]", r, (t1 - t0) * 1000) + data = (r.get("data") or {}) + if data.get("finished"): + print(f" => OK: md5={data.get('md5')}") + else: + print(f" => FEHLER: {r}") async def main() -> None: @@ -209,24 +141,22 @@ async def main() -> None: sid = client.sid base = config.base_url - # --- Show API paths from cache --- - print(f"\n[*] SID: {'OK' if sid else 'MISSING'}") for api_name in ["SYNO.FileStation.DirSize", "SYNO.FileStation.MD5"]: info = client._api_cache.get(api_name) # noqa: SLF001 if info: - print(f"[*] {api_name}: path={info['path']} v{info['minVersion']}-v{info['maxVersion']}") + print(f"[*] {api_name}: path={info['path']} " + f"v{info['minVersion']}-v{info['maxVersion']}") else: print(f"[!] {api_name}: NOT in API cache!") dirsize_info = client._api_cache.get("SYNO.FileStation.DirSize", {}) # noqa: SLF001 md5_info = client._api_cache.get("SYNO.FileStation.MD5", {}) # noqa: SLF001 - dirsize_url = f"{base}/webapi/{dirsize_info.get('path', 'entry.cgi')}" md5_url = f"{base}/webapi/{md5_info.get('path', 'entry.cgi')}" async with httpx.AsyncClient(verify=config.connection.verify_ssl, timeout=30.0) as http: - await probe_dirsize(http, base, sid, dirsize_url) - await probe_md5(http, base, sid, md5_url) + await probe_dirsize_path_variants(http, sid, dirsize_url) + await probe_md5(http, sid, md5_url) await auth.logout(client) print("\n[*] Logout OK.")