From 95cd4760b7fbf704a2d1ecc66f5c0faa59d17da9 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Sat, 26 Apr 2025 13:18:02 +0200 Subject: [PATCH] Generated scripts --- conjurer_musician/revoke_shares.py | 99 +++++++++++++++++++++++++++ conjurer_musician/scan_shares.py | 44 ++++++++++++ conjurer_musician/scan_shares.service | 6 ++ conjurer_musician/scan_shares.timer | 11 +++ conjurer_musician/search_and_share.py | 93 +++++++++++++++++++++++++ 5 files changed, 253 insertions(+) create mode 100755 conjurer_musician/revoke_shares.py create mode 100755 conjurer_musician/scan_shares.py create mode 100644 conjurer_musician/scan_shares.service create mode 100644 conjurer_musician/scan_shares.timer create mode 100755 conjurer_musician/search_and_share.py diff --git a/conjurer_musician/revoke_shares.py b/conjurer_musician/revoke_shares.py new file mode 100755 index 0000000..c68822b --- /dev/null +++ b/conjurer_musician/revoke_shares.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import os +import re +import time +import json +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') +METADATA_FILE = SHARE_DIR / '.downloads.json' +STATE_FILE = SHARE_DIR / '.logpos' +# Regex to match lines like: "GET /share/abcdef1234... HTTP/1.1" +TOKEN_REGEX = re.compile(r'"\s*GET\s+/share/([0-9a-f]{32})\s+HTTP/') + +def debug(msg, args): + if args.debug: + print(f"[DEBUG] {msg}", file=sys.stderr) + +def load_metadata(args): + if METADATA_FILE.exists(): + data = json.loads(METADATA_FILE.read_text()) + debug(f"Loaded metadata: {data}", args) + return data + return {} + +def save_metadata(md, args): + METADATA_FILE.write_text(json.dumps(md)) + debug(f"Saved metadata: {md}", args) + +def load_state(args): + if STATE_FILE.exists(): + pos = int(STATE_FILE.read_text()) + debug(f"Loaded last log position: {pos}", args) + return pos + return 0 + +def save_state(pos, args): + STATE_FILE.write_text(str(pos)) + debug(f"Saved new log position: {pos}", args) + +def scan_log(since_pos, args): + try: + with open(APACHE_LOG, 'r') as f: + f.seek(since_pos) + lines = f.readlines() + newpos = f.tell() + debug(f"Read {len(lines)} new lines", args) + return lines, newpos + except Exception as e: + print(f"Error reading log: {e}", file=sys.stderr) + return [], since_pos + +def record_downloads(lines, metadata, args): + now = time.time() + for line in lines: + m = TOKEN_REGEX.search(line) + if m: + tok = m.group(1) + if tok not in metadata: + metadata[tok] = now + debug(f"Recorded download of token {tok} at {now}", args) + return metadata + +def revoke_old(metadata, args): + now = time.time() + to_remove = [tok for tok, ts in metadata.items() if now - ts >= 120] + for tok in to_remove: + link = SHARE_DIR / tok + try: + link.unlink() + debug(f"Unlinked token {tok}", args) + except FileNotFoundError: + debug(f"Link {link} not found when revoking", args) + metadata.pop(tok, None) + return bool(to_remove) + +def main(): + p = argparse.ArgumentParser(description="Revoke share links 2min after download") + p.add_argument('--debug', action='store_true', help='print debug info to stderr') + args = p.parse_args() + + SHARE_DIR.mkdir(parents=True, exist_ok=True) + metadata = load_metadata(args) + state = load_state(args) + + lines, newpos = scan_log(state, args) + metadata = record_downloads(lines, metadata, args) + save_metadata(metadata, args) # always persist new downloads + + if revoke_old(metadata, args): + save_metadata(metadata, args) # persist removals + + save_state(newpos, args) + +if __name__ == '__main__': + main() diff --git a/conjurer_musician/scan_shares.py b/conjurer_musician/scan_shares.py new file mode 100755 index 0000000..dd78636 --- /dev/null +++ b/conjurer_musician/scan_shares.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import os +import json +import time +from pathlib import Path + +# Configuration +SCAN_PATH = '/mnt/shares' +OUTPUT_FILE = '/var/log/share_scan.json' + +def scan_directory(root): + """Recursively walk `root` and collect metadata.""" + entries = [] + for dirpath, dirs, files in os.walk(root): + for name in dirs + files: + full = os.path.join(dirpath, name) + try: + stat = os.stat(full) + entries.append({ + 'path': full, + 'is_dir': os.path.isdir(full), + 'size_bytes': stat.st_size, + 'mtime': time.strftime('%Y-%m-%dT%H:%M:%S%z', + time.localtime(stat.st_mtime)), + }) + except Exception as e: + # skip items we can't stat + continue + return entries + +def main(): + data = { + 'scanned_at': time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime()), + 'root': SCAN_PATH, + 'entries': scan_directory(SCAN_PATH), + } + # Write atomically + temp = OUTPUT_FILE + '.tmp' + with open(temp, 'w') as f: + json.dump(data, f, indent=2) + os.replace(temp, OUTPUT_FILE) + +if __name__ == '__main__': + main() diff --git a/conjurer_musician/scan_shares.service b/conjurer_musician/scan_shares.service new file mode 100644 index 0000000..861c100 --- /dev/null +++ b/conjurer_musician/scan_shares.service @@ -0,0 +1,6 @@ +[Unit] +Description=One‑shot scan of /mnt/shares → JSON + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/scan_shares.py diff --git a/conjurer_musician/scan_shares.timer b/conjurer_musician/scan_shares.timer new file mode 100644 index 0000000..3196c32 --- /dev/null +++ b/conjurer_musician/scan_shares.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Hourly timer for share‑scanner + +[Timer] +# delay 2min after boot, then every hour +OnBootSec=20min +OnUnitActiveSec=24h +Unit=scan_shares.service + +[Install] +WantedBy=timers.target diff --git a/conjurer_musician/search_and_share.py b/conjurer_musician/search_and_share.py new file mode 100755 index 0000000..c41160d --- /dev/null +++ b/conjurer_musician/search_and_share.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import uuid +from pathlib import Path + +# CONFIGURATION +JSON_DB = '/var/log/share_scan.json' +SHARE_DIR = Path('/var/www/html/share') +BASE_URL = 'https://czernobog.pl/share' + +# Ensure share directory exists +SHARE_DIR.mkdir(parents=True, exist_ok=True) + +def load_db(): + with open(JSON_DB) as f: + return json.load(f)['entries'] + +def relevancy(path, keywords): + score = 0 + low = path.lower() + for kw in keywords: + if kw.lower() in low: + score += low.count(kw.lower()) + return score + +def find_matches(entries, keywords): + scored = [] + for e in entries: + score = relevancy(e['path'], keywords) + if score > 0: + scored.append((score, e['path'])) + scored.sort(reverse=True, key=lambda x: x[0]) + return [p for _, p in scored] + +def interactive_select(candidates): + print("Search results:") + for idx, p in enumerate(candidates, 1): + print(f" {idx:3d}. {p}") + sel = input("\nSelect files (e.g. 1,3-5): ").strip() + nums = set() + for part in sel.split(','): + if '-' in part: + a,b = part.split('-',1) + nums.update(range(int(a), int(b)+1)) + else: + nums.add(int(part)) + return [candidates[i-1] for i in sorted(nums) if 1 <= i <= len(candidates)] + +def publish(paths): + urls = [] + for path in paths: + token = uuid.uuid4().hex + link = SHARE_DIR / token + try: + os.symlink(path, link) + except FileExistsError: + pass + urls.append(f"{BASE_URL}/{token}") + return urls + +def main(): + parser = argparse.ArgumentParser(description="Search and share files from JSON DB") + parser.add_argument('keywords', nargs='*', + help='Search keywords (interactive if omitted)') + parser.add_argument('-n','--count', type=int, default=10, + help='How many top results to share') + args = parser.parse_args() + + if not args.keywords: + # Interactive: ask for query + q = input("Enter search keywords (space-separated): ").strip().split() + args.keywords = q + + entries = load_db() + matches = find_matches(entries, args.keywords) + top_n = matches[:args.count] + + if sys.stdin.isatty(): + # interactive selection + sel = interactive_select(top_n) + else: + # non-interactive: share all top_n + sel = top_n + + urls = publish(sel) + for u in urls: + print(u) + +if __name__=='__main__': + main()