#!/usr/bin/env python3 """Deploy public/, config/, and .env to wint.global / Plesk via FTP/FTPS.""" from __future__ import annotations import argparse import ftplib import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent def load_env(path: Path) -> dict[str, str]: if not path.is_file(): sys.exit(f"Missing {path} — copy .env.example to .env and fill in credentials.") env: dict[str, str] = {} for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#"): continue key, _, value = line.partition("=") env[key.strip()] = value.strip() return env def require_env(env: dict[str, str], *keys: str) -> None: missing = [k for k in keys if not env.get(k)] if missing: sys.exit(f"Missing in .env: {', '.join(missing)}") def connect(env: dict[str, str]) -> ftplib.FTP: host = env["FTP_HOST"] user = env["FTP_USER"] password = env["FTP_PASSWORD"] try: ftp = ftplib.FTP_TLS(host, timeout=60) ftp.login(user, password) ftp.prot_p() print("Connected (FTPS)") return ftp except ftplib.all_errors as exc: print(f"FTPS failed ({exc}), trying plain FTP …") ftp = ftplib.FTP(host, timeout=60) ftp.login(user, password) print("Connected (FTP)") return ftp def cwd_to_root(ftp: ftplib.FTP) -> None: try: ftp.cwd("/") except ftplib.error_perm: pass def ensure_remote_dir(ftp: ftplib.FTP, remote_dir: str) -> None: cwd_to_root(ftp) for part in [p for p in remote_dir.split("/") if p]: try: ftp.cwd(part) except ftplib.error_perm: ftp.mkd(part) ftp.cwd(part) def upload_file(ftp: ftplib.FTP, local: Path, remote_path: str) -> None: remote_path = remote_path.replace("\\", "/").lstrip("/") remote_dir = os.path.dirname(remote_path) remote_name = os.path.basename(remote_path) if remote_dir: ensure_remote_dir(ftp, remote_dir) else: cwd_to_root(ftp) with local.open("rb") as handle: ftp.storbinary(f"STOR {remote_name}", handle) print(f" ↑ {local.relative_to(ROOT)} → {remote_path}") def upload_tree(ftp: ftplib.FTP, local_dir: Path, remote_base: str) -> None: remote_base = remote_base.strip("/") for path in sorted(local_dir.rglob("*")): if path.is_file(): rel = path.relative_to(local_dir).as_posix() upload_file(ftp, path, f"{remote_base}/{rel}") def main() -> None: parser = argparse.ArgumentParser(description="Deploy 0012.de webspace") parser.add_argument("--dry-run", action="store_true", help="Show plan only") args = parser.parse_args() env = load_env(ROOT / ".env") require_env(env, "FTP_HOST", "FTP_USER", "FTP_PASSWORD") remote_web = env.get("FTP_REMOTE_PATH", "httpdocs").strip("/") remote_private = env.get("FTP_PRIVATE_PATH", "private").strip("/") print("Deploy plan:") public_files = sum(1 for p in (ROOT / "public").rglob("*") if p.is_file()) config_files = sum(1 for p in (ROOT / "config").rglob("*") if p.is_file()) print(f" public/ ({public_files} files) → {remote_web}/") print(f" config/ ({config_files} files) → {remote_private}/config/") print(f" .env → {remote_private}/.env") print(f" (mkdir) {remote_private}/data/") if args.dry_run: return print("\nConnecting …") ftp = connect(env) try: print("Uploading …") upload_tree(ftp, ROOT / "public", remote_web) upload_tree(ftp, ROOT / "config", f"{remote_private}/config") ensure_remote_dir(ftp, f"{remote_private}/data") upload_file(ftp, ROOT / ".env", f"{remote_private}/.env") print("\nDone.") finally: ftp.quit() if __name__ == "__main__": main()