import { randomUUID } from 'node:crypto' import { posix } from 'node:path' export interface FileNameMeta { name: string ext: string } export interface UniqueNameFormatterParams { base: string count: number extension: string } export type UniqueNameFormatter = (params: UniqueNameFormatterParams) => string export default class PathService { public toStoragePath(path: string = ''): string { let safePath = path.replace(/^(\.\.(\/|\\|$))+/, '') safePath = safePath.startsWith('/') ? safePath.slice(1) : safePath return safePath } public toClientPath(path: string = ''): string { return path.replace(/^\/?uploads\//, '/') } public join(...parts: string[]): string { return posix.join(...parts) } public dirname(path: string): string { return this.toStoragePath(path).split('/').slice(0, -1).join('/') } public basename(path: string): string { return path.split('/').reverse()[0] } public getFileNameMeta(name: string): FileNameMeta { const extIndex = name.lastIndexOf('.') const rawName = extIndex !== -1 ? name.slice(0, extIndex) : name const ext = extIndex !== -1 ? name.slice(extIndex).trim() : '' return { name: rawName, ext, } } public sanitizeFileName(name: string): string { return name.replace(/[()]/g, '').replace(/[^a-zA-Z0-9.\-_ ]/g, '') } public async generateUniqueName( dir: string, fileName: string, exists: (path: string) => Promise, formatter?: UniqueNameFormatter ): Promise { const { ext, name } = this.getFileNameMeta(fileName) const safeBase = this.sanitizeFileName(name) let counter = 1 let newName = `${safeBase}${ext}` while (await exists(this.join(dir, newName))) { newName = typeof formatter === 'function' ? formatter({ base: safeBase, count: counter, extension: ext }) : `${safeBase}-${counter}${ext}` counter++ if (counter > 5) { newName = `${randomUUID()}${ext}` break } } return newName } }