fix(blur): .blurred round-trips any rel, and one writer serves both surfaces
The heid bug-hunt on r2b merge 1 found the /blur route stripping `f` before
writing, so the form for " a.png" blurred its neighbour "a.png". The route was
only half of it: `.blurred` was one stripped rel per line, so no writer could
store a rel with a leading space or a newline, whatever the route did.
Operator-ruled 2026-09-23 ("fix the blur").
- booth/blur.py (new, stdlib-only): read_blurred / set_blurred / BLUR_FILE.
`.blurred` is now a JSON array in sorted order, the `.seen` shape: opened
O_NOFOLLOW | O_NONBLOCK with an S_ISREG check and a 1 MiB cap, so a planted
symlink is refused and a FIFO can no longer hang every Desk render (the old
read_text() blocked on one). Writes go through mkstemp + os.replace. The
legacy line format is still READ, so the 6 live line-format files keep their
blur until their next write upgrades them. Measured before the change: 42
live rels, none with edge whitespace, so the defect had no live victims.
- The route no longer strips `f`.
- scripts/booth `blur`/`unblur` go through booth.blur.set_blurred instead of
their own grep/printf line writer. Two writers of one format is how the
formats drift, and after this change the shell writer would have appended a
line to a JSON array. Every path is checked before anything is written.
- Item.blurred_self (appended to the record): the item's own blur, resolved in
booth_items from the same read as `blurred`. It replaces build_gallery's
second read_blurred, which a write between the two reads could split
(invariant 3). app.py no longer reads blur state at all, and a test asserts
it.
Names stay importable from booth.app and booth.items (invariant 4). blur joins
test_stdlib_only. test_cli's per-item-survives test now reads through the reader
rather than asserting the old byte format. The r2b contract and its mutation
row follow blurred_self onto the record. tests/mutations/blur_storage.toml
proves 12 falsifiers by running the change each forbids.
Not in this change, and still ours: the "off"-means-ON idiom drift between
/blur, /blurbooth and /flag (forms only ever send 0/1), and the CLI's
`.blurbooth` touch following a symlink where the service no longer does.
This commit is contained in:
+20
-12
@@ -144,7 +144,6 @@ set -euo pipefail
|
||||
DATA="${BOOTH_DATA_DIR:-$HOME/booth-data}"
|
||||
URL="${BOOTH_URL:-http://10.100.10.50:8090}"
|
||||
KEEP=".forever" # must match KEEP_MARKER in booth/app.py
|
||||
BLUR=".blurred" # one booth-relative item path per line; see `blur` below
|
||||
LINKS_BOARD="${BOOTH_LINKS_BOARD:-links}"
|
||||
|
||||
# `--why` / `--title` for `new` and `add`. Pulled out of "$@" wherever they
|
||||
@@ -306,7 +305,6 @@ case "$cmd" in
|
||||
[ $# -ge 1 ] || usage
|
||||
b="$1"; shift
|
||||
[ -d "$DATA/$b" ] || { echo "no such booth: $b" >&2; exit 1; }
|
||||
f="$DATA/$b/$BLUR"
|
||||
|
||||
# NO FILES NAMED = THE WHOLE BOOTH. The Desk shows up to four images from
|
||||
# every booth on the page the operator opens first, so a booth that should
|
||||
@@ -327,23 +325,33 @@ case "$cmd" in
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Every item is checked BEFORE anything is written, so a refused path
|
||||
# leaves the blur set exactly as it was.
|
||||
items=()
|
||||
for item in "$@"; do
|
||||
item="${item#"$DATA/$b/"}"; item="${item#/}"
|
||||
case "$item" in
|
||||
*..*) echo "refusing path with '..': $item" >&2; exit 2 ;;
|
||||
esac
|
||||
[ -e "$DATA/$b/$item" ] || echo "warning: no such item in $b: $item" >&2
|
||||
touch "$f"
|
||||
if [ "$cmd" = blur ]; then
|
||||
grep -qxF -- "$item" "$f" || printf '%s\n' "$item" >> "$f"
|
||||
else
|
||||
grep -vxF -- "$item" "$f" > "$f.tmp" || true
|
||||
mv -- "$f.tmp" "$f"
|
||||
fi
|
||||
items+=("$item")
|
||||
done
|
||||
# An empty marker is a lie by omission — `ls -a` should say whether
|
||||
# anything here is blurred at all.
|
||||
[ -s "$f" ] || rm -f -- "$f"
|
||||
# ONE WRITER. `.blurred` is a JSON array now (a rel may carry a leading
|
||||
# space or a newline, and the old line format could not round-trip it), and
|
||||
# the service writes it too — so the CLI goes through the same stdlib-only
|
||||
# booth.blur the service does, never a grep/printf of its own. Items travel
|
||||
# as argv, which carries any byte but NUL; an env var or a line would not.
|
||||
# An emptied set removes the file (booth.blur), so `ls -a` still says
|
||||
# whether anything here is blurred at all.
|
||||
BOOTH_SRC="$(booth_src)" BOOTH_DIR="$DATA/$b" python3 -c '
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, os.environ["BOOTH_SRC"])
|
||||
from booth.blur import set_blurred # stdlib only — no venv needed
|
||||
on = sys.argv[1] == "blur"
|
||||
for rel in sys.argv[2:]:
|
||||
set_blurred(Path(os.environ["BOOTH_DIR"]), rel, on)
|
||||
' "$cmd" "${items[@]}"
|
||||
if [ "$cmd" = blur ]; then
|
||||
echo "blurred (cosmetic — still served): $URL/b/$b/"
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user