feat: bootstrap webspace deploy and status-beacon landing page
Add public/index.php, private config bootstrap, FTPS deploy script, and complete todo/01 including Let's Encrypt, HTTPS redirect, and HSTS docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Binary file not shown.
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/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()
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec python3 "$(dirname "$0")/deploy.py" "$@"
|
||||
Reference in New Issue
Block a user