From 300edfb02359f7ed7897dbdf16d5a8475d79969c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 21:32:32 +0100 Subject: [PATCH] sort the walk, because readdir does not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `readdir` returns entries in the filesystem's own order — inode order on ext4 — which is arbitrary and stable only until somebody moves a file. Harmless for the sync, which does the same work whatever the order. Not harmless for "Create Gallery": that list becomes the gallery's initial order, and nine photographs arriving as 05, 08, 01, 07 reads as a bug even though nothing is wrong. `numeric` collation, so galeria2 sorts before galeria10 — which is what anybody naming files that way means. Co-Authored-By: Claude Opus 5 --- api/derive.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/api/derive.ts b/api/derive.ts index b5de482..bf2bfa2 100644 --- a/api/derive.ts +++ b/api/derive.ts @@ -401,7 +401,19 @@ async function buildVideo( /** Every file under `originals/`, relative to it, depth first. Directories are structure, not work. */ export async function listOriginals(root: string, prefix = ''): Promise { - const entries = await readdir(join(root, prefix), { withFileTypes: true }).catch(() => []); + /* + Sorted, because `readdir` is not. + + It returns entries in the filesystem's own order — inode order on ext4 — which is arbitrary and + stable only until somebody moves a file. That is harmless for the sync, which does the same work in + any order, and it is not harmless for "Create Gallery": this list becomes the gallery's initial + order, and nine photographs arriving as 05, 08, 01, 07 reads as a bug even though nothing is wrong. + + `numeric` so `galeria2` sorts before `galeria10`, which is what anybody naming files that way means. + */ + const entries = (await readdir(join(root, prefix), { withFileTypes: true }).catch(() => [])).sort((a, b) => + a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }), + ); const found: string[] = []; for (const entry of entries) {