derived/ is built from originals/, and you can watch it happen
The mapping: derived/ mirrors originals/ directory for directory, and where originals holds a FILE, derived holds a DIRECTORY of the same name — extension included — with the renditions inside it. Keeping the extension is what makes it total: a.jpg and a.png are different originals and must not share a folder. The inverse is pure string work, so the filesystem is the record and there is no second copy of the truth. Images get a WebP ladder at 320/640/960/1280/1920/2560 plus full.webp, and nothing is ever upscaled — a rung wider than the original would be a soft file that srcset then confidently prefers over the sharp one. full.webp always exists because for a HEIC it is the only browser-readable copy of the picture there is. Video gets av-club's web-1080 and web-720 presets by name, plus a poster. Images are named by width because srcset descriptors are widths; video by height because that is av-club's vocabulary and nobody misreads 1080.mp4. The ladder is fixed here and the SITE chooses from it. This never reads website/src/media/slots.ts — a plugin that knows a website's layout is a plugin for one website, and that boundary is the difference between a template and something fofos-shaped. Each derived folder carries a manifest naming the size and mtime it was built from, so the sync is idempotent and resumable: a run that died halfway is finished by running it again. That is what makes an in-memory job acceptable rather than merely convenient. It builds into <name>.partial and renames, so an interruption can never leave a folder that looks finished. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { deleteConnection, readConnection, readCredentials, recordProbe, saveCon
|
||||
import { readSettings, saveRootPath } from './settings';
|
||||
import { readWebsiteStatus, cloneWebsite, updateWebsite } from './website';
|
||||
import { readMediaStatus, createMediaDir } from './media';
|
||||
import { readSync, startSync } from './sync';
|
||||
import { fofosRequest, FofosError, probe, type FofosResponse } from './fofos-client';
|
||||
|
||||
/**
|
||||
@@ -228,6 +229,28 @@ router.post('/_config/media/create', async (ctx) => {
|
||||
return ctx.json({ ...result, status: await readMediaStatus(user.email) }, result.ok ? 200 : 400);
|
||||
});
|
||||
|
||||
/**
|
||||
* Building `derived/` from `originals/`.
|
||||
*
|
||||
* Two routes and no more: start one, and ask how it is going. The work is asynchronous because a
|
||||
* folder of photographs takes minutes, and the panel polls the second route rather than holding a
|
||||
* request open — a connection that has to survive an eight-minute encode is a connection that will
|
||||
* not.
|
||||
*
|
||||
* `POST` answers 200 whether it started a run or found one already going, and says which in `started`.
|
||||
* "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.
|
||||
*/
|
||||
router.post('/_config/media/sync', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
return ctx.json(startSync(user.email));
|
||||
});
|
||||
|
||||
router.get('/_config/media/sync', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
return ctx.json(readSync(user.email));
|
||||
});
|
||||
|
||||
/**
|
||||
* Is this plugin usable? Asked once by the detail panel rather than by all nine sections.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user