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>
73 lines
3.1 KiB
TypeScript
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}`;
|