UNPKG

bunshine

Version:

A Bun HTTP & WebSocket server that is a little ray of sunshine.

178 lines (162 loc) 4.79 kB
import { BunFile } from 'bun'; import { fileTypeFromBuffer } from 'file-type'; import fs from 'fs/promises'; import { LRUCache } from 'lru-cache'; import path from 'node:path'; export type FileLike = string | Blob | Uint8Array | ArrayBuffer; const defaultMimeType = 'application/octet-stream'; const detectMimeChunkSize = 4100; // from file-type reasonableDetectionSizeInBytes const mimeCache = new LRUCache<string, string>({ max: 10_000 }); export function isFileLike(file: any): file is FileLike { return ( typeof file === 'string' || file instanceof Blob || file instanceof Uint8Array || file instanceof ArrayBuffer ); } function isBunFile(file: FileLike): file is BunFile { // @ts-expect-error - Only way I know to distinguish Blob from BunFile is to check .exists return file instanceof Blob && typeof file.exists === 'function'; } function readChunk(path: string, start: number, end: number) { const bunFile = Bun.file(path); const blob = bunFile.slice(start, end); return blob.arrayBuffer(); } export async function getBufferMime(arrayBuffer: ArrayBuffer | Uint8Array) { const type = await fileTypeFromBuffer(arrayBuffer); return type?.mime || defaultMimeType; } export async function getFileMime( file: FileLike, maybeEntireFile?: ArrayBuffer | Uint8Array ) { const filename = getFileBaseName(file); if (filename) { const ext = path.extname(filename).slice(1); const mime = getMimeByExt(ext); if (mime) { // string or BunFile with unambiguous extension return mime; } } if (typeof file === 'string') { return ( (maybeEntireFile ? await getBufferMime(maybeEntireFile) : await getChunkMime(file)) || defaultMimeType ); } if (file instanceof Blob) { const mime = await getBufferMime(await file.arrayBuffer()); return mime || file.type || defaultMimeType; } return getBufferMime(file); } export async function getChunkMime(path: string) { const lastModified = Bun.file(path).lastModified; const key = `${path}-${lastModified}`; if (!mimeCache.has(key)) { const chunk = await readChunk(path, 0, detectMimeChunkSize); const mime = chunk ? await getBufferMime(chunk) : ''; mimeCache.set(key, mime); } return mimeCache.get(key); } export function getFileBaseName(file: FileLike) { if (typeof file === 'string') { return path.basename(file); } else if (isBunFile(file) && file.name) { return path.basename(file.name); } else { return ''; } } const unambiguiousExtensions = { cjs: 'text/javascript', css: 'text/css', html: 'text/html', js: 'text/javascript', json: 'application/json', map: 'application/json', md: 'text/markdown', mjs: 'text/javascript', tif: 'image/tiff', tiff: 'image/tiff', txt: 'text/plain', wasm: 'application/wasm', webmanifest: 'application/json', xml: 'text/xml', }; export function getMimeByExt(extension: string) { return unambiguiousExtensions[extension.toLowerCase()]; } export async function getFileStats(file: FileLike) { if (typeof file === 'string') { try { const stat = await fs.stat(file); return { size: stat.size, lastModified: stat.mtime, doesExist: stat.isFile(), }; } catch (e) { return { size: null, lastModified: null, doesExist: false, }; } } if (isBunFile(file)) { return { size: file.size, lastModified: file.lastModified ? new Date(file.lastModified) : null, doesExist: await file.exists(), }; } // Blob, Uint8Array, ArrayBuffer all definitely "exist" return { size: getObjectSize(file), lastModified: null, doesExist: true, }; } export function getObjectSize(obj: BunFile | Blob | Uint8Array | ArrayBuffer) { if (obj instanceof Blob) { // BunFile and Blob return obj.size; } return obj.byteLength || 0; } export async function getFileChunk( file: FileLike, start: number, length: number ) { if (typeof file === 'string') { return readChunk(file, start, start + length); } const buffer = file instanceof Blob ? await file.arrayBuffer() : file; return buffer.slice(start, start + length); } export async function getFileFull(file: FileLike) { if (typeof file === 'string') { // Read the file as a Buffer const buffer = await fs.readFile(file); // Convert Buffer to Uint8Array return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); } if (file instanceof Blob) { return await file.arrayBuffer(); } if (Buffer.isBuffer(file)) { return new Uint8Array(file.buffer, file.byteOffset, file.byteLength); } if (file instanceof Uint8Array) { // Return Uint8Array as is return file; } return new Uint8Array(); }