Files
manage/backend/src/media_library_viewer_api/clients/local.py
T
2026-05-07 12:34:16 +02:00

97 lines
3.1 KiB
Python

"""Local command execution helpers.
These mirror the remote SSH helpers but execute commands on the API host
itself. They are used for the built-in local monitoring machine.
"""
from __future__ import annotations
import json
import logging
import posixpath
import shlex
import subprocess
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass
class CommandResult:
"""Plain result object returned by local command execution."""
command: str
exit_status: int
stdout: str
stderr: str
class LocalCommandClient:
"""Execute the same POSIX shell snippets used by the SSH client locally."""
def __init__(self, timeout: int = 20):
self.timeout = timeout
def run(self, command: str, timeout: int | None = None) -> CommandResult:
shell_command = ["/bin/sh", "-c", command]
logger.debug("Local run timeout=%s command=%s", timeout or self.timeout, command)
proc = subprocess.run(
shell_command,
capture_output=True,
text=True,
timeout=timeout or self.timeout,
)
result = CommandResult(
command=command,
exit_status=proc.returncode,
stdout=proc.stdout,
stderr=proc.stderr,
)
if result.exit_status == 0:
logger.debug("Local command ok exit_status=%s", result.exit_status)
else:
logger.warning(
"Local command failed exit_status=%s stderr=%s",
result.exit_status,
result.stderr.strip() or result.stdout.strip(),
)
return result
def list_dir(self, path: str) -> CommandResult:
quoted = shlex.quote(path)
not_dir_message = shlex.quote(f"Not a directory: {path}")
command = (
f"test -d {quoted} || "
f"{{ echo {not_dir_message} >&2; exit 20; }}; "
f"find {quoted} -maxdepth 1 -mindepth 1 -printf "
"'%y\\t%s\\t%T@\\t%f\\0' | python3 -c "
+ shlex.quote(
"import sys,json; data=sys.stdin.buffer.read().split(b'\\0'); "
"rows=[]\n"
"for row in data:\n"
" if not row: continue\n"
" t,s,m,n=row.decode('utf-8','replace').split('\\t',3)\n"
" rows.append({'type':t,'size':int(s),'mtime':float(m),'name':n})\n"
"print(json.dumps(rows))"
)
)
return self.run(command)
def stat_path(self, path: str) -> CommandResult:
quoted = shlex.quote(path)
return self.run(f"stat --printf='%F\\n%s bytes\\n%y\\n%n\\n' {quoted}")
def ffprobe_json(self, path: str) -> dict[str, object]:
quoted = shlex.quote(path)
result = self.run(
"ffprobe -v error -show_format -show_streams -print_format json " + quoted,
timeout=60,
)
if result.exit_status != 0:
raise RuntimeError(result.stderr or result.stdout or "ffprobe failed")
return json.loads(result.stdout)
@staticmethod
def join(parent: str, child: str) -> str:
return posixpath.normpath(posixpath.join(parent, child))