Files
archie/archie_init.py
Mario Fetka 1e4baef047 Port Archie 3.5 to Linux/CMake, add Debian packaging and CI
- Replace autoconf/make build system with CMake (installs to /opt/archie)
- Add CPack DEB packaging for Debian Trixie (non-free/net, postinst creates
  archie user, extracts DB skeleton, sets setuid bits, enables systemd units)
- Add Gitea Actions workflow building .deb + binary/source tarballs on tag push
- Add portable archie_init.py for non-Debian post-install setup
- Port all scripts to Linux: getent passwd, systemctl, tail -n +N, gzip
- Add SFTP (libssh2) and FTPS (OpenSSL) scrapers alongside anonftp
- Add Flask web frontend (archie-web.service)
- Fix filter scripts (exec cat replaces broken sed s///g)
- Update all manpages: paths, contacts, add SFTP/FTPS section
- Update etc/: enable gzip, add webindex catalog, fix localhost refs
- Remove: AIX-2/SunOS-4.1.4/SunOS-5.4 dirs, tcl7.6/, tcl-dp/, tk4.2/,
  berkdb/, old Makefile.in/pre/post fragments, build.sh, unwrap scripts
- Add .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 23:05:12 +02:00

281 lines
8.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
archie_init.py — portable post-install initializer for Archie 3.5
Performs all the steps that the Debian postinst does, but works on any
Linux (or Unix-like) system where systemd + useradd/groupadd are available.
Safe to run multiple times (idempotent).
Usage (as root):
python3 archie_init.py [--prefix /opt/archie] [--no-systemd] [--dry-run]
"""
import argparse
import grp
import os
import pwd
import shutil
import subprocess
import sys
import tarfile
ARCHIE_USER = "archie"
ARCHIE_GROUP = "archie"
SYSTEMD_SYSTEM = "/lib/systemd/system"
UNITS = [
"archie-dirsrv.service",
"archie-arserver.socket",
"archie-arcontrol.timer",
"archie-web.service",
]
# ── helpers ────────────────────────────────────────────────────────────────
def run(cmd, dry_run=False, check=True):
print(f" + {' '.join(cmd)}")
if dry_run:
return
subprocess.run(cmd, check=check)
def ensure_dir(path, dry_run=False):
if not os.path.exists(path):
print(f" mkdir {path}")
if not dry_run:
os.makedirs(path, exist_ok=True)
def ensure_symlink(src, dst, dry_run=False):
if os.path.lexists(dst):
return
print(f" ln -sf {src} {dst}")
if not dry_run:
os.symlink(src, dst)
def chown(path, uid, gid, recursive=False, dry_run=False):
print(f" chown {'R ' if recursive else ''}{uid}:{gid} {path}")
if dry_run:
return
if recursive:
for root, dirs, files in os.walk(path):
os.lchown(root, uid, gid)
for f in files:
os.lchown(os.path.join(root, f), uid, gid)
else:
os.lchown(path, uid, gid)
def chmod(path, mode, dry_run=False):
print(f" chmod {oct(mode)} {path}")
if not dry_run:
os.chmod(path, mode)
# ── steps ─────────────────────────────────────────────────────────────────
def step_user_group(dry_run):
print("\n[1] Creating system user/group ...")
try:
grp.getgrnam(ARCHIE_GROUP)
print(f" group '{ARCHIE_GROUP}' already exists")
except KeyError:
run(["groupadd", "--system", ARCHIE_GROUP], dry_run)
try:
pwd.getpwnam(ARCHIE_USER)
print(f" user '{ARCHIE_USER}' already exists")
except KeyError:
run([
"useradd",
"--system",
"--home-dir", args.prefix,
"--no-create-home",
"--gid", ARCHIE_GROUP,
"--shell", "/usr/sbin/nologin",
"--comment", "Archie FTP index server",
ARCHIE_USER,
], dry_run)
def step_directories(prefix, dry_run):
print("\n[2] Creating runtime directories ...")
for d in [
"db", "db/host_db", "logs", "tmp", "incoming",
"anonftp", "locks", "etc/ssl",
"pfs", "pfs/shadow", "pfs/pfsdat", "pfs/info-tree", "pfs/history",
]:
ensure_dir(os.path.join(prefix, d), dry_run)
for lf in ["pfs/pfs.log", "logs/archie.log", "logs/email.log"]:
p = os.path.join(prefix, lf)
if not os.path.exists(p):
print(f" touch {p}")
if not dry_run:
open(p, "w").close()
def step_symlinks(prefix, dry_run):
print("\n[3] Creating binary symlinks ...")
bindir = os.path.join(prefix, "bin")
ensure_symlink("telnet-client", os.path.join(bindir, "-telnet-client"), dry_run)
ensure_symlink("arserver", os.path.join(bindir, "arexchange"), dry_run)
ensure_symlink("arserver", os.path.join(bindir, "arretrieve"), dry_run)
ensure_symlink("update_anonftp", os.path.join(bindir, "update_webindex"), dry_run)
print("\n /pfs → Prospero shadow filesystem ...")
ensure_symlink(os.path.join(prefix, "pfs"), "/pfs", dry_run)
def step_database(prefix, dry_run):
print("\n[4] Extracting initial database skeleton ...")
marker = os.path.join(prefix, "db", "host_db", "host-db.dir")
if os.path.exists(marker):
print(" database already present — skipping")
return
db_tar = os.path.join(prefix, "tmp", "db.tar.init")
if not os.path.isfile(db_tar):
print(f" WARNING: {db_tar} not found — skipping DB init")
return
print(f" tar xf {db_tar} -C {prefix}")
if not dry_run:
with tarfile.open(db_tar) as tf:
tf.extractall(path=prefix)
def step_permissions(prefix, dry_run):
print("\n[5] Setting ownership and permissions ...")
try:
pw = pwd.getpwnam(ARCHIE_USER)
uid, gid = pw.pw_uid, pw.pw_gid
except KeyError:
uid, gid = -1, -1
print(f" WARNING: user '{ARCHIE_USER}' not found — skipping chown")
if uid != -1:
chown(prefix, uid, gid, recursive=True, dry_run=dry_run)
chmod(prefix, 0o750, dry_run)
bindir = os.path.join(prefix, "bin")
# telnet-client: setuid root to bind privileged ports
tc = os.path.join(bindir, "telnet-client")
if os.path.exists(tc):
chown(tc, 0, 0, dry_run=dry_run)
chmod(tc, 0o4111, dry_run)
# pstart: setuid+setgid root so any user can restart dirsrv
ps = os.path.join(bindir, "pstart")
if os.path.exists(ps):
chown(ps, 0, 0, dry_run=dry_run)
chmod(ps, 0o6111, dry_run)
# cgi-client: setuid archie so the web server can access the DB
cgi = os.path.join(prefix, "cgi", "bin", "cgi-client")
if os.path.exists(cgi) and uid != -1:
chown(cgi, uid, gid, dry_run=dry_run)
chmod(cgi, 0o4755, dry_run)
# log / tmp permissions
email_log = os.path.join(prefix, "logs", "email.log")
if os.path.exists(email_log):
chmod(email_log, 0o662, dry_run)
tmp = os.path.join(prefix, "tmp")
if os.path.exists(tmp):
chmod(tmp, 0o1777, dry_run)
db_tmp = os.path.join(prefix, "db", "tmp")
if os.path.exists(db_tmp):
chmod(db_tmp, 0o1777, dry_run)
db = os.path.join(prefix, "db")
if os.path.exists(db):
cur = os.stat(db).st_mode
chmod(db, cur | 0o005, dry_run) # o+rx
def step_systemd(prefix, dry_run):
print("\n[6] Installing systemd units ...")
unit_src = os.path.join(prefix, "lib", "systemd", "system")
if not os.path.isdir(unit_src):
print(f" {unit_src} not found — skipping systemd setup")
return
if not os.path.isdir(SYSTEMD_SYSTEM):
print(f" {SYSTEMD_SYSTEM} not found — is systemd running?")
return
for unit in UNITS:
src = os.path.join(unit_src, unit)
dst = os.path.join(SYSTEMD_SYSTEM, unit)
if os.path.isfile(src):
ensure_symlink(src, dst, dry_run)
else:
print(f" WARNING: {src} not found")
if os.path.isdir("/run/systemd/system"):
run(["systemctl", "daemon-reload"], dry_run, check=False)
for unit in UNITS:
run(["systemctl", "enable", unit], dry_run, check=False)
else:
print(" systemd not running — units installed but not enabled")
# ── main ──────────────────────────────────────────────────────────────────
def main():
global args
parser = argparse.ArgumentParser(
description="Archie 3.5 portable post-install initializer"
)
parser.add_argument(
"--prefix", default="/opt/archie",
help="Archie installation prefix (default: /opt/archie)"
)
parser.add_argument(
"--no-systemd", action="store_true",
help="Skip systemd unit installation"
)
parser.add_argument(
"--dry-run", action="store_true",
help="Print actions without executing them"
)
args = parser.parse_args()
if os.geteuid() != 0:
print("ERROR: This script must be run as root.", file=sys.stderr)
sys.exit(1)
if not os.path.isdir(args.prefix):
print(f"ERROR: prefix '{args.prefix}' does not exist.", file=sys.stderr)
print(" Run 'cmake --install <builddir>' first.", file=sys.stderr)
sys.exit(1)
print(f"Archie 3.5 init — prefix: {args.prefix}"
+ (" [DRY RUN]" if args.dry_run else ""))
step_user_group(args.dry_run)
step_directories(args.prefix, args.dry_run)
step_symlinks(args.prefix, args.dry_run)
step_database(args.prefix, args.dry_run)
step_permissions(args.prefix, args.dry_run)
if not args.no_systemd:
step_systemd(args.prefix, args.dry_run)
print("\nDone.")
if not args.dry_run:
print(f"\nStart archie with:\n"
f" systemctl start archie-dirsrv.service\n"
f" systemctl start archie-arserver.socket\n"
f" systemctl start archie-arcontrol.timer\n"
f" systemctl start archie-web.service")
if __name__ == "__main__":
main()