44 行
1.5 KiB
TypeScript
44 行
1.5 KiB
TypeScript
export function fileExtension(pathOrName: string): string {
|
|
const clean = pathOrName.split(/[?#]/, 1)[0] ?? ''
|
|
const name = clean.split('/').pop() ?? clean
|
|
const index = name.lastIndexOf('.')
|
|
return index > 0 ? name.slice(index + 1).toLowerCase() : ''
|
|
}
|
|
|
|
export function sanitizeFileName(name: string, fallback = 'file'): string {
|
|
const sanitized = name
|
|
.trim()
|
|
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_')
|
|
.replace(/\s+/g, ' ')
|
|
return sanitized || fallback
|
|
}
|
|
|
|
/** Resolve an RFC 5987/6266 filename from a Content-Disposition header. */
|
|
export function parseContentDispositionFileName(header: string): string | null {
|
|
const encoded = header.match(/filename\*=(?:[^']*'[^']*')?([^;\s]+)/i)?.[1]
|
|
if (encoded) {
|
|
try {
|
|
return sanitizeFileName(decodeURIComponent(encoded.replace(/['"]/g, '')))
|
|
} catch {
|
|
// Fall through to the non-encoded forms.
|
|
}
|
|
}
|
|
|
|
const quoted = header.match(/filename="([^"]+)"/i)?.[1]
|
|
if (quoted?.trim()) return sanitizeFileName(quoted)
|
|
|
|
const plain = header.match(/filename=([^;\s"]+)/i)?.[1]
|
|
return plain?.trim() ? sanitizeFileName(plain) : null
|
|
}
|
|
|
|
export function fileNameFromUrl(url: string, fallback = 'download'): string {
|
|
try {
|
|
const pathname = new URL(url).pathname
|
|
const candidate = pathname.split('/').filter(Boolean).at(-1)
|
|
return candidate ? sanitizeFileName(decodeURIComponent(candidate), fallback) : fallback
|
|
} catch {
|
|
const candidate = url.split(/[?#]/, 1)[0]?.split('/').filter(Boolean).at(-1)
|
|
return candidate ? sanitizeFileName(candidate, fallback) : fallback
|
|
}
|
|
}
|