from __future__ import annotations import os from pathlib import Path import paramiko def _sftp_client() -> tuple[paramiko.SFTPClient, paramiko.Transport]: host = (os.environ.get("SFTP_HOST") or "").strip() user = (os.environ.get("SFTP_USER") or "").strip() port = int((os.environ.get("SFTP_PORT") or "22").strip() or "22") password = os.environ.get("SFTP_PASS") if not host or not user: raise RuntimeError("Missing SFTP_HOST or SFTP_USER") if not password: raise RuntimeError("Missing SFTP_PASS") t = paramiko.Transport((host, port)) # force password-only auth t.connect(username=user, password=password) return paramiko.SFTPClient.from_transport(t), t def _mkdir_p(sftp: paramiko.SFTPClient, remote_dir: str) -> None: parts = [p for p in remote_dir.strip("/").split("/") if p] cur = "" for part in parts: cur += "/" + part try: sftp.stat(cur) except FileNotFoundError: sftp.mkdir(cur) def store_to_sftp( pdf_id: str, template_id: str, cfg_json_bytes: bytes, pdf_bytes: bytes, pdf_name: str, ) -> str: """ Remote layout: /// trainer_config___.json .pdf> """ base = (os.environ.get("SFTP_BASE_DIR") or "/").strip() if not base.startswith("/"): base = "/" + base base = base.rstrip("/") or "/" sftp, transport = _sftp_client() try: remote_dir = f"{base}/{template_id}/{pdf_id}".replace("//", "/") _mkdir_p(sftp, remote_dir) remote_cfg = f"{remote_dir}/trainer_config_{pdf_id}__{template_id}.json" remote_pdf_name = (pdf_name or f"{pdf_id}.pdf").lstrip("/") remote_pdf = f"{remote_dir}/{remote_pdf_name}" with sftp.open(remote_cfg, "wb") as f: f.write(cfg_json_bytes) with sftp.open(remote_pdf, "wb") as f: f.write(pdf_bytes) return remote_dir finally: try: sftp.close() finally: transport.close()