share: dockerize the file-share pipeline and document it

The Python half of the short-lived share links lived in the repo; the Apache
config and the cron entries that make it work were hand-placed on the host, so
the feature could not be rebuilt from a checkout. This adds the missing half.

Scripts (defaults unchanged, so existing bare-metal cron keeps working):
* scan_shares.py / revoke_shares.py take their paths from CONJURER_SHARE_*
  instead of hardcoding the Pi layout, and create their parent dirs;
* the revoke TTL is now CONJURER_SHARE_TTL_SECONDS. Its --help claimed "2min"
  while the code used a hardcoded 3600 - the help text now reports the real,
  configured value.

New share service:
* docker/Dockerfile.share - Apache + the two jobs, reusing the same scripts
  rather than forking copies;
* docker/share-vhost.conf.tpl - the previously undocumented Apache half.
  Three settings are load-bearing and commented as such: +FollowSymLinks (the
  shares ARE symlinks), -Indexes (a listing would expose every live token), and
  a deny rule for dotfiles (revoke_shares.py keeps .downloads.json - a map of
  every live token - inside the served directory);
* docker/entrypoint.share.sh - renders the vhost, seeds the index on first run,
  then runs scanner/revoker in sleep loops beside Apache (no cron, so their
  output shows up in docker logs);
* compose + env example, incl. the two volumes that MUST be shared with the
  musician (it creates the links and reads the index).

docs/deployment/FILE_SHARING.md documents the mechanism, both deployment routes
(docker and existing host Apache), the Discord command, why each Apache setting
matters, verification commands, and the known limitations - notably that a link
nobody ever downloads is never revoked, since the TTL starts at first download.

Verified: scanner and revoker exercised end-to-end against temp dirs (index
built; token recorded from a combined-format log line and the symlink unlinked).
Compose file not validated - no docker on this machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michal Tuszowski
2026-07-19 22:16:49 +02:00
parent dc449dd74c
commit 33ec1c0e35
9 changed files with 437 additions and 10 deletions
+14 -5
View File
@@ -7,9 +7,13 @@ import argparse
import sys
from pathlib import Path
# — CONFIGURATION — adjust as needed
APACHE_LOG = '/var/log/apache2/share_access.log' # <- point to your share-specific log!
SHARE_DIR = Path('/var/www/html/share')
# — CONFIGURATION — env-overridable; defaults = original bare-metal Pi layout
# APACHE_LOG must be the share-specific access log (see docker/share-vhost.conf);
# pointing it at the site-wide log also works but wastes time scanning noise.
APACHE_LOG = os.getenv('CONJURER_APACHE_LOG', '/var/log/apache2/share_access.log')
SHARE_DIR = Path(os.getenv('CONJURER_SHARE_DIR', '/var/www/html/share'))
# Seconds to keep a link alive after its FIRST download (not after creation).
TTL_SECONDS = int(os.getenv('CONJURER_SHARE_TTL_SECONDS', '3600'))
METADATA_FILE = SHARE_DIR / '.downloads.json'
STATE_FILE = SHARE_DIR / '.logpos'
# Regex to match lines like: "GET /share/abcdef1234... HTTP/1.1"
@@ -66,7 +70,7 @@ def record_downloads(lines, metadata, args):
def revoke_old(metadata, args):
now = time.time()
to_remove = [tok for tok, ts in metadata.items() if now - ts >= 3600]
to_remove = [tok for tok, ts in metadata.items() if now - ts >= TTL_SECONDS]
for tok in to_remove:
link = SHARE_DIR / tok
try:
@@ -78,7 +82,12 @@ def revoke_old(metadata, args):
return bool(to_remove)
def main():
p = argparse.ArgumentParser(description="Revoke share links 2min after download")
p = argparse.ArgumentParser(
description=(
"Revoke share links CONJURER_SHARE_TTL_SECONDS "
f"(currently {TTL_SECONDS}s) after their first download"
)
)
p.add_argument('--debug', action='store_true', help='print debug info to stderr')
args = p.parse_args()