142 lines
4.0 KiB
TypeScript
142 lines
4.0 KiB
TypeScript
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<void>
|
|
removeUpload: (_u: any) => Promise<void>
|
|
listUploads: () => Promise<any[]>
|
|
findUploadsByFingerprint: (_fp: string) => Promise<any[]>
|
|
}
|
|
|
|
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
|