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>
This commit is contained in:
@@ -238,7 +238,7 @@ def register_filestation(
|
|||||||
"SYNO.FileStation.List",
|
"SYNO.FileStation.List",
|
||||||
"getinfo",
|
"getinfo",
|
||||||
params={
|
params={
|
||||||
"path": ",".join(paths),
|
"path": json.dumps(paths),
|
||||||
"additional": json.dumps(
|
"additional": json.dumps(
|
||||||
["real_path", "size", "time", "perm", "owner", "type"]
|
["real_path", "size", "time", "perm", "owner", "type"]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
"""Throwaway script: test SYNO.FileStation.List::getinfo with multiple paths.
|
|
||||||
|
|
||||||
Run with:
|
|
||||||
uv run python test_getinfo_multipath.py
|
|
||||||
|
|
||||||
Tests three variants for passing multiple paths to getinfo and prints the
|
|
||||||
raw DSM response for each so we know which format the API actually accepts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# ── config ────────────────────────────────────────────────────────────────
|
|
||||||
# Change these to two paths that exist on your NAS (as returned by list_shares
|
|
||||||
# or list_dir — share-relative, e.g. "/dev/somefile.txt")
|
|
||||||
PATH_A = "/dev"
|
|
||||||
PATH_B = "/data"
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
async def run() -> None:
|
|
||||||
from mcp_synology_filestation.auth import AuthManager
|
|
||||||
from mcp_synology_filestation.client import FileStationClient
|
|
||||||
from mcp_synology_filestation.config import load_config
|
|
||||||
|
|
||||||
config = load_config()
|
|
||||||
auth = AuthManager(config)
|
|
||||||
|
|
||||||
async with FileStationClient(
|
|
||||||
config.base_url,
|
|
||||||
config.connection.verify_ssl,
|
|
||||||
config.connection.timeout,
|
|
||||||
) as client:
|
|
||||||
await client.query_api_info()
|
|
||||||
sid = await auth.login(client)
|
|
||||||
client.sid = sid
|
|
||||||
|
|
||||||
api = "SYNO.FileStation.List"
|
|
||||||
method = "getinfo"
|
|
||||||
api_info = client._api_cache[api]
|
|
||||||
version = api_info["maxVersion"]
|
|
||||||
url = f"{client._base_url}/webapi/{api_info['path']}"
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"SYNO.FileStation.List — path={api_info.get('path')!r} "
|
|
||||||
f"v{api_info.get('minVersion')}-v{api_info.get('maxVersion')}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
additional = json.dumps(["real_path", "size", "time", "perm", "owner", "type"])
|
|
||||||
|
|
||||||
http = client._http
|
|
||||||
|
|
||||||
# ── Variant 1: comma-separated string ─────────────────────────────
|
|
||||||
print("─" * 60)
|
|
||||||
print(f"Variant 1 — path='{PATH_A},{PATH_B}' (comma-separated string)")
|
|
||||||
base_params = {
|
|
||||||
"api": api,
|
|
||||||
"version": str(version),
|
|
||||||
"method": method,
|
|
||||||
"_sid": sid,
|
|
||||||
"additional": additional,
|
|
||||||
}
|
|
||||||
params_v1 = {**base_params, "path": f"{PATH_A},{PATH_B}"}
|
|
||||||
resp = await http.get(url, params=params_v1)
|
|
||||||
body = resp.json()
|
|
||||||
print(f" HTTP {resp.status_code} success={body.get('success')}")
|
|
||||||
if body.get("success"):
|
|
||||||
files = body.get("data", {}).get("files", [])
|
|
||||||
print(f" files returned: {len(files)}")
|
|
||||||
for f in files:
|
|
||||||
print(f" path={f.get('path')!r} isdir={f.get('isdir')} add={list((f.get('additional') or {}).keys())}")
|
|
||||||
else:
|
|
||||||
print(f" ERROR: {body!r}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# ── Variant 2: repeated path[] parameters ─────────────────────────
|
|
||||||
print("─" * 60)
|
|
||||||
print(f"Variant 2 — path[]={PATH_A!r} & path[]={PATH_B!r} (repeated params)")
|
|
||||||
# httpx accepts a list of tuples for repeated keys
|
|
||||||
params_v2_list = list(base_params.items()) + [("path[]", PATH_A), ("path[]", PATH_B)]
|
|
||||||
resp = await http.get(url, params=params_v2_list)
|
|
||||||
body = resp.json()
|
|
||||||
print(f" HTTP {resp.status_code} success={body.get('success')}")
|
|
||||||
if body.get("success"):
|
|
||||||
files = body.get("data", {}).get("files", [])
|
|
||||||
print(f" files returned: {len(files)}")
|
|
||||||
for f in files:
|
|
||||||
print(f" path={f.get('path')!r} isdir={f.get('isdir')} add={list((f.get('additional') or {}).keys())}")
|
|
||||||
else:
|
|
||||||
print(f" ERROR: {body!r}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# ── Variant 3: JSON array ──────────────────────────────────────────
|
|
||||||
print("─" * 60)
|
|
||||||
print(f"Variant 3 — path=json.dumps([{PATH_A!r}, {PATH_B!r}]) (JSON array)")
|
|
||||||
params_v3 = {**base_params, "path": json.dumps([PATH_A, PATH_B])}
|
|
||||||
resp = await http.get(url, params=params_v3)
|
|
||||||
body = resp.json()
|
|
||||||
print(f" HTTP {resp.status_code} success={body.get('success')}")
|
|
||||||
if body.get("success"):
|
|
||||||
files = body.get("data", {}).get("files", [])
|
|
||||||
print(f" files returned: {len(files)}")
|
|
||||||
for f in files:
|
|
||||||
print(f" path={f.get('path')!r} isdir={f.get('isdir')} add={list((f.get('additional') or {}).keys())}")
|
|
||||||
else:
|
|
||||||
print(f" ERROR: {body!r}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
await auth.logout(client)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
asyncio.run(run())
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fatal: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -369,9 +370,9 @@ async def test_get_info_multiple_paths(config: AppConfig) -> None:
|
|||||||
assert "/data/b.txt" in result
|
assert "/data/b.txt" in result
|
||||||
assert "2 item(s)" in result
|
assert "2 item(s)" in result
|
||||||
|
|
||||||
# Verify the API received both paths as a comma-joined string
|
# Verify the API received paths as a JSON array (confirmed working format)
|
||||||
call_params = client.request.call_args[1]["params"]
|
call_params = client.request.call_args[1]["params"]
|
||||||
assert call_params["path"] == "/dev/a.txt,/data/b.txt"
|
assert call_params["path"] == json.dumps(["/dev/a.txt", "/data/b.txt"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user