Created
September 6, 2025 18:19
-
-
Save corn-snake/4c6486fce344df7e957d6367e87fd5f7 to your computer and use it in GitHub Desktop.
a native implementation of @oak/oak's send() function for NodeJS. just changed the deno function calls to node fs ones, when deployed you should be able to just import this normally
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /////////////////////////// | |
| // original credit to: the OakJS team and contributors | |
| // their license is as follows: | |
| /* | |
| MIT License | |
| Copyright (c) 2018-2025 the oak authors | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE. | |
| */ | |
| import { createHttpError, HttpError, Request, Response, Status } from "@oak/oak" | |
| import { Stats } from "fs"; | |
| import { open, readFile, stat } from "fs/promises"; | |
| import { eTag } from "@jsr/std__http/etag"; | |
| import { range } from "@oak/commons/range"; | |
| import { contentType } from "@jsr/std__media-types/content-type"; | |
| import { basename, extname, isAbsolute, join, normalize, parse, SEPARATOR } from "@jsr/std__path"; | |
| // npx jsr install @std/http @std/media-types @std/path | |
| // npm i @oak/oak | |
| // adapt to your needs ofc | |
| const MAXBUFFER_DEFAULT = 1_048_576, // 1MiB; | |
| UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; | |
| const isHidden = (path) => { | |
| const pathArr = path.split("/"); | |
| for (const segment of pathArr) { | |
| if (segment[0] === "." && segment !== "." && segment !== "..") { | |
| return true; | |
| } | |
| return false; | |
| } | |
| } | |
| const exists = async (path) => { | |
| try { | |
| return (await stat(path)).isFile(); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| function decode(pathname = "") { | |
| try { | |
| return decodeURI(pathname); | |
| } catch (err) { | |
| if (err instanceof URIError) { | |
| throw createHttpError(400, "Failed to decode URI", { expose: false }); | |
| } | |
| throw err; | |
| } | |
| } | |
| const getEntity = async ( | |
| path = "", | |
| mtime = 0, | |
| stats = Stats, | |
| maxbuffer = MAXBUFFER_DEFAULT, | |
| response = Response, | |
| ) => { | |
| let body; | |
| let entity; | |
| const fileInfo = { mtime: new Date(mtime), size: stats.size }; | |
| if (stats.size < maxbuffer) { | |
| const buffer = await readFile(path); | |
| body = entity = buffer; | |
| } else { | |
| const file = await (await open(path, "r")).readFile(); | |
| response.addResource(file); | |
| body = file; | |
| entity = fileInfo; | |
| } | |
| return [body, entity, fileInfo]; | |
| } | |
| const resolvePath = (rootPath, relativePath) => { | |
| let path = relativePath; | |
| let root = rootPath; | |
| // root is optional, similar to root.resolve | |
| if (relativePath === undefined) { | |
| path = rootPath; | |
| root = "."; | |
| } | |
| if (path == null) { | |
| throw new TypeError("Argument relativePath is required."); | |
| } | |
| // containing NULL bytes is malicious | |
| if (path.includes("\0")) { | |
| throw createHttpError(400, "Malicious Path"); | |
| } | |
| // path should never be absolute | |
| if (isAbsolute(path)) { | |
| throw createHttpError(400, "Malicious Path"); | |
| } | |
| // path outside root | |
| if (UP_PATH_REGEXP.test(normalize(`.${SEPARATOR}${path}`))) { | |
| throw createHttpError(403); | |
| } | |
| // join the relative path | |
| return normalize(join(root, path)); | |
| } | |
| const send = async ({ request = Request, response = Response }, path = "", { | |
| brotli = true, | |
| gzip = true, | |
| contentTypes = {}, | |
| extensions = [], | |
| format = true, | |
| hidden = false, | |
| immutable = false, | |
| index = "", | |
| maxage = 0, | |
| root = "", | |
| maxbuffer = MAXBUFFER_DEFAULT | |
| } | |
| ) => { | |
| const trailingSlash = path[path.length - 1] === "/"; | |
| path = decode(path.substring(parse(path).root.length)); | |
| if (index && trailingSlash) { | |
| path += index; | |
| } | |
| if (!hidden && isHidden(path)) { | |
| throw createHttpError(403); | |
| } | |
| path = resolvePath(root, path); | |
| let encodingExt = ""; | |
| if ( | |
| brotli && | |
| request.acceptsEncodings("br", "identity") === "br" && | |
| (await exists(`${path}.br`)) | |
| ) { | |
| path = `${path}.br`; | |
| response.headers.set("Content-Encoding", "br"); | |
| response.headers.delete("Content-Length"); | |
| encodingExt = ".br"; | |
| } else if ( | |
| gzip && | |
| request.acceptsEncodings("gzip", "identity") === "gzip" && | |
| (await exists(`${path}.gz`)) | |
| ) { | |
| path = `${path}.gz`; | |
| response.headers.set("Content-Encoding", "gzip"); | |
| response.headers.delete("Content-Length"); | |
| encodingExt = ".gz"; | |
| } | |
| if (extensions && !/\.[^/]*$/.exec(path)) { | |
| for (let ext of extensions) { | |
| if (!/^\./.exec(ext)) { | |
| ext = `.${ext}`; | |
| } | |
| if (await exists(`${path}${ext}`)) { | |
| path += ext; | |
| break; | |
| } | |
| } | |
| } | |
| let stats; | |
| try { | |
| stats = await stat(path); | |
| if (stats.isDirectory()) { | |
| if (format && index) { | |
| path += `/${index}`; | |
| stats = await stat(path); | |
| } else { | |
| return; | |
| } | |
| } | |
| } catch (err) { | |
| if (err.code === 'ENOENT') { | |
| throw createHttpError(404, "File not found."); | |
| } | |
| throw createHttpError( | |
| 500, | |
| err instanceof Error ? err.message : "[non-error thrown]", | |
| ); | |
| } | |
| let mtime = null; | |
| if (response.headers.has("Last-Modified")) { | |
| mtime = new Date(response.headers.get("Last-Modified")).getTime(); | |
| } else if (stats.mtime) { | |
| // Round down to second because it's the precision of the UTC string. | |
| mtime = stats.mtime.getTime(); | |
| mtime -= mtime % 1000; | |
| response.headers.set("Last-Modified", new Date(mtime).toUTCString()); | |
| } | |
| if (!response.headers.has("Cache-Control")) { | |
| const directives = [`max-age=${(maxage / 1000) | 0}`]; | |
| if (immutable) { | |
| directives.push("immutable"); | |
| } | |
| response.headers.set("Cache-Control", directives.join(",")); | |
| } | |
| if (!response.type) { | |
| response.type = encodingExt !== "" | |
| ? extname(basename(path, encodingExt)) | |
| : contentTypes[extname(path)] ?? extname(path); | |
| } | |
| let entity = null; | |
| let body = null; | |
| let fileInfo = null; | |
| if (request.headers.has("If-None-Match") && mtime) { | |
| [body, entity, fileInfo] = await getEntity( | |
| path, | |
| mtime, | |
| stats, | |
| maxbuffer, | |
| response, | |
| ); | |
| const etag = await eTag(entity); | |
| if ( | |
| etag && (!ifNoneMatch(request.headers.get("If-None-Match"), etag)) | |
| ) { | |
| response.headers.set("ETag", etag); | |
| response.status = 304; | |
| return path; | |
| } | |
| } | |
| if (request.headers.has("If-Modified-Since") && mtime) { | |
| const ifModifiedSince = new Date(request.headers.get("If-Modified-Since")); | |
| if (ifModifiedSince.getTime() >= mtime) { | |
| response.status = 304; | |
| return path; | |
| } | |
| } | |
| if (!body || !entity || !fileInfo) { | |
| [body, entity, fileInfo] = await getEntity( | |
| path, | |
| mtime ?? 0, | |
| stats, | |
| maxbuffer, | |
| response, | |
| ); | |
| } | |
| let returnRanges, | |
| size; | |
| if (request.source && body && entity) { | |
| const { ok, ranges } = ArrayBuffer.isView(body) | |
| ? await range(request.source, body, fileInfo) | |
| : await range(request.source, fileInfo); | |
| if (ok && ranges) { | |
| size = ArrayBuffer.isView(entity) ? entity.byteLength : entity.size; | |
| returnRanges = ranges; | |
| } else if (!ok) { | |
| response.status = Status.RequestedRangeNotSatisfiable; | |
| } | |
| } | |
| if (!response.headers.has("ETag")) { | |
| const etag = await eTag(entity); | |
| if (etag) { | |
| response.headers.set("ETag", etag); | |
| } | |
| } | |
| if (returnRanges && size) { | |
| response.with( | |
| responseRange(body, size, returnRanges, { headers: response.headers }, { | |
| type: response.type ? contentType(response.type) : "", | |
| }), | |
| ); | |
| } else { | |
| response.body = body; | |
| } | |
| return path; | |
| } | |
| export default send; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment