Commit Graph

17 Commits

Author SHA1 Message Date
marcus 8b2f07d9c3 revert: restore _poll_task and dir_size/get_md5 to 0.2.2 state
All changes since 0.2.2 to _poll_task, dir_size, and get_md5 (window_timeout,
_poll_oneshot, start_and_poll_immediately) are reverted. The 0.2.2 behaviour
worked reliably for small directories and is the last known-good baseline.

The remaining known limitation (occasional 599 on large directories) is
documented in SPEC.md. Retry the operation as a workaround.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 14:18:43 +02:00
marcus 62f8e41931 fix: _poll_oneshot for DirSize/MD5 with burst-retry on early 599
Add FileStationClient.start_and_poll_immediately: starts the async task and
immediately makes the first status poll within the same method, with no
intermediate awaits other than the two HTTP calls. This minimises scheduler
latency between start and first poll for one-shot tasks.

_poll_oneshot now accepts the first_status from start_and_poll_immediately:
- finished=True on first poll → return immediately
- finished=False → Phase 2 (exponential backoff, 60 s timeout)
- None (first poll was 599) → burst-retry 10× at 10 ms, then Phase 2
  (Phase 2 keeps polling through 599 until seen_alive, then fails fast)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:50:28 +02:00
marcus 6510493930 fix: window_timeout for one-shot tasks, drop debug stderr logging
_poll_task now accepts window_timeout. For DirSize and MD5 (one-shot result
windows), if only 599 errors arrive for window_timeout seconds without ever
seeing the task alive (finished=False), return a fast "result window missed —
please retry" error instead of waiting the full 60 s. Tasks that return
finished=False at least once (large dirs, large files) are unaffected.

Also removes the stale [dsm] debug stderr.write left in client.request().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:30:20 +02:00
marcus 4bf655236d fix: treat DSM 599 as task-not-ready in _poll_task, poll until 60s timeout
DirSize/MD5 return error 599 while the async task is still initialising on
the NAS, not only after the task is gone. Remove the 5-consecutive-599 abort
limit and the debug stderr logging; instead pass on 599 and keep polling
until the existing 60 s timeout fires. Rename the test that checked the old
limit to reflect the new timeout-based behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:19:32 +02:00
marcus e3fa71b458 fix: retry _poll_task on transient 599 instead of aborting immediately
DirSize for large directories (e.g. /docker, 8441 folders, 46832 files)
takes ~800ms to compute. While running, status returns intermediate
progress (finished=false). But on the very first poll the task can return
599 transiently (task just started, not yet available). Previously
_poll_task caught any SynologyError and returned immediately, making
dir_size always fail on the first 599.

Fix: treat 599 as a transient condition and continue polling. Give up
only after 5 consecutive 599 responses. All other error codes remain
immediately fatal.

Investigation confirmed with test_dirsize_md5.py:
- /test-mcp (2937 B): finished=true at 0ms
- /docker (3.9 GB, 46832 files): finished=false at 35ms, finished=true at 789ms

Tests: 2 new cases (retry-succeeds, 5x-599-gives-up) → 95 total

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:01:27 +02:00
marcus 1d0cf940b4 feat: add dir_size and get_md5 tools
dir_size: SYNO.FileStation.DirSize v2 — async scan of one or more
directories, returns folder/file count and total size as a table.
get_md5: SYNO.FileStation.MD5 v2 — async MD5 checksum of a file.
Both follow the existing _poll_task pattern. 9 new unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 12:10:51 +02:00
marcus 473c771c20 feat: add compress and extract tools
Implements SYNO.FileStation.Compress (v3) and SYNO.FileStation.Extract (v2)
with async polling identical to copy/move. Includes input validation for
compress (level, mode, format, empty paths) and 11 new unit tests.
Bumps version to 0.2.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 11:26:08 +02:00
marcus dbab842738 feat: add check_exist tool
SYNO.FileStation.CheckExist returns error 400 for every parameter format on
this DSM firmware, so the tool falls back to SYNO.FileStation.List::getinfo.
DSM returns an entry per requested path with name=None for non-existent paths,
which provides a reliable exists/not-exists signal.

