spb/drive Public
SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.
JavaScript 82.7%
CSS 10.6%
Nunjucks 3.6%
Shell 1.8%
SQL 1.3%
1/**2 * ─────────────────────────────────────────────3 * SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/preview/router.mjs8 * Purpose : MIME/extension → preview strategy dispatcher9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { OFFICE_EXTS } from './office.mjs';14import { archiveKind } from './archive.mjs';15import { EXT_LANG } from './code.mjs';1617const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'bmp', 'ico']);18const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'mpg', 'mpeg', 'wmv', 'flv', 'ts']);19const AUDIO_EXTS = new Set(['mp3', 'wav', 'flac', 'ogg', 'oga', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'aif']);20const FONT_EXTS = new Set(['ttf', 'otf', 'woff', 'woff2']);21const STRUCTURED_EXTS = new Set(['json', 'yaml', 'yml', 'toml', 'xml', 'plist']);22const MODEL3D_EXTS = new Set(['stl', 'glb', 'gltf', 'obj']);2324/** Lowercased extension without the dot ('' if none). */25export function extOf(name) {26 const base = String(name).toLowerCase();27 const special = base.endsWith('.tar.gz') ? 'tar.gz' : null;28 if (special) return special;29 const dot = base.lastIndexOf('.');30 return dot > 0 ? base.slice(dot + 1) : '';31}3233/**34 * Decide how a file previews.35 * @param {{name: string, mime?: string, size?: number}} node36 * @returns {{strategy: string, ext: string}}37 */38export function previewStrategy(node) {39 const name = node.name ?? '';40 const mime = node.mime ?? '';41 const ext = extOf(name);4243 if (ext === 'svg' || mime === 'image/svg+xml') return { strategy: 'svg', ext };44 if (ext === 'heic' || ext === 'heif') return { strategy: 'heic', ext };45 if (IMAGE_EXTS.has(ext) || mime.startsWith('image/')) return { strategy: 'image', ext };46 if (VIDEO_EXTS.has(ext) || mime.startsWith('video/')) return { strategy: 'video', ext };47 if (AUDIO_EXTS.has(ext) || mime.startsWith('audio/')) return { strategy: 'audio', ext };48 if (ext === 'pdf' || mime === 'application/pdf') return { strategy: 'pdf', ext };49 if (OFFICE_EXTS.has(ext)) return { strategy: 'office', ext };50 if (ext === 'md' || ext === 'markdown') return { strategy: 'markdown', ext };51 if (ext === 'csv' || ext === 'tsv') return { strategy: 'csv', ext };52 if (ext === 'ipynb') return { strategy: 'notebook', ext };53 if (STRUCTURED_EXTS.has(ext)) return { strategy: 'structured', ext };54 if (archiveKind(name)) return { strategy: 'archive', ext };55 if (FONT_EXTS.has(ext)) return { strategy: 'font', ext };56 if (ext === 'eml') return { strategy: 'email', ext };57 if (ext === 'epub') return { strategy: 'epub', ext };58 if (MODEL3D_EXTS.has(ext)) return { strategy: 'model3d', ext };59 if (ext === 'html' || ext === 'htm') return { strategy: 'code', ext };60 if (EXT_LANG[ext] || mime.startsWith('text/')) return { strategy: 'code', ext };61 return { strategy: 'fallback', ext };62}6364/** Which thumbnail pipeline (if any) applies to a file. */65export function thumbKind(node) {66 const { strategy } = previewStrategy(node);67 if (['image', 'heic'].includes(strategy)) return 'image';68 if (strategy === 'video') return 'video';69 if (strategy === 'pdf') return 'pdf';70 if (strategy === 'office') return 'office'; // via converted PDF when cached71 return null;72}7374/** Icon family for the crisp per-extension SVG icon set. */75export function iconFamily(node) {76 if (node.type === 'folder') return 'folder';77 const { strategy, ext } = previewStrategy(node);78 const map = {79 image: 'image', svg: 'image', heic: 'image', video: 'video', audio: 'audio',80 pdf: 'pdf', office: /x?ls|ods/.test(ext) ? 'sheet' : /pptx?|odp/.test(ext) ? 'slides' : 'doc',81 markdown: 'text', csv: 'sheet', notebook: 'code', structured: 'code',82 archive: 'archive', font: 'font', email: 'mail', epub: 'book',83 model3d: 'cube', code: 'code', fallback: 'file',84 };85 return map[strategy] ?? 'file';86}87