import { isAbsolute, join, normalize } from 'node:path'; import { db } from 'officerdb/db'; import { eq } from 'drizzle-orm'; import { getOwnerHomeDir } from '@@/data-path'; import { fofosAdminSettings } from '../db/schema'; /** * Administration's settings — currently one, where the fofos site is on this machine. * * ── The default is COMPUTED, not written ── * * No row means `/fofos`, worked out on every read. The alternative — writing the default * in at install — freezes whatever the home directory was on the day the plugin was installed, and a * restore onto a machine with a different owner then points at a directory that does not exist while * looking deliberately chosen. Computed, it follows the machine; written, it follows the install. * * A saved path is a DECISION and is stored exactly as given. Only the absence of one is derived — and * once saved it stays saved: there is no route back to the default, because "reset this to something * you never chose" is not a thing anybody asked for and it read as a second, quieter Save. * * ── Absolute, always ── * * `~` is expanded here and never stored. It means "whoever is asking", and the people asking are not * all the same person: this plugin is grantable, and the site is in one place regardless of who is * looking at it. Storing the tilde would give every user a different answer to a question that has * one. */ export type SettingsView = { /** Absolute. Never null — an unset root reports its default rather than nothing. */ rootPath: string; /** False while the default is standing in, so a form can show it as a suggestion, not a choice. */ isSet: boolean; }; const row = async () => { const rows = await db.select().from(fofosAdminSettings).limit(1); return rows[0] ?? null; }; /** `/fofos`. The owner's, not the caller's — see the note on absoluteness above. */ export const defaultRootPath = (email: string): string => join(getOwnerHomeDir(email), 'fofos'); export async function readSettings(email: string): Promise { const found = await row(); const stored = found?.rootPath ?? null; return { rootPath: stored ?? defaultRootPath(email), isSet: stored !== null }; } /** * Expand `~`, normalise, and refuse anything that is still not absolute. * * Refuses rather than resolving against a working directory: a relative path here would be resolved * against whatever `process.cwd()` happened to be, which is the platform's checkout — a silently wrong * answer that only shows up as an empty listing much later. */ export function normaliseRootPath(input: string, email: string): string { const trimmed = input.trim(); if (!trimmed) throw new Error('A path is required.'); const home = getOwnerHomeDir(email); const expanded = trimmed === '~' ? home : trimmed.startsWith('~/') ? join(home, trimmed.slice(2)) : trimmed; if (!isAbsolute(expanded)) throw new Error('The root location must be an absolute path.'); // No trailing slash, so two spellings of the same directory cannot be stored as different values. const cleaned = normalize(expanded).replace(/\/+$/, ''); return cleaned === '' ? '/' : cleaned; } export async function saveRootPath(input: string, email: string): Promise { const rootPath = normaliseRootPath(input, email); const existing = await row(); if (existing) { await db .update(fofosAdminSettings) .set({ rootPath, updatedAt: new Date() }) .where(eq(fofosAdminSettings.id, existing.id)); } else { await db.insert(fofosAdminSettings).values({ rootPath }); } return readSettings(email); }