'use strict'; const path = require('path'); // Types a browser can show inline. The type is derived from the file name on the // server, the type the client claimed at upload time is never trusted. // Text is always served as text/plain, html is deliberately not listed. const MEDIA = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', bmp: 'image/bmp', svg: 'image/svg+xml', pdf: 'application/pdf', mp4: 'video/mp4', m4v: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', ogv: 'video/ogg', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4', flac: 'audio/flac', }; const TEXT = ['txt', 'md', 'log', 'json', 'xml', 'csv', 'tsv', 'yml', 'yaml', 'ini', 'conf', 'cfg', 'js', 'mjs', 'ts', 'jsx', 'tsx', 'vue', 'css', 'scss', 'less', 'sh', 'bat', 'cmd', 'ps1', 'py', 'java', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'sql', 'lua', 'pug', 'go', 'rs', 'rb']; const PREVIEW = { ...MEDIA }; TEXT.forEach(ext => { PREVIEW[ext] = 'text/plain; charset=utf-8'; }); function extname(name) { return path.extname(String(name || '')).slice(1).toLowerCase(); } /** Content-Type for inline preview or null when the file has to be downloaded. */ function previewMime(name) { return PREVIEW[extname(name)] || null; } /** 'image' | 'pdf' | 'video' | 'audio' | 'text' | null */ function previewKind(name) { const mime = previewMime(name); if (!mime) return null; if (mime === 'application/pdf') return 'pdf'; return mime.split('/')[0]; } /** Detect the image type of a thumbnail by its signature. */ function sniffImage(buf) { if (!Buffer.isBuffer(buf) || buf.length < 12) return null; if (buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'image/png'; if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return 'image/jpeg'; if (buf.subarray(0, 4).toString('latin1') === 'RIFF' && buf.subarray(8, 12).toString('latin1') === 'WEBP') return 'image/webp'; return null; } module.exports = { previewMime, previewKind, sniffImage };