import { join } from 'node:path'; import { getHomeDir, getOwnerHomeDir } from '@@/data-path'; /** `Documents/SendOff` under whichever home this account actually browses. */ export const SENDOFF_SUBPATH = join('Documents', 'SendOff'); /** * The SendOff directory for an account, in the home its own file browser shows. * * The two homes are not interchangeable and picking the wrong one makes a delivery invisible: the owner * browses their real login home (`/home/pastilhas`), while another account browses the managed home under * `DATA_PATH//home`. `getOwnerHomeDir` ignores the email it is passed and always answers the owner, * so it can only be used once the role has been checked — which is why this takes the role rather than * working it out from the email. */ export function sendoffDirFor(account: { email: string; isOwner: boolean }): string { const home = account.isOwner ? getOwnerHomeDir(account.email) : getHomeDir(account.email); return join(home, SENDOFF_SUBPATH); } /** * A folder name safe to create, from a name a person or a device chose. * * Anything that is not a letter, digit, dash, underscore or dot becomes a dash, and leading dots go — so a * name cannot produce `..`, a hidden directory, or a path with a separator in it. Empty after that is * refused by the caller rather than silently becoming a directory called `-`. */ export function safeSegment(name: string): string { return name .trim() .replace(/[^A-Za-z0-9._-]+/g, '-') .replace(/^\.+/, '') .replace(/^-+|-+$/g, '') .slice(0, 64); } /** * A filename safe to write, preserving the extension. * * Same rules as a segment, with the basename taken first: a browser will happily send `../../etc/passwd` * as a filename and the multipart spec does not forbid it. */ export function safeFilename(name: string): string { const base = name.split(/[/\\]/).pop() ?? ''; const cleaned = base .trim() .replace(/[^A-Za-z0-9._-]+/g, '-') .replace(/^\.+/, '') .slice(0, 200); return cleaned || 'file'; } /** * The name an account appears as, and therefore the name of the folder its files land in. * * One definition, used by the writer AND by `/targets`, because the two must agree: a picker that says * "Fedra" while the writer creates `fedra.almeida` has told somebody the wrong place to look. Mobile asked * for this to be served rather than derived, which is right — three clients reimplementing a fallback is * how three clients disagree. * * `users.name` is nullable, so the fallback chain ends at the account id, which is always there. */ export function displayNameFor(account: { id: number; name: string | null; email: string }): string { const chosen = account.name?.trim(); if (chosen) return chosen; const local = account.email.split('@')[0]?.trim(); return local || `user-${account.id}`; } /** What that name becomes on disk — the same value, through the segment sanitiser. */ export const folderNameFor = (account: { id: number; name: string | null; email: string }): string => safeSegment(displayNameFor(account)) || `user-${account.id}`;