- 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>
177 lines
5.2 KiB
Python
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)
|