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>
78 lines
3.2 KiB
TypeScript
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 };
|
|
}
|
|
}
|