From a610f9318974bccc9b601a9d684b0e95814fa34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 21:16:05 +0100 Subject: [PATCH] Create Gallery on a folder means the files inside it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It stored the folder. What somebody right-clicks is a directory; what a gallery holds is pictures — obvious the moment you look at the result, and not before. Expanded by the server, because that side already knows which extensions are media and how to walk the tree. It is the same `listOriginals` the sync uses, so a gallery cannot end up holding a file the pipeline would never convert. Recursive, deliberately: a folder of folders is still "these pictures", and refusing to descend would silently miss most of what was pointed at. An expansion that returns nothing creates nothing. Falling back to the folder itself is exactly the bug being fixed, and doing it quietly is worse than doing nothing. Co-Authored-By: Claude Opus 5 --- api/router.ts | 26 +++++++++++++++++++++++++- web/file-actions.ts | 28 ++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/api/router.ts b/api/router.ts index 3ca0b6c..d0e0ef8 100644 --- a/api/router.ts +++ b/api/router.ts @@ -7,7 +7,7 @@ import { readWebsiteStatus, cloneWebsite, updateWebsite } from './website'; import { readMediaStatus, createMediaDir } from './media'; import { readSync, startSync } from './sync'; import { readRuns } from './ledger'; -import { reconcile } from './derive'; +import { listOriginals, originalsDir, reconcile, safeScope } from './derive'; import { createDatabases, pushSchema, readDatabases } from './databases'; import { readBackups, restoreBackup, runBackup, saveBackups, schedule, testDestination } from './backups'; import { watchMailbox } from './messages'; @@ -250,6 +250,30 @@ router.post('/_config/media/create', async (ctx) => { * "Already running" is not a failure to report to somebody who pressed Sync; the honest answer is the * run that is in progress, which is what comes back. */ +/** + * The media files under a folder in `originals/`, relative to it. + * + * For "Create Gallery" on a directory: what somebody right-clicked is the folder, and what a gallery + * holds is the files. Expanded here rather than in the browser because this side already knows which + * extensions are media and how to walk the tree — `listOriginals` is the same function the sync uses, + * so a gallery cannot contain a file the pipeline would not convert. + * + * Recursive, deliberately: a folder of folders is still "these pictures", and refusing to descend would + * make the action silently miss most of what somebody pointed at. + */ +router.get('/_config/media/files', async (ctx) => { + const user = ctx.get('user'); + const scope = ctx.req.query('path') ?? ''; + try { + const root = await originalsDir(user.email); + const files = await listOriginals(root, safeScope(scope)); + // Prefixed back to the scope, so what comes out is what a gallery item stores. + return ctx.json({ files: files.map((file) => (scope ? `${safeScope(scope)}/${file}` : file)) }); + } catch (error) { + return ctx.json({ error: (error as Error).message }, 400); + } +}); + router.post('/_config/media/sync', async (ctx) => { const user = ctx.get('user'); // A folder inside `originals/`, or nothing for all of it. Named `path` because that is what the file diff --git a/web/file-actions.ts b/web/file-actions.ts index b49e3cc..39b3d0d 100644 --- a/web/file-actions.ts +++ b/web/file-actions.ts @@ -43,8 +43,32 @@ export const fileActions: FileAction[] = [ const directories = entries.filter((entry) => entry.type === 'directory').length; return directories === 0 || (directories === 1 && entries.length === 1); }, - run: (entries) => { - const paths = entries.map((entry) => relativeTo(entry.path)).filter((path): path is string => Boolean(path)); + run: async (entries) => { + /* + A folder is expanded into the files inside it. + + What somebody right-clicked is the directory; what a gallery holds is pictures. Without this the + gallery gets one item — the folder — which is what shipped first and is obviously wrong the + moment you look at it. + + Expanded by the SERVER, because that side already knows which extensions are media and how to + walk the tree: it is the same `listOriginals` the sync uses, so a gallery cannot end up holding + a file the pipeline would never convert. + */ + const chosen = entries.map((entry) => relativeTo(entry.path)).filter((path): path is string => Boolean(path)); + + let paths = chosen; + if (entries.length === 1 && entries[0]!.type === 'directory') { + const response = await fetch( + `/api/fofos-admin/_config/media/files?path=${encodeURIComponent(chosen[0] ?? '')}`, + { credentials: 'same-origin' }, + ).catch(() => null); + + const body = response?.ok ? ((await response.json()) as { files?: string[] }) : null; + // Falling back to the folder itself would create the wrong gallery quietly. Nothing is better. + if (!body?.files?.length) return; + paths = body.files; + } /* Handed over in the URL, and the Galleries panel does the asking.