"""Recovery CLI. Everyday password changes happen in the UI. This exists for the one case the UI cannot help with — a forgotten password — and for the initial handover. docker exec -it parts python -m app.admin set-password docker exec parts python -m app.admin show-status docker exec parts python -m app.admin prune-images """ import getpass import sys from . import auth, db def set_password(argv): password = argv[0] if argv else None if password is None: password = getpass.getpass("New password: ") if password != getpass.getpass("Repeat: "): print("Passwords did not match.", file=sys.stderr) return 1 problem = auth.password_problem(password) if problem: print(problem, file=sys.stderr) return 1 with db.session() as conn: auth.set_password(conn, password) print("Password set. All existing sessions have been signed out.") return 0 def clear_password(_argv): """Fall back to the PARTS_PASSWORD environment variable again.""" with db.session() as conn: conn.execute("DELETE FROM settings WHERE key = ?", (auth.PASSWORD_KEY,)) auth.bump_epoch(conn) print("Stored password cleared; PARTS_PASSWORD from the environment is live again.") print("All existing sessions have been signed out.") return 0 def prune_images(_argv): """Delete image files with no row pointing at them. Uploads write the file before the row, so an ill-timed crash can strand one. Nothing else creates orphans; this is a sweeper, not a routine step. """ import os with db.session() as conn: known = { os.path.basename(db.image_path(r["token"], r["mime"])) for r in conn.execute("SELECT token, mime FROM part_images").fetchall() } removed = 0 for name in os.listdir(db.IMAGE_DIR): if name not in known: os.remove(os.path.join(db.IMAGE_DIR, name)) removed += 1 print(f"{removed} orphaned image file(s) removed; {len(known)} kept.") return 0 def show_status(_argv): import os with db.session() as conn: stored = auth.stored_hash(conn) images = conn.execute("SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b " "FROM part_images").fetchone() print("password source :", "database (set in the app)" if stored else "PARTS_PASSWORD env var") print("session epoch :", auth.current_epoch(conn)) print("auth enabled :", auth.auth_enabled()) print("images :", f"{images['n']} rows, {images['b'] / 1024 / 1024:.1f} MB", f"({len(os.listdir(db.IMAGE_DIR))} files in {db.IMAGE_DIR})") return 0 COMMANDS = {"set-password": set_password, "clear-password": clear_password, "show-status": show_status, "prune-images": prune_images} def main(argv=None): argv = list(sys.argv[1:] if argv is None else argv) if not argv or argv[0] not in COMMANDS: print("usage: python -m app.admin {" + "|".join(COMMANDS) + "}", file=sys.stderr) return 2 db.init() return COMMANDS[argv[0]](argv[1:]) if __name__ == "__main__": raise SystemExit(main())