Create Gallery on a folder means the files inside it

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 21:16:05 +01:00
co-authored by Claude Opus 5
parent f61653f7af
commit a610f93189
2 changed files with 51 additions and 3 deletions
+25 -1
View File
@@ -7,7 +7,7 @@ import { readWebsiteStatus, cloneWebsite, updateWebsite } from './website';
import { readMediaStatus, createMediaDir } from './media'; import { readMediaStatus, createMediaDir } from './media';
import { readSync, startSync } from './sync'; import { readSync, startSync } from './sync';
import { readRuns } from './ledger'; import { readRuns } from './ledger';
import { reconcile } from './derive'; import { listOriginals, originalsDir, reconcile, safeScope } from './derive';
import { createDatabases, pushSchema, readDatabases } from './databases'; import { createDatabases, pushSchema, readDatabases } from './databases';
import { readBackups, restoreBackup, runBackup, saveBackups, schedule, testDestination } from './backups'; import { readBackups, restoreBackup, runBackup, saveBackups, schedule, testDestination } from './backups';
import { watchMailbox } from './messages'; 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 * "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. * 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) => { router.post('/_config/media/sync', async (ctx) => {
const user = ctx.get('user'); const user = ctx.get('user');
// A folder inside `originals/`, or nothing for all of it. Named `path` because that is what the file // A folder inside `originals/`, or nothing for all of it. Named `path` because that is what the file
+26 -2
View File
@@ -43,8 +43,32 @@ export const fileActions: FileAction[] = [
const directories = entries.filter((entry) => entry.type === 'directory').length; const directories = entries.filter((entry) => entry.type === 'directory').length;
return directories === 0 || (directories === 1 && entries.length === 1); return directories === 0 || (directories === 1 && entries.length === 1);
}, },
run: (entries) => { run: async (entries) => {
const paths = entries.map((entry) => relativeTo(entry.path)).filter((path): path is string => Boolean(path)); /*
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. Handed over in the URL, and the Galleries panel does the asking.