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>
This commit is contained in:
@@ -49,3 +49,24 @@ export function safeFilename(name: string): string {
|
||||
.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}`;
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
upsertSendoffDevice,
|
||||
} from 'officerdb';
|
||||
import { getNotifyServerUrl } from '../notify/router';
|
||||
import { safeFilename, safeSegment, sendoffDirFor } from './paths';
|
||||
import { displayNameFor, folderNameFor, safeFilename, safeSegment, sendoffDirFor } from './paths';
|
||||
|
||||
/*
|
||||
SendOff — files land in a real directory in the recipient's own tree.
|
||||
@@ -140,7 +140,7 @@ sendoffRouter.post('/send', async (ctx) => {
|
||||
targetUserId = toUser;
|
||||
targetAccount = { email: recipient.email, isOwner: full?.id === 1 };
|
||||
// From the TOKEN. This is the whole reason the folder name can be trusted.
|
||||
folder = safeSegment(me.name || me.email.split('@')[0] || `user-${me.id}`);
|
||||
folder = folderNameFor({ id: me.id, name: me.name ?? null, email: me.email });
|
||||
}
|
||||
|
||||
if (!folder) throw errors.BAD_REQUEST('that name has no usable characters');
|
||||
@@ -172,7 +172,19 @@ sendoffRouter.get('/targets', async (ctx) => {
|
||||
listSendoffDevices(me.id),
|
||||
isOwnerRole(me.role) ? listOtherAccounts(me.id) : listMyRecipients(me.id),
|
||||
]);
|
||||
return ctx.json({ devices, people });
|
||||
/*
|
||||
`displayName` is served rather than derived, at mobile's request and for the right reason: the writer
|
||||
already computes this exact string to name a folder, and a picker that computes its own would sooner or
|
||||
later disagree with it — which means telling somebody the wrong place to look.
|
||||
|
||||
`myFolder` answers the question a picker actually wants: files I send land in the RECIPIENT's tree under
|
||||
MY name, so it is one value for the whole list, not a field on each row.
|
||||
*/
|
||||
return ctx.json({
|
||||
devices,
|
||||
people: people.map((p) => ({ ...p, displayName: displayNameFor(p) })),
|
||||
myFolder: folderNameFor({ id: me.id, name: me.name ?? null, email: me.email }),
|
||||
});
|
||||
});
|
||||
|
||||
/** GET /sendoff/senders — who I have allowed to send to me. */
|
||||
|
||||
Reference in New Issue
Block a user