Files
platform/src/servers/api/sendoff/paths.ts
T
pastilhasandClaude Opus 5 2137d06498 the folder name is served, not derived three times
Mobile asked for `/targets` to carry each person's display name as the folder will
be named, so a picker can honestly say where files will appear. Right request: the
writer already computes that exact string to create a directory, and any client
computing its own would eventually disagree with it — which means telling somebody
the wrong place to look.

So `displayNameFor` is the one definition and the writer calls it too. `/targets`
gains `displayName` per person, and `myFolder` for the whole response: files land
in the RECIPIENT's tree under the SENDER's name, so that is one value rather than
a field on each row. The two are easy to confuse and the distinction matters — a
picker showing `displayName` as the destination folder would name the wrong one.

The web panel now uses both instead of its own copy of the fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 02:50:23 +00:00

73 lines
3.1 KiB
TypeScript

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/<email>/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}`;