- 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>
281 lines
8.9 KiB
Python
Executable File
281 lines
8.9 KiB
Python
Executable File
#!/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()
|