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>
This commit is contained in:
@@ -781,6 +781,101 @@ def register_filestation(
|
||||
dest = status.get("dest_folder_path", dest_folder_path)
|
||||
return f"Extracted to: {dest}"
|
||||
|
||||
@mcp.tool()
|
||||
async def dir_size(path: str):
|
||||
"""Get total size, file count and folder count for one or more directories.
|
||||
path: comma-separated share-relative paths."""
|
||||
from mcp_synology_filestation.client import SynologyError
|
||||
|
||||
paths = [p.strip() for p in path.split(",") if p.strip()]
|
||||
if not paths:
|
||||
return "Error: no path provided."
|
||||
|
||||
try:
|
||||
start_data = await client.request(
|
||||
"SYNO.FileStation.DirSize",
|
||||
"start",
|
||||
version=2,
|
||||
params={"path": json.dumps(paths)},
|
||||
)
|
||||
except SynologyError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
taskid: str = start_data.get("taskid", "")
|
||||
if not taskid:
|
||||
return "Error: DSM did not return a task ID."
|
||||
|
||||
ok, result = await _poll_task("SYNO.FileStation.DirSize", 2, taskid)
|
||||
if not ok:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
status: dict[str, Any] = result # type: ignore[assignment]
|
||||
num_dir = status.get("num_dir", 0)
|
||||
num_file = status.get("num_file", 0)
|
||||
total_size = status.get("total_size", 0)
|
||||
|
||||
path_label = ", ".join(paths)
|
||||
w_path = max(len("Path"), len(path_label))
|
||||
num_dir_str = str(num_dir)
|
||||
num_file_str = str(num_file)
|
||||
size_str = _fmt_size(total_size)
|
||||
|
||||
sep = (
|
||||
f"+{'-' * (w_path + 2)}"
|
||||
f"+{'-' * (max(len('Folders'), len(num_dir_str)) + 2)}"
|
||||
f"+{'-' * (max(len('Files'), len(num_file_str)) + 2)}"
|
||||
f"+{'-' * (max(len('Total Size'), len(size_str)) + 2)}+"
|
||||
)
|
||||
w_dir = max(len("Folders"), len(num_dir_str))
|
||||
w_file = max(len("Files"), len(num_file_str))
|
||||
w_size = max(len("Total Size"), len(size_str))
|
||||
|
||||
sep = (
|
||||
f"+{'-' * (w_path + 2)}+{'-' * (w_dir + 2)}+{'-' * (w_file + 2)}+{'-' * (w_size + 2)}+"
|
||||
)
|
||||
header = (
|
||||
f"| {'Path':<{w_path}} "
|
||||
f"| {'Folders':<{w_dir}} "
|
||||
f"| {'Files':<{w_file}} "
|
||||
f"| {'Total Size':<{w_size}} |"
|
||||
)
|
||||
row = (
|
||||
f"| {path_label:<{w_path}} "
|
||||
f"| {num_dir_str:<{w_dir}} "
|
||||
f"| {num_file_str:<{w_file}} "
|
||||
f"| {size_str:<{w_size}} |"
|
||||
)
|
||||
return "\n".join([sep, header, sep, row, sep])
|
||||
|
||||
@mcp.tool()
|
||||
async def get_md5(path: str):
|
||||
"""Compute the MD5 checksum of a file on the NAS. path: share-relative file path."""
|
||||
from mcp_synology_filestation.client import SynologyError
|
||||
|
||||
try:
|
||||
start_data = await client.request(
|
||||
"SYNO.FileStation.MD5",
|
||||
"start",
|
||||
version=2,
|
||||
params={"file_path": json.dumps(path)},
|
||||
)
|
||||
except SynologyError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
taskid: str = start_data.get("taskid", "")
|
||||
if not taskid:
|
||||
return "Error: DSM did not return a task ID."
|
||||
|
||||
ok, result = await _poll_task("SYNO.FileStation.MD5", 2, taskid)
|
||||
if not ok:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
status: dict[str, Any] = result # type: ignore[assignment]
|
||||
md5 = status.get("md5", "")
|
||||
if not md5:
|
||||
return "Error: DSM returned no MD5 hash."
|
||||
return f"MD5 of {path}: {md5}"
|
||||
|
||||
@mcp.tool()
|
||||
async def upload(
|
||||
path: str,
|
||||
|
||||
Reference in New Issue
Block a user