mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 08:06:59 +00:00
42 lines
1.9 KiB
TypeScript
42 lines
1.9 KiB
TypeScript
// Ported from the webmail's lib/jmap/filenode-name.ts (#869).
|
|
//
|
|
// Percent-decode a FileNode name that Stalwart stored in URI form.
|
|
//
|
|
// When a file or folder is created over WebDAV (rclone, WinSCP, Finder, ...),
|
|
// Stalwart keeps the raw, percent-encoded path segment as the FileNode name -
|
|
// "Spares Catalog" arrives as "Spares%20Catalog" and Arabic/other non-ASCII
|
|
// names as UTF-8 escapes. WebDAV clients never notice because Stalwart echoes
|
|
// that string back verbatim in <D:href> and they decode it, but JMAP
|
|
// FileNode/get returns it as-is, so the Files tab showed "Spares%20Catalog".
|
|
// Decode it at the API boundary so the UI shows the human-readable name.
|
|
//
|
|
// Conservative on purpose: a name is only decoded when it contains at least
|
|
// one valid %XX escape and decodes cleanly; a literal "%" in a JMAP-created
|
|
// name ("100% done.txt") is left untouched. A decoded "/" would turn a name
|
|
// into a path, so such names are also left alone.
|
|
const PERCENT_ESCAPE = /%[0-9A-Fa-f]{2}/;
|
|
|
|
export function decodeFileNodeName(name: string): string {
|
|
if (!PERCENT_ESCAPE.test(name)) return name;
|
|
try {
|
|
const decoded = decodeURIComponent(name);
|
|
if (decoded.includes('/') || decoded.includes('\0')) return name;
|
|
return decoded;
|
|
} catch {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
// Append " (1)", " (2)", ... before the extension until the name is free in
|
|
// the target folder (webmail file-store getUniqueName). Used when uploading a
|
|
// file whose name already exists so two nodes never share one name.
|
|
export function getUniqueName(name: string, existingNames: Set<string>): string {
|
|
if (!existingNames.has(name)) return name;
|
|
const dotIndex = name.lastIndexOf('.');
|
|
const base = dotIndex > 0 ? name.substring(0, dotIndex) : name;
|
|
const ext = dotIndex > 0 ? name.substring(dotIndex) : '';
|
|
let counter = 1;
|
|
while (existingNames.has(`${base} (${counter})${ext}`)) counter++;
|
|
return `${base} (${counter})${ext}`;
|
|
}
|