From d7241531f974d04c28a6edcb476dc67a375c2f04 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Tue, 29 Sep 2026 07:10:41 +0700 Subject: [PATCH] web: a reading walks past the recycle bin, not into it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The walk already turned its back on the folders a camera and an editor leave behind: a name beginning with `.` or `@` is skipped, which is `.thumbnails`, `.git`, `.Trash`, and the `@eaDir` a Synology writes beside every frame. An external volume drags along a second set of names, and none of them begins with a dot, so every one of them was walked. `$RECYCLE.BIN` and `RECYCLER` are where Windows parks what the reader deleted — frames among them, at full size, and every one of them decoded into a thumbnail on the way in. `System Volume Information` is Windows' own bookkeeping. `#recycle` is the Synology share, and `lost+found` is the directory a Linux volume keeps for repairs. A frame that comes back out of a bin is a frame the reader threw away, and the pictures in a share's recycle folder are pictures someone else deleted; neither belongs in the catalogue, and both cost the reading the same seconds a real frame does. The folder also stood in the column as a row of its own. The rule is now one `SYSTEM_DIR` expression over both sets, matched case-insensitively — the same volume spells the bin `$RECYCLE.BIN` on one drive and `$Recycle.Bin` on the next — and the walk asks it before it looks at what the entry is, so a refused folder is neither read nor named. Verified: roll-walk-check.mjs — all passed, with two new assertions: the stand-in roll now carries `$RECYCLE.BIN`, `$Recycle.Bin`, `RECYCLER`, `System Volume Information`, `#recycle` and `lost+found`, and every one of them holds a file named like a frame, so nothing about the files can be what keeps them out. No frame is read from them, and none of them reaches the column. library-check.mjs — 43 steps, all passed. scan-nav-check.mjs — all passed. frontend tsc --noEmit clean. ponytail: the list is the names the volumes being read actually use, not a guess at every spelling there is — a box that files its junk under something else gets a line in that expression, which is the whole of the change. A folder of the reader's own that happens to be called `#recycle` is skipped too; that name is worth the trade. Co-authored-by: PenguinHarness --- docker/frontend/scripts/roll-walk-check.mjs | 17 +++++++++++++++-- docker/frontend/src/engine/rollWalk.ts | 13 ++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/docker/frontend/scripts/roll-walk-check.mjs b/docker/frontend/scripts/roll-walk-check.mjs index deea1c9..abd4743 100644 --- a/docker/frontend/scripts/roll-walk-check.mjs +++ b/docker/frontend/scripts/roll-walk-check.mjs @@ -19,7 +19,9 @@ const { DIRS_PER_PASS, jumpQueue, walkPass } = await import(`data:text/javascrip // A directory handle is only ever read for `values()` and `getDirectoryHandle()`, // so a stand-in tree needs nothing else: `{ name: { … } }` for folders and // `name: null` for a file. `@eaDir` and `.thumbnails` are the folders a real roll -// drags along. +// drags along, and the recycle bins below are the ones an external volume puts at +// its root — each holds a frame-named file, so only the folder rule can be what +// keeps them out. const dir = (name, children) => ({ kind: 'directory', name, @@ -44,6 +46,12 @@ const roll = dir('CheckRoll', { 'Empty': { 'notes.txt': null }, '2026': { 'P2.JPG': null, '04': { 'P3.RW2': null, 'notes.txt': null } }, '2025': { 'P4.JPG': null }, + '$RECYCLE.BIN': { 'deleted.JPG': null }, + '$Recycle.Bin': { 'also-deleted.JPG': null }, + 'RECYCLER': { 'older.JPG': null }, + 'System Volume Information': { 'W.JPG': null }, + '#recycle': { 'synology.JPG': null }, + 'lost+found': { 'linux.JPG': null }, }); // Walks to the end, one click per pass, which is what the browser does between @@ -77,6 +85,11 @@ assert.ok( 'no deeper frame is read before a shallower one' ); assert.ok(!rels.some((rel) => /\.thumbnails|@eaDir|notes\.txt/.test(rel)), 'hidden and unsupported names are skipped'); +// The frames inside the recycle bins are named like frames, so nothing about the +// files keeps them out: the folders themselves are what the walk refuses. +const SYSTEM = /\$Recycle\.Bin|RECYCLER|System Volume Information|#recycle|lost\+found/i; +assert.ok(!rels.some((rel) => SYSTEM.test(rel)), 'a recycle bin or system folder yields no frame'); +assert.ok(!names.some((rel) => SYSTEM.test(rel)), 'and never becomes a row either'); assert.deepEqual( names, ['2025/', '2026/', 'Empty/', '2026/04/'], @@ -130,4 +143,4 @@ assert.equal(await walkPass(many, found, [], keep, () => null), false, 'the queu assert.equal(found.length, DIRS_PER_PASS - 1, `one pass read the picked folder and ${DIRS_PER_PASS - 1} below it`); assert.equal(many.pending.length, 6, 'the rest is left in the queue, for the next pass'); -console.log(`ok roll walk — ${rels.length} frames layer by layer, names a pass ahead, click jumps the queue, ${DIRS_PER_PASS} folders a pass`); +console.log(`ok roll walk — ${rels.length} frames layer by layer, recycle bins and system folders skipped, names a pass ahead, click jumps the queue, ${DIRS_PER_PASS} folders a pass`); diff --git a/docker/frontend/src/engine/rollWalk.ts b/docker/frontend/src/engine/rollWalk.ts index c23b4e2..48dd033 100644 --- a/docker/frontend/src/engine/rollWalk.ts +++ b/docker/frontend/src/engine/rollWalk.ts @@ -37,6 +37,15 @@ export interface Walk { // pass rather than a hundred. export const DIRS_PER_PASS = 200; +// Folders a roll drags along that are never the reader's photographs. A leading +// `.` or `@` is the `.thumbnails` a camera writes, `.git`, `.Trash`, and the +// `@eaDir` a Synology puts beside every frame; the spelled-out names are the +// recycle bins and system folders of Windows, Synology and Linux volumes, which +// hold deleted or machine-written files rather than frames. Matching is +// case-insensitive because the same folder is `$RECYCLE.BIN` on one volume and +// `$Recycle.Bin` on the next. +export const SYSTEM_DIR = /^[.@]|^\$RECYCLE\.BIN$|^RECYCLER$|^System Volume Information$|^#recycle$|^lost\+found$/i; + // The folder the reader clicked goes first. When it is not in the queue the walk // has not reached it yet — its row is on screen because the parent pass named it // — so it is opened from the picked folder and read next. A folder already read, @@ -82,9 +91,7 @@ export async function walkPass( walk.walked.add(here.rel); try { for await (const entry of here.dir.values()) { - // Names beginning with `.` or `@` are skipped — that is `.thumbnails`, - // `.git`, and the `@eaDir` a Synology writes beside every frame. - if (/^[.@]/.test(entry.name)) continue; + if (SYSTEM_DIR.test(entry.name)) continue; if (entry.kind === 'directory') { const rel = `${here.rel}${entry.name}/`; walk.pending.push({ dir: entry as FileSystemDirectoryHandle, rel });