New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

bunshine

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bunshine - npm Package Compare versions

Comparing version 0.10.0 to 0.11.0

src/middleware/serveFiles/serveFiles.spec.ts

2

LICENSE.md

@@ -1,2 +0,2 @@

ISC License (ISC) Copyright 2023 Ken Snyder
ISC License (ISC) Copyright 2023-2024 Ken Snyder

@@ -3,0 +3,0 @@ Permission to use, copy, modify, and/or distribute this software for any purpose

{
"name": "bunshine",
"version": "0.10.0",
"version": "0.11.0",
"module": "server/server.ts",

@@ -13,2 +13,3 @@ "type": "module",

"lru-cache": "^10.1.0",
"ms": "^2.1.3",
"path-to-regexp": "^6.2.1"

@@ -18,2 +19,3 @@ },

"@types/eventsource": "^1.1.15",
"@types/ms": "^0.7.34",
"bun-types": "^1.0.21",

@@ -20,0 +22,0 @@ "eventsource": "^2.0.2",

@@ -1,6 +0,7 @@

<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=0.10.0" width="200" height="187" />
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=0.11.0" width="200" height="187" />
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=0.10.0)](https://npmjs.com/package/bunshine)
[![Dependencies](https://badgen.net/static/dependencies/2/green?v=0.10.0)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
[![ISC License](https://img.shields.io/npm/l/bunshine.svg?v=0.10.0)](https://opensource.org/licenses/ISC)
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=0.11.0)](https://npmjs.com/package/bunshine)
[![Dependencies](https://badgen.net/static/dependencies/3/green?v=0.11.0)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
![Test Coverage: 96%](https://badgen.net/static/test%20coverage/96%25/green?v=0.11.0)
[![ISC License](https://img.shields.io/npm/l/bunshine.svg?v=0.11.0)](https://opensource.org/licenses/ISC)

@@ -136,2 +137,4 @@ # Bunshine

See the [serveFiles](#serveFiles) section for more info.
## Writing middleware

@@ -562,2 +565,66 @@

How to respond to both GET and HEAD requests:
```ts
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.on(['HEAD', 'GET'], '/public/*', serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
```
How to alter the response:
```ts
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
const addFooHeader = async (_, next) => {
const response = await next();
response.headers.set('x-foo', 'bar');
return response;
};
app.get('/public/*', addFooHeader, serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
```
serveFiles accepts an optional second parameter for options:
```ts
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.get(
'/public/*',
serveFiles(`${import.meta.dir}/public`, {
extensions: ['html', 'css', 'js', 'png', 'jpg', 'gif', 'svg', 'ico'],
index: true,
})
);
app.listen({ port: 3100 });
```
All options for serveFiles:
| Option | Default | Description |
| ------------ | ----------- | ----------------------------------------------------------------------------------------- |
| acceptRanges | `true` | If true, accept ranged byte requests |
| dotfiles | `"ignore"` | How to handle dotfiles; allow=>serve normally, deny=>return 403, ignore=>run next handler |
| etag | N/A | Not yet implemented |
| extensions | `[]` | If given, a list of file extensions to allow |
| fallthrough | `true` | If false, issue a 404 when a file is not found, otherwise proceed to next handler |
| immutable | `false` | If true, add immutable directive to Cache-Control header; must also specify maxAge |
| index | `[]` | If given, a list of filenames (e.g. index.html) to look for when path is a folder |
| lastModified | `true` | If true, set the Last-Modified header |
| maxAge | `undefined` | If given, add a Cache-Control header with max-age† |
† _A number in milliseconds or [ms](https://www.npmjs.com/package/ms) compatible expression such as '30m' or '1y'._
### cors

@@ -564,0 +631,0 @@

@@ -51,3 +51,3 @@ import type { Server } from 'bun';

const text = await file.text();
expect(resp.status).toBe(206);
expect(resp.status).toBe(200);
expect(text).toBe('<h1>');

@@ -86,3 +86,3 @@ });

expect(resp).toBeInstanceOf(Response);
expect(resp.status).toBe(206);
expect(resp.status).toBe(200);
expect(await resp.text()).toBe('<h1>');

@@ -89,0 +89,0 @@ });

@@ -32,4 +32,3 @@ import { BunFile } from 'bun';

filenameOrBunFile: string | BunFile,
fileOptions: FileResponseOptions = {},
responseInit: ResponseInit = {}
fileOptions: FileResponseOptions = {}
) => {

@@ -44,34 +43,9 @@ let file =

}
let resp: Response;
const rangeMatch = fileOptions.range?.match(/^bytes=(\d*)-(\d*)$/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1]) || 0;
let end = parseInt(rangeMatch[2]);
if (isNaN(end)) {
// Initial request: some browsers use "Range: bytes=0-"
end = Math.min(
start + (fileOptions.chunkSize || 3 * 1024 ** 2),
totalFileSize - 1
);
}
if (end > totalFileSize - 1) {
return new Response('416 Range not satisfiable', { status: 416 });
}
// Bun has a bug when setting content-length and content-range automatically
// so convert file to buffer
let buffer = await file.arrayBuffer();
// the range is less than the entire file
if (end - 1 < totalFileSize) {
buffer = buffer.slice(start, end + 1);
}
resp = new Response(buffer, { ...responseInit, status: 206 });
if (!resp.headers.has('Content-Type')) {
resp.headers.set('Content-Type', 'application/octet-stream');
}
resp.headers.set('Content-Length', String(buffer.byteLength));
resp.headers.set('Content-Range', `bytes ${start}-${end}/${totalFileSize}`);
} else {
// Bun will automatically set content-type and length
resp = new Response(file);
}
const resp = await buildFileResponse({
file,
acceptRanges: true,
chunkSize: fileOptions.chunkSize,
rangeHeader: fileOptions.range,
method: 'GET',
});
// tell the client that we are capable of handling range requests

@@ -176,2 +150,3 @@ resp.headers.set('Accept-Ranges', 'bytes');

headers.set('Connection', 'keep-alive');
// @ts-ignore
return new Response(stream, { ...init, headers });

@@ -184,2 +159,3 @@ };

...init,
// @ts-ignore
headers: {

@@ -192,1 +168,55 @@ ...(init.headers || {}),

}
export async function buildFileResponse({
file,
acceptRanges,
chunkSize,
rangeHeader,
method,
}: {
file: BunFile;
acceptRanges: boolean;
chunkSize?: number;
rangeHeader?: string | null;
method: string;
}) {
let response: Response;
const rangeMatch = String(rangeHeader).match(/^bytes=(\d*)-(\d*)$/);
if (acceptRanges && rangeMatch) {
const totalFileSize = file.size;
const start = parseInt(rangeMatch[1]) || 0;
let end = parseInt(rangeMatch[2]);
if (isNaN(end)) {
// Initial request: some browsers use "Range: bytes=0-"
end = Math.min(start + (chunkSize || 3 * 1024 ** 2), totalFileSize - 1);
}
if (end > totalFileSize - 1) {
return new Response('416 Range not satisfiable', { status: 416 });
}
// Bun has a bug when setting content-length and content-range automatically
// so convert file to buffer
let buffer = await file.arrayBuffer();
let status = 206;
// the range is less than the entire file
if (end - 1 < totalFileSize) {
buffer = buffer.slice(start, end + 1);
status = 200;
}
response = new Response(buffer, { status });
if (!response.headers.has('Content-Type')) {
response.headers.set('Content-Type', 'application/octet-stream');
}
response.headers.set('Content-Length', String(buffer.byteLength));
response.headers.set(
'Content-Range',
`bytes ${start}-${end}/${totalFileSize}`
);
} else {
// Bun will automatically set content-type and length
response = new Response(file, { status: method === 'HEAD' ? 204 : 200 });
}
if (acceptRanges) {
response.headers.set('Accept-Ranges', 'bytes');
}
return response;
}

@@ -0,3 +1,5 @@

import ms from 'ms';
import path from 'path';
import type { Middleware } from '../../HttpRouter/HttpRouter.ts';
import { buildFileResponse } from '../../HttpRouter/responseFactories.ts';

@@ -8,3 +10,2 @@ // see https://expressjs.com/en/4x/api.html#express.static

acceptRanges?: boolean;
cacheControl?: boolean;
dotfiles?: 'allow' | 'deny' | 'ignore';

@@ -15,7 +16,5 @@ etag?: boolean;

immutable?: boolean;
index?: boolean | string | string[];
index?: string[];
lastModified?: boolean;
maxAge?: number | string;
redirect?: boolean;
setHeaders?: Middleware;
};

@@ -27,18 +26,99 @@

acceptRanges = true,
cacheControl = true,
dotfiles = 'ignore',
etag = true,
etag = true, // Not yet implemented
extensions = [],
fallthrough = true,
immutable = false,
index = false,
index = [],
lastModified = true,
maxAge = undefined,
redirect = true,
setHeaders,
}: StaticOptions = {}
): Middleware {
return c => {
return c.file(path.join(directory, c.params[0] || c.url.pathname));
const cacheControlHeader =
maxAge === undefined ? null : getCacheControl(maxAge, immutable);
return async c => {
const filename = c.params[0] || c.url.pathname;
if (filename.startsWith('.')) {
if (dotfiles === 'ignore') {
// fall through to next handler
return;
}
if (dotfiles === 'deny') {
return new Response('403 Forbidden', { status: 403 });
}
}
if (extensions.length > 0) {
const ext = path.extname(filename).slice(1);
if (!extensions.includes(ext)) {
// fall through to next handler
return;
}
}
// get file path
const filePath = path.join(directory, filename);
// init file
let file = Bun.file(filePath);
// handle existence
let exists = await file.exists();
// console.log('----------=========------- exists?', { exists, filePath });
// handle index files
if (!exists && index.length > 0) {
// try to find index file such as index.html or index.js
for (const indexFile of index) {
const indexFilePath = path.join(filePath, indexFile);
file = Bun.file(indexFilePath);
exists = await file.exists();
if (exists) {
break;
}
}
}
if (!exists) {
if (fallthrough) {
return;
}
return new Response('404 Not Found', { status: 404 });
}
// get base response
const response = await buildFileResponse({
file,
acceptRanges,
chunkSize: 0,
rangeHeader: c.request.headers.get('range'),
method: c.request.method,
});
// add current date
response.headers.set('Date', new Date().toUTCString());
// add last modified
if (lastModified) {
response.headers.set(
'Last-Modified',
new Date(file.lastModified).toUTCString()
);
}
// add Cache-Control header
if (cacheControlHeader) {
response.headers.set('Cache-Control', cacheControlHeader);
}
return response;
};
}
function getCacheControl(maxAge: string | number, immutable: boolean) {
let cacheControl = 'public, ';
if (typeof maxAge === 'string') {
const milliseconds = ms(maxAge);
if (milliseconds === undefined) {
throw new Error(`Invalid maxAge: ${maxAge}`);
}
maxAge = milliseconds;
}
if (typeof maxAge === 'number' && maxAge >= 0) {
const seconds = Math.floor(maxAge / 1000);
cacheControl += `max-age=${seconds}`;
}
if (immutable) {
cacheControl += ', immutable';
}
return cacheControl;
}

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc