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 ── * * `/media`, a sibling of `/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 — `/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 => { const settings = await readSettings(email); return join(settings.rootPath, DIR_NAME); }; export async function readMediaStatus(email: string): Promise { 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 }; } }