Files
0012/scripts/deploy.py
T
Frank Schwenk 01de7ed4fb feat: SMTP mail stack and separate server.env deploy
Add mail.php with STARTTLS sending, test scripts, server.env split from
local .env, admin@ postbox docs, DKIM DNS support-ticket notes, and todo 11
for boka relay.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 23:19:03 +02:00

158 lines
4.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Deploy public/, config/, and server.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 delete_remote_file(ftp: ftplib.FTP, remote_path: str) -> None:
remote_path = remote_path.replace("\\", "/").lstrip("/")
remote_dir = os.path.dirname(remote_path)
remote_name = os.path.basename(remote_path)
try:
if remote_dir:
ensure_remote_dir(ftp, remote_dir)
else:
cwd_to_root(ftp)
ftp.delete(remote_name)
print(f" ✕ deleted {remote_path}")
except ftplib.error_perm as exc:
print(f" (skip delete {remote_path}: {exc})")
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")
server_env = ROOT / "server.env"
if not server_env.is_file():
sys.exit(
"Missing server.env — run: python3 scripts/sync-server-env.py\n"
"(or copy server.env.example to server.env and fill in server values)"
)
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" server.env → {remote_private}/server.env")
print(f" (remove legacy {remote_private}/.env if present)")
print(f" (mkdir) {remote_private}/data/")
print(" .env stays local — not deployed")
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, server_env, f"{remote_private}/server.env")
delete_remote_file(ftp, f"{remote_private}/.env")
print("\nDone.")
finally:
ftp.quit()
if __name__ == "__main__":
main()