"""SFTP push/pull of local app folders. Never delete remote extras.""" from __future__ import annotations from collections.abc import Callable from pathlib import Path from gpu_rent.config import Config from gpu_rent.payload import has_payload, iter_payload_files, model_push_set, sha256_file from gpu_rent.ssh_ops import ( get_file_on, open_ssh, put_file_on, remote_sha256_on, run_ssh_on, ) Log = Callable[[str], None] def push_tree( cfg: Config, host: str, local_root: Path, remote_root: str, log: Log, *, models: bool, ) -> int: if not has_payload(local_root): log(f"push {local_root.name}: пусто — skip") return 0 files = model_push_set(local_root) if models else iter_payload_files(local_root) file_list = list(files) total = len(file_list) sent = 0 skipped = 0 client = open_ssh(cfg, host) try: for i, path in enumerate(file_list, start=1): rel = path.relative_to(local_root).as_posix() remote = f"{remote_root.rstrip('/')}/{rel}" local_hash = sha256_file(path) remote_hash = remote_sha256_on(client, remote) if remote_hash and remote_hash.lower() == local_hash.lower(): skipped += 1 continue log(f"push [{i}/{total}] {rel}") put_file_on(client, path, remote) sent += 1 finally: client.close() if sent == 0: log(f"push {local_root.name}: всё уже на VM ({total} файл(ов), skip={skipped})") else: log(f"push {local_root.name}: {sent}/{total} отправлено (skip={skipped})") return sent def pull_tree(cfg: Config, host: str, remote_root: str, local_root: Path, log: Log) -> int: client = open_ssh(cfg, host) try: listing = run_ssh_on( client, f"find {remote_root} -type f 2>/dev/null | sed 's|^{remote_root}/||'", check=False, timeout=120, ) names = [line.strip() for line in listing.splitlines() if line.strip()] work = [ rel for rel in names if not ( rel.endswith("/.gitkeep") or rel.rsplit("/", 1)[-1] in {".gitkeep", "README.md"} ) ] total = len(work) pulled = 0 skipped = 0 for i, rel in enumerate(work, start=1): remote = f"{remote_root.rstrip('/')}/{rel}" local = local_root / rel remote_hash = remote_sha256_on(client, remote) if local.is_file() and remote_hash and sha256_file(local).lower() == remote_hash.lower(): skipped += 1 continue log(f"pull [{i}/{total}] {rel}") get_file_on(client, remote, local) pulled += 1 finally: client.close() if pulled: log(f"pull Output: {pulled}/{total} (skip={skipped})") else: log(f"pull Output: нечего забирать ({total} файл(ов), skip={skipped})") return pulled