Expire stored source photos after 7 days, keeping grades forever

Storing originals for Regrade grew the database ~16MB per grade at the
upload cap. Photos now expire on a configurable window (default 7 days,
counted from last grade so regrading resets it) and the file is VACUUMed so
the space is actually returned — clearing the column alone only moves pages
to the freelist, which would have made the whole feature a no-op on disk.
Grades, thumbnails and measurements are never pruned; a pruned card's
Regrade falls back to the existing re-pick path, now with a banner saying
why.
This commit is contained in:
Barely Removable 2026-08-22 13:07:09 -07:00
parent 633cb98462
commit 602bce8ce3
5 changed files with 127 additions and 0 deletions

35
app.py
View file

@ -21,6 +21,7 @@ import io
import json
import os
import sys
import threading
import time
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@ -43,6 +44,11 @@ MAX_UPLOAD_BYTES = 12 * 1024 * 1024
# ~4/3 that as base64, plus JSON overhead. Kept in step with nginx's
# client_max_body_size in nginx/card-grader.conf.
MAX_BODY_BYTES = 20 * 1024 * 1024
# How often to expire stored source photos (see store.prune_source_images).
# Six-hourly rather than daily so a long-running container doesn't hold a
# week's worth of extra images for up to another day past the window, and
# so the first sweep after a restart isn't the only one that ever runs.
PRUNE_INTERVAL_SECONDS = 6 * 60 * 60
# More angles help — front, back, corner close-ups — but past a handful the
# extra photos cost tokens without adding evidence.
MAX_GRADE_IMAGES = 6
@ -490,6 +496,26 @@ class Server(ThreadingHTTPServer):
daemon_threads = True
def _prune_forever():
"""Expire stored source photos on a timer, for the life of the process.
A daemon thread rather than a cron entry so the retention window holds
on any host this runs on, without a second thing to install and keep in
step. Never lets an exception end the loop: a failed sweep should cost
one cycle, not disable pruning until the next restart.
"""
while True:
try:
pruned = store.prune_source_images()
if pruned:
print("[prune] dropped stored photos from {} grade(s) older "
"than {} days".format(pruned, store.SOURCE_IMAGE_RETENTION_DAYS),
flush=True)
except Exception as exc:
print("[prune] failed: {}".format(exc), flush=True)
time.sleep(PRUNE_INTERVAL_SECONDS)
def main():
store.init()
try:
@ -507,8 +533,17 @@ def main():
if ip:
print(" On your phone: http://{}:{} (same Wi-Fi)".format(ip, PORT))
print(" Database: {}".format(store.DB_PATH))
if store.SOURCE_IMAGE_RETENTION_DAYS > 0:
print(" Photos kept: {} days (grades kept forever)".format(
store.SOURCE_IMAGE_RETENTION_DAYS))
else:
print(" Photos kept: forever (pruning disabled)")
print("\n Ctrl-C to stop.\n")
# Sweep once at boot so a container that restarts often still expires
# things, then hand off to the timer.
threading.Thread(target=_prune_forever, daemon=True).start()
try:
server.serve_forever()
except KeyboardInterrupt: