diff --git a/src/serve-static.ts b/src/serve-static.ts index 567b0be..22a5a5c 100644 --- a/src/serve-static.ts +++ b/src/serve-static.ts @@ -1,7 +1,7 @@ -import type { ReadStream } from 'fs' -import { createReadStream, existsSync, lstatSync } from 'fs' +import type { ReadStream, Stats } from 'fs' +import { createReadStream, lstatSync } from 'fs' import type { Context, MiddlewareHandler } from 'hono' -import { getFilePath } from 'hono/utils/filepath' +import { getFilePath, getFilePathWithoutDefaultDocument } from 'hono/utils/filepath' import { getMimeType } from 'hono/utils/mime' export type ServeStaticOptions = { @@ -33,6 +33,18 @@ const createStreamBody = (stream: ReadStream) => { return body } +const addCurrentDirPrefix = (path: string) => { + return `./${path}` +} + +const getStats = (path: string) => { + let stats: Stats | undefined + try { + stats = lstatSync(path) + } catch {} + return stats +} + export const serveStatic = (options: ServeStaticOptions = { root: '' }): MiddlewareHandler => { return async (c, next) => { // Do nothing if Response is already set @@ -41,19 +53,37 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }): Middlew } const filename = options.path ?? decodeURIComponent(c.req.path) - let path = getFilePath({ + + let path = getFilePathWithoutDefaultDocument({ filename: options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename, root: options.root, - defaultDocument: options.index ?? 'index.html', }) - if (!path) { + if (path) { + path = addCurrentDirPrefix(path) + } else { return next() } - path = `./${path}` + let stats = getStats(path) + + if (stats && stats.isDirectory()) { + path = getFilePath({ + filename: options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename, + root: options.root, + defaultDocument: options.index ?? 'index.html', + }) + + if (path) { + path = addCurrentDirPrefix(path) + } else { + return next() + } + + stats = getStats(path) + } - if (!existsSync(path)) { + if (!stats) { await options.onNotFound?.(path, c) return next() } @@ -63,8 +93,7 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }): Middlew c.header('Content-Type', mimeType) } - const stat = lstatSync(path) - const size = stat.size + const size = stats.size if (c.req.method == 'HEAD' || c.req.method == 'OPTIONS') { c.header('Content-Length', size.toString()) @@ -80,11 +109,11 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }): Middlew } c.header('Accept-Ranges', 'bytes') - c.header('Date', stat.birthtime.toUTCString()) + c.header('Date', stats.birthtime.toUTCString()) const parts = range.replace(/bytes=/, '').split('-', 2) const start = parts[0] ? parseInt(parts[0], 10) : 0 - let end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1 + let end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1 if (size < end - start + 1) { end = size - 1 } @@ -93,7 +122,7 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }): Middlew const stream = createReadStream(path, { start, end }) c.header('Content-Length', chunksize.toString()) - c.header('Content-Range', `bytes ${start}-${end}/${stat.size}`) + c.header('Content-Range', `bytes ${start}-${end}/${stats.size}`) return c.body(createStreamBody(stream), 206) } diff --git a/test/assets/static/extensionless b/test/assets/static/extensionless new file mode 100644 index 0000000..de2dba2 --- /dev/null +++ b/test/assets/static/extensionless @@ -0,0 +1 @@ +Extensionless \ No newline at end of file diff --git a/test/serve-static.test.ts b/test/serve-static.test.ts index b26a6e8..511ec77 100644 --- a/test/serve-static.test.ts +++ b/test/serve-static.test.ts @@ -134,4 +134,10 @@ describe('Serve Static Middleware', () => { const res = await request(server).get('/static/../secret.txt') expect(res.status).toBe(404) }) + + it('Should handle an extension less files', async () => { + const res = await request(server).get('/static/extensionless') + expect(res.status).toBe(200) + expect(res.text).toBe('Extensionless') + }) })