diff --git a/src/send-file.ts b/src/send-file.ts new file mode 100644 index 0000000..fbf1e20 --- /dev/null +++ b/src/send-file.ts @@ -0,0 +1,65 @@ +import type { Context, Env } from 'hono' +import type { StatusCode } from 'hono/utils/http-status' +import { getMimeType } from 'hono/utils/mime' +import type { Stats } from 'node:fs' +import { createReadStream } from 'node:fs' +import { createStreamBody, getStats } from './serve-static' + +export const sendFile = async ( + c: Context, + path: string, + options: { + onNotFound?: (path: string, c: Context) => Response | Promise + emptyBody?: { + [method: string]: StatusCode + } + } = { + emptyBody: { + HEAD: 204, + OPTIONS: 204, + }, + } +): Promise => { + const stats: Stats | undefined = getStats(path) + if (!stats?.isFile()) { + return options.onNotFound ? options.onNotFound(path, c) : c.notFound() + } + + const mimeType: string = getMimeType(path) || 'application/octet-stream' + c.header('Content-Type', mimeType) + + const size = stats.size + + const { emptyBody } = options + for (const [k, v] of Object.entries(emptyBody ?? {})) { + if (k === c.req.method) { + c.header('Content-Length', size.toString()) + return c.body(null, v) + } + } + + const range = c.req.header('range') || '' + + if (!range) { + c.header('Content-Length', size.toString()) + return c.body(createStreamBody(createReadStream(path)), 200) + } + + c.header('Accept-Ranges', 'bytes') + c.header('Date', stats.birthtime.toUTCString()) + + const parts = range.replace(/bytes=/, '').split('-', 2) + const start = parts[0] ? Number.parseInt(parts[0], 10) : 0 + let end = parts[1] ? Number.parseInt(parts[1], 10) : stats.size - 1 + if (size < end - start + 1) { + end = size - 1 + } + + const chunksize = end - start + 1 + const stream = createReadStream(path, { start, end }) + + c.header('Content-Length', chunksize.toString()) + c.header('Content-Range', `bytes ${start}-${end}/${stats.size}`) + + return c.body(createStreamBody(stream), 206) +} diff --git a/src/serve-static.ts b/src/serve-static.ts index d058fd9..cf4b354 100644 --- a/src/serve-static.ts +++ b/src/serve-static.ts @@ -26,7 +26,7 @@ const ENCODINGS = { } as const const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS) as (keyof typeof ENCODINGS)[] -const createStreamBody = (stream: ReadStream) => { +export const createStreamBody = (stream: ReadStream) => { const body = new ReadableStream({ start(controller) { stream.on('data', (chunk) => { @@ -48,7 +48,7 @@ const addCurrentDirPrefix = (path: string) => { return `./${path}` } -const getStats = (path: string) => { +export const getStats = (path: string) => { let stats: Stats | undefined try { stats = lstatSync(path)