"""SFTP файловый менеджер.""" from __future__ import annotations import stat from dataclasses import dataclass from pathlib import PurePosixPath from typing import Callable, List, Optional import asyncssh from app.core.ssh import SSHClient @dataclass class FileEntry: name: str path: str is_dir: bool size: int mtime: float permissions: str def _fmt_perms(mode: int) -> str: return stat.filemode(mode) if mode else "?" async def list_dir(c: SSHClient, path: str) -> List[FileEntry]: async with c.conn.start_sftp_client() as sftp: entries = [] try: attrs = await sftp.readdir(path) except asyncssh.SFTPFailure as e: raise PermissionError(str(e)) for a in attrs: if a.filename in (".", ".."): continue full = str(PurePosixPath(path) / a.filename) is_dir = stat.S_ISDIR(a.attrs.permissions or 0) entries.append( FileEntry( name=a.filename, path=full, is_dir=is_dir, size=a.attrs.size or 0, mtime=a.attrs.mtime or 0, permissions=_fmt_perms(a.attrs.permissions or 0), ) ) entries.sort(key=lambda e: (not e.is_dir, e.name.lower())) return entries async def download(c: SSHClient, remote: str, local: str, progress: Optional[Callable[[int, int], None]] = None) -> None: async with c.conn.start_sftp_client() as sftp: size = (await sftp.stat(remote)).size or 0 done = 0 async with sftp.open(remote, "rb") as rf, open(local, "wb") as lf: while True: chunk = await rf.read(65536) if not chunk: break lf.write(chunk) done += len(chunk) if progress: progress(done, size) async def upload(c: SSHClient, local: str, remote: str, progress: Optional[Callable[[int, int], None]] = None) -> None: import os size = os.path.getsize(local) done = 0 async with c.conn.start_sftp_client() as sftp: async with sftp.open(remote, "wb") as rf, open(local, "rb") as lf: while True: chunk = lf.read(65536) if not chunk: break await rf.write(chunk) done += len(chunk) if progress: progress(done, size) async def delete(c: SSHClient, path: str, is_dir: bool) -> None: async with c.conn.start_sftp_client() as sftp: if is_dir: await c.run(f"rm -rf {path!r}") else: await sftp.remove(path) async def mkdir(c: SSHClient, path: str) -> None: async with c.conn.start_sftp_client() as sftp: await sftp.makedirs(path, exist_ok=True) async def rename(c: SSHClient, old: str, new: str) -> None: async with c.conn.start_sftp_client() as sftp: await sftp.rename(old, new) async def read_text(c: SSHClient, path: str, limit: int = 500_000) -> str: async with c.conn.start_sftp_client() as sftp: async with sftp.open(path, "rb") as f: data = await f.read(limit) return data.decode("utf-8", errors="replace") async def write_text(c: SSHClient, path: str, content: str, use_sudo: bool = False) -> None: if use_sudo: # запись в root-owned файл через sudo tee import base64 b64 = base64.b64encode(content.encode()).decode() r = await c.sudo(f"echo {b64} | base64 -d > {path!r}") if not r.ok: raise PermissionError(r.stderr) else: async with c.conn.start_sftp_client() as sftp: async with sftp.open(path, "wb") as f: await f.write(content.encode())