import * as tus from 'tus-js-client' type FileRow = { name: string; path: string; is_dir: boolean; size: number; mtime: number } type RowLike = { name?: unknown Name?: unknown path?: unknown Path?: unknown is_dir?: unknown IsDir?: unknown isDir?: unknown size?: unknown Size?: unknown mtime?: unknown Mtime?: unknown } type NoResumeStorage = { addUpload: (_u: any) => Promise removeUpload: (_u: any) => Promise listUploads: () => Promise findUploadsByFingerprint: (_fp: string) => Promise } const videoExt = new Set(['mp4', 'mkv', 'mov', 'avi', 'm4v', 'webm', 'mpg', 'mpeg', 'ts', 'm2ts']) const imageExt = new Set(['jpg', 'jpeg', 'png', 'gif', 'heic', 'heif', 'webp', 'bmp', 'tif', 'tiff']) const extLower = (n: string) => (n.includes('.') ? n.split('.').pop()!.toLowerCase() : '') function sanitizeDesc(s: string) { s = s.trim().replace(/\s+/g, '_').replace(/[^A-Za-z0-9._-]+/g, '_') if (!s) s = 'upload' return s.slice(0, 64) } function sanitizeFolderName(s: string) { // Only allow a single path segment and collapse punctuation to keep mkdir/rename predictable. s = s.trim().replace(/[\/]+/g, '/').replace(/^\//, '').replace(/\/.*$/, '') s = s.replace(/\s+/g, '_').replace(/[^\w.\-]/g, '_').replace(/_+/g, '_') return s.slice(0, 64) } function extOf(n: string) { const i = n.lastIndexOf('.') return i > -1 ? n.slice(i + 1).toLowerCase() : '' } function stemOf(n: string) { const i = n.lastIndexOf('.') const stem = i > -1 ? n.slice(0, i) : n // Keep the stem safe and avoid extra dots inside the composed upload name. return sanitizeDesc(stem.replace(/\./g, '_')) || 'file' } function composeName(date: string, desc: string, orig: string) { const d = date || new Date().toISOString().slice(0, 10) const [Y, M, D] = d.split('-') const sDesc = sanitizeDesc(desc) const sStem = stemOf(orig) const ext = extOf(orig) || 'bin' return `${Y}.${M}.${D}.${sDesc}.${sStem}.${ext}` } function clampOneLevel(p: string) { if (!p) return '' return p.replace(/^\/+|\/+$/g, '').split('/')[0] || '' } function normalizeRows(listRaw: unknown[]): FileRow[] { return (Array.isArray(listRaw) ? listRaw : []).map((r: unknown) => { const row = (r && typeof r === 'object' ? r : {}) as RowLike return { name: String(row.name ?? row.Name ?? ''), path: String(row.path ?? row.Path ?? ''), is_dir: Boolean(row.is_dir ?? row.IsDir ?? row.isDir ?? false), size: Number(row.size ?? row.Size ?? 0), mtime: Number(row.mtime ?? row.Mtime ?? 0), } }) } const isVideoFile = (f: File) => f.type.startsWith('video/') || videoExt.has(extLower(f.name)) const isImageFile = (f: File) => f.type.startsWith('image/') || imageExt.has(extLower(f.name)) function isDetailedError(e: unknown): e is tus.DetailedError { return typeof e === 'object' && e !== null && ('originalRequest' in (e as any) || 'originalResponse' in (e as any)) } function isLikelyMobileUA(): boolean { if (typeof window === 'undefined') return false const ua = navigator.userAgent || '' const coarse = window.matchMedia && window.matchMedia('(pointer: coarse)').matches return coarse || /Mobi|Android|iPhone|iPad|iPod/i.test(ua) } function fmt(n: number) { if (n < 1024) return `${n} B` const units = ['KB', 'MB', 'GB', 'TB'] let idx = -1 do { n /= 1024 idx++ } while (n >= 1024 && idx < units.length - 1) return `${n.toFixed(1)} ${units[idx]}` } function createNoResumeStorage(): NoResumeStorage { return { async addUpload(_u: any) {}, async removeUpload(_u: any) {}, async listUploads() { return [] }, async findUploadsByFingerprint(_fp: string) { return [] }, } } async function createNoResumeFingerprint() { return `noresume-${Date.now()}-${Math.random().toString(36).slice(2)}` } const uploaderUtils = { sanitizeDesc, sanitizeFolderName, extOf, stemOf, composeName, clampOneLevel, normalizeRows, isVideoFile, isImageFile, isDetailedError, isLikelyMobileUA, fmt, createNoResumeStorage, createNoResumeFingerprint, } export default uploaderUtils