Files
fofos-admin/api/media.ts
T
pastilhasandClaude Opus 5 003613872d provision <root>/media, beside the checkout
where the images and the files will live. same row shape as the checkout above it,
because it is the same kind of fact: a thing that either is or is not at a known
path under the root, with one button that puts it there.

BESIDE `website`, never inside it, and that is the point of it being here rather
than a folder in the repository:

  - it must SURVIVE the checkout. deleting `website` and cloning it again is a
    normal thing to do when something is wrong with it, and it must never be the
    thing that destroys the client's photographs.
  - it must never be TRACKED. media in git is a repository nobody can clone a year
    from now, and a .gitignore is a promise somebody eventually breaks with
    `git add -f`.

both under one root, so a backup of the root is the whole site — the code and the
things it serves. that is the property that makes the root worth being a setting.

NOTHING ABOUT ITS INSIDES IS DECIDED. this creates the directory and reports
whether it is there; the layout, who writes to it and what goes where are still
being described. inventing a folder tree now would settle that in the file least
able to change its mind, because directories on disk are not a migration you can
run.

`mkdir -p`, so it also creates a root that is still only a default — saving a root
creates nothing, and provisioning into a path that does not exist is the ordinary
first run. idempotent by the same flag, which is what makes the button safe to
offer next to a status that might be stale.

no console for this one: making a directory either works or gives one line about
why not, and a black transcript box for a single sentence makes a small thing look
like a large one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 01:03:28 +01:00

78 lines
3.2 KiB
TypeScript

import { mkdir, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { readSettings } from './settings';
/**
* The media directory under the root — where the images and files live.
*
* ── BESIDE the checkout, never inside it ──
*
* `<root>/media`, a sibling of `<root>/website`. That is the whole point of it being here rather than
* a folder in the repository:
*
* - it must survive the checkout. deleting `website` and cloning it again is a normal thing to do
* when something is wrong with it, and it must never be a thing that destroys the client's photos.
* - it must never be tracked. media in git is a repository nobody can clone a year from now, and a
* `.gitignore` is a promise somebody eventually breaks with `git add -f`.
*
* The two live under one root so a backup of the root is the whole site — the code and the things it
* serves — which is the property that makes the root worth being a setting at all.
*
* ── Nothing about its INSIDES is decided here ──
*
* This creates the directory and reports whether it is there. What goes in it, how it is laid out, and
* who writes to it are not settled, and inventing a folder tree now would be deciding that here — in
* the file least able to change its mind later, because directories on disk are not a migration you
* can run.
*/
const DIR_NAME = 'media';
export type MediaStatus = {
/** Absolute — `<root>/media`. */
path: string;
present: boolean;
/** How many things are directly inside. Null when it is not there. */
entries: number | null;
/** It exists but is not a directory — a file in the way, which `mkdir` would fail on. */
blocked: boolean;
};
export const mediaPath = async (email: string): Promise<string> => {
const settings = await readSettings(email);
return join(settings.rootPath, DIR_NAME);
};
export async function readMediaStatus(email: string): Promise<MediaStatus> {
const path = await mediaPath(email);
const info = await stat(path).catch(() => null);
if (!info) return { path, present: false, entries: null, blocked: false };
if (!info.isDirectory()) return { path, present: true, entries: null, blocked: true };
const entries = await readdir(path).catch(() => null);
return { path, present: true, entries: entries?.length ?? null, blocked: false };
}
/**
* Make it.
*
* `recursive`, so it also creates the root when the root is still only a default — saving a root does
* not create anything, and provisioning into a path that does not exist yet is the ordinary first run
* rather than an error worth reporting.
*
* Idempotent by the same flag: pressing it on a directory that already exists changes nothing and
* fails at nothing, which is what makes it safe to offer next to a status that might be stale.
*/
export async function createMediaDir(email: string): Promise<{ ok: boolean; error?: string }> {
const status = await readMediaStatus(email);
if (status.blocked) return { ok: false, error: `${status.path} exists and is not a directory.` };
try {
await mkdir(status.path, { recursive: true });
return { ok: true };
} catch (error) {
return { ok: false, error: (error as Error).message };
}
}