Files
archie/web/app.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

177 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""
Archie 3.5 Web Interface — Flask frontend wrapping cgi-client
"""
import os
import subprocess
import shutil
from flask import Flask, render_template, request
app = Flask(__name__)
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.normpath(os.path.join(_HERE, '..'))
BUILD_DIR = os.environ.get('ARCHIE_BUILD_DIR',
os.path.join(_ROOT, 'build_test'))
ARCHIE_DB = os.environ.get('ARCHIE_DB',
os.path.join(_ROOT, 'build_test', 'db'))
ARCHIE_USER = os.environ.get('ARCH_USER', os.environ.get('USER', 'archie'))
# Cached at startup — avoids filesystem scan on every request
_CGI_CLIENT = None
def _find_cgi_client():
global _CGI_CLIENT
if _CGI_CLIENT is not None:
return _CGI_CLIENT
for subdir in ['archie/clients/cgi', 'bin']:
p = os.path.join(BUILD_DIR, subdir, 'cgi-client')
if os.path.isfile(p):
_CGI_CLIENT = p
return p
found = shutil.which('cgi-client')
if found:
_CGI_CLIENT = found
return found
raise FileNotFoundError('cgi-client not found in BUILD_DIR or PATH')
def _run_query(query, search_type, case_sens, maxhits, database):
"""Run cgi-client and return parsed results dict."""
cgi = _find_cgi_client()
type_map = {
'exact': 'Exact',
'sub': 'Sub String',
'regex': 'Regular Expression',
}
case_map = {
'sensitive': 'Sensitive',
'insensitive': 'Insensitive',
}
db_map = {
'anonftp': 'Anonymous FTP',
'webindex': 'Web Index',
}
stdin_data = (
f"oflag=1\n"
f"query={query}\n"
f"database={db_map.get(database, 'Anonymous FTP')}\n"
f"type={type_map.get(search_type, 'Sub String')}\n"
f"case={case_map.get(case_sens, 'Insensitive')}\n"
f"maxhits={maxhits}\n"
)
env = {**os.environ,
'ARCH_USER': ARCHIE_USER,
'HOME': os.environ.get('HOME', '/tmp')}
try:
proc = subprocess.run(
[cgi, '-M', ARCHIE_DB],
input=stdin_data,
capture_output=True,
text=True,
timeout=30,
env=env,
)
except subprocess.TimeoutExpired:
return {'error': 'Query timed out after 30 seconds.', 'hits': 0, 'results': []}
except FileNotFoundError as e:
return {'error': str(e), 'hits': 0, 'results': []}
return _parse_output(proc.stdout, proc.stderr, proc.returncode)
def _parse_output(stdout, stderr, returncode):
"""Parse cgi-client plain-text output into structured data."""
out = {'hits': 0, 'results': [], 'error': None, 'more': False}
if returncode != 0 and not stdout.strip():
out['error'] = (stderr or 'cgi-client returned non-zero exit').strip()[:400]
return out
current = {}
for line in stdout.splitlines():
if '=' not in line:
continue
key, _, val = line.partition('=')
key = key.strip()
if key == 'HITS':
out['hits'] = int(val) if val.isdigit() else 0
elif key == 'START_RESULT':
current = {'index': val}
elif key == 'END_RESULT':
if current:
out['results'].append(current)
current = {}
elif key == 'more':
out['more'] = val.strip().upper() == 'YES'
elif key in ('URL', 'STRING', 'SITE', 'PATH', 'TYPE',
'SIZE', 'DATE', 'PERMS', 'FTYPE', 'TITLE', 'WEIGHT'):
current[key] = val
elif key == 'ERROR':
out['error'] = val
# flush unclosed result
if current:
out['results'].append(current)
return out
@app.route('/', methods=['GET', 'POST'])
def index():
results = None
query = ''
search_type = 'sub'
case_sens = 'insensitive'
maxhits = 95
database = 'anonftp'
error = None
if request.method == 'POST':
query = request.form.get('query', '').strip().replace('\n', '').replace('\r', '')[:200]
search_type = request.form.get('type', 'sub')
case_sens = request.form.get('case', 'insensitive')
maxhits = min(int(request.form.get('maxhits', 95) or 95), 500)
database = request.form.get('database', 'anonftp')
if query:
data = _run_query(query, search_type, case_sens, maxhits, database)
results = data['results']
error = data.get('error')
hits = data.get('hits', len(results))
more = data.get('more', False)
else:
error = 'Please enter a search term.'
hits = 0
more = False
else:
hits = 0
more = False
return render_template(
'index.html',
query=query,
search_type=search_type,
case_sens=case_sens,
maxhits=maxhits,
database=database,
results=results,
hits=hits,
more=more,
error=error,
)
if __name__ == '__main__':
host = os.environ.get('ARCHIE_WEB_HOST', '0.0.0.0')
port = int(os.environ.get('ARCHIE_WEB_PORT', 5000))
debug = os.environ.get('ARCHIE_WEB_DEBUG', '').lower() in ('1', 'true', 'yes')
app.run(host=host, port=port, debug=debug)