Accepts a single path or a comma-separated list; returns a table of
Path | Exists (Yes/No) with a count footer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 10:49:50 +02:00
marcus 80ac894165 feat: add create_folder, rename, copy, move, delete, upload tools
All path/name params are json.dumps-wrapped per confirmed DSM behaviour.
copy and move use async polling via a shared _poll_task helper
(exponential backoff 200ms→2s, 60s timeout). delete requires
confirmed=True; without it only a preview is returned and no DSM call
is made. upload decodes base64 and enforces a 50 MB cap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 10:28:32 +02:00
marcus 544cfb8b06 fix: retain last non-empty result set in search polling loop
DSM sends files=[] on the final finished=True poll even when results
exist in earlier rounds. Overwriting files each iteration discarded
the real results, producing a false "No files found" response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:48:05 +02:00
marcus c47221dc8f feat: add search and download tools
Implements SYNO.FileStation.Search with async polling (exponential
backoff 200ms→2s, 60s timeout) and SYNO.FileStation.Download with
base64 output and a 10 MB size cap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:43:48 +02:00
marcus 1bccf1e5d2 fix: use json.dumps(paths) for getinfo multi-path, delete probe script
DSM accepts multiple paths as a JSON array string, not comma-separated.
Comma-separated is treated as a single literal path; repeated path[]
params return error 400. Confirmed via test_getinfo_multipath.py.

- get_info: path param changed from ",".join(paths) to json.dumps(paths)
- tests: update multi-path assertion to expect JSON array format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:32:11 +02:00
marcus 014af1aefe feat: add get_info tool using SYNO.FileStation.List::getinfo
SYNO.FileStation.Stat is absent from this NAS's API registry.
SYNO.FileStation.List::getinfo returns identical data and is confirmed
working.

- tools/filestation.py: new get_info tool — accepts one or more
  comma-separated paths, calls getinfo with real_path/size/time/perm/
  owner/type additional fields, returns a 9-column table
- tests: 6 new tests covering single file, directory, multi-path,
  empty input, DSM error, and correct API method assertion
- SPEC.md: remove SYNO.FileStation.Stat from API table, rewrite get_info
  tool section to reference getinfo, update list_dir note
- CLAUDE.md: update Implemented Tools list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:28:04 +02:00
marcus 8fc2f731ce fix: restore additional=["size","time"] to list_dir, update SPEC + tests
Root cause of DSM 408 was wrong path format (volume path vs share path),
not the additional parameter. Confirmed via test_additional.py that
json.dumps(["size","time"]) is the correct format; comma-separated string
is silently ignored by DSM.

- list_dir: restore 4-column table (Name, Type, Size, Modified)
- list_dir: use additional=json.dumps(["size","time"]) (confirmed working)
- SPEC.md: document share path requirement, additional format rules,
  note SYNO.FileStation.Stat unavailability, remove comma-sep gotcha
- tests: restore size/mtime mock data and assertions
- delete test_additional.py (throwaway diagnostic script)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:21:52 +02:00
marcus a3e1a557c3 fix: use share paths, drop additional from list_dir, remove debug logging
Root cause: DSM expects share paths (/dev) not volume paths (/volume1/dev).
The 408 errors were triggered by any additional field on the wrong path format.

- list_shares: use share["path"] directly (e.g. /dev), drop real_path from
  additional — only volume_status remains
- list_dir: remove additional parameter entirely; table now shows name + type
  (isdir is returned by default); update docstring to show share path examples
- client.py: remove diagnostic REQUEST and RAW ERROR stderr logging
- tests: update assertions to match share paths and two-column table output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:11:10 +02:00
marcus 59301ae760 feat: implement auth infrastructure and first two FileStation tools
- auth.py: AuthManager with OS keyring, env var fallback, 2FA device token flow
- config.py: AppConfig/ConnectionConfig dataclasses, YAML load/save, env overrides
- client.py: FileStationClient with lazy init, session re-auth, upload/download
- cli.py: setup / check / serve subcommands (anyio.run throughout)
- server.py: create_server factory wiring FastMCP to FileStation tools
- tools/filestation.py: list_shares and list_dir with ASCII table output,
  pagination hints, input validation, DSM error mapping
- tests: 30 unit tests, all passing (auth, config, tools)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 08:23:34 +02:00
marcus 9fc5a3d68c feat: initial project structure 2026-04-14 07:51:51 +02:00