Support range requests

This commit is contained in:
hzrd149 2025-07-19 18:17:41 -05:00
parent 6f8b0038c3
commit d45cf57ec9
6 changed files with 60 additions and 6 deletions

View File

@ -0,0 +1,5 @@
---
"nsite-gateway": minor
---
Support range requests

View File

@ -28,6 +28,7 @@
"keyv": "^5.4.0",
"koa": "^2.16.1",
"koa-morgan": "^1.0.1",
"koa-range": "^0.3.0",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"mime": "^4.0.7",
@ -46,6 +47,7 @@
"@types/follow-redirects": "^1.14.4",
"@types/koa": "^2.15.0",
"@types/koa-morgan": "^1.0.8",
"@types/koa-range": "^0.3.5",
"@types/koa-send": "^4.1.6",
"@types/koa-static": "^4.0.4",
"@types/koa__cors": "^5.0.0",

26
pnpm-lock.yaml generated
View File

@ -38,6 +38,9 @@ importers:
koa-morgan:
specifier: ^1.0.1
version: 1.0.1
koa-range:
specifier: ^0.3.0
version: 0.3.0
koa-send:
specifier: ^5.0.1
version: 5.0.1
@ -87,6 +90,9 @@ importers:
'@types/koa-morgan':
specifier: ^1.0.8
version: 1.0.8
'@types/koa-range':
specifier: ^0.3.5
version: 0.3.5
'@types/koa-send':
specifier: ^4.1.6
version: 4.1.6
@ -639,6 +645,9 @@ packages:
'@types/koa-morgan@1.0.8':
resolution: {integrity: sha512-2GredUi+iA3V0XrbzdsOAYgwj4F6+FnN+f5YjoKjessIE2lrMkqnc06YQQnzbMG75hRsXjyD+p6d5vlI70s1vg==}
'@types/koa-range@0.3.5':
resolution: {integrity: sha512-DvbLgWVctu3k0dnuQsb2If7ROdYvUK+oGOJTxFviQjfrMjaewZYM1IPYoH1yZ2zhcpD+hKzBiGi5DMOj0kLAQg==}
'@types/koa-send@4.1.6':
resolution: {integrity: sha512-vgnNGoOJkx7FrF0Jl6rbK1f8bBecqAchKpXtKuXzqIEdXTDO6dsSTjr+eZ5m7ltSjH4K/E7auNJEQCAd0McUPA==}
@ -1287,6 +1296,10 @@ packages:
koa-morgan@1.0.1:
resolution: {integrity: sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==}
koa-range@0.3.0:
resolution: {integrity: sha512-Ich3pCz6RhtbajYXRWjIl6O5wtrLs6kE3nkXc9XmaWe+MysJyZO7K4L3oce1Jpg/iMgCbj+5UCiMm/rqVtcDIg==}
engines: {node: '>=7'}
koa-send@5.0.1:
resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==}
engines: {node: '>= 8'}
@ -1751,6 +1764,9 @@ packages:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
stream-slice@0.1.2:
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@ -2415,6 +2431,10 @@ snapshots:
'@types/koa': 2.15.0
'@types/morgan': 1.9.10
'@types/koa-range@0.3.5':
dependencies:
'@types/koa': 2.15.0
'@types/koa-send@4.1.6':
dependencies:
'@types/koa': 2.15.0
@ -3130,6 +3150,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
koa-range@0.3.0:
dependencies:
stream-slice: 0.1.2
koa-send@5.0.1:
dependencies:
debug: 4.4.1(supports-color@5.5.0)
@ -3671,6 +3695,8 @@ snapshots:
statuses@1.5.0: {}
stream-slice@0.1.2: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0

View File

@ -29,8 +29,12 @@ export async function findBlobURLs(sha256: string, servers: string[]): Promise<s
return filtered;
}
/** Downloads a file from multiple servers */
export async function streamBlob(sha256: string, servers: string[]): Promise<IncomingMessage | undefined> {
/** Downloads a file from multiple servers with optional range support */
export async function streamBlob(
sha256: string,
servers: string[],
headers?: Record<string, string>,
): Promise<IncomingMessage | undefined> {
if (servers.length === 0) return undefined;
// First find all available URLs
@ -49,7 +53,7 @@ export async function streamBlob(sha256: string, servers: string[]): Promise<Inc
}, 10_000);
const url = new URL(urlString);
const response = await makeRequestWithAbort(url, controller);
const response = await makeRequestWithAbort(url, controller, headers);
res = response;
clearTimeout(timeout);
@ -58,6 +62,7 @@ export async function streamBlob(sha256: string, servers: string[]): Promise<Inc
const size = response.headers["content-length"];
if (size && parseInt(size) > MAX_FILE_SIZE) throw new Error("File too large");
// Accept both 200 (full content) and 206 (partial content) status codes
if (response.statusCode >= 200 && response.statusCode < 300) return response;
} catch (error) {
if (res) res.resume();

View File

@ -4,7 +4,7 @@ const { http, https } = followRedirects;
import agent from "../proxy.js";
export function makeRequestWithAbort(url: URL, controller: AbortController) {
export function makeRequestWithAbort(url: URL, controller: AbortController, headers?: Record<string, string>) {
return new Promise<IncomingMessage>((res, rej) => {
controller.signal.addEventListener("abort", () => rej(new Error("Aborted")));
@ -13,6 +13,7 @@ export function makeRequestWithAbort(url: URL, controller: AbortController) {
{
signal: controller.signal,
agent,
headers,
},
(response) => {
res(response);

View File

@ -8,6 +8,7 @@ import fs from "node:fs";
import { fileURLToPath } from "node:url";
import mime from "mime";
import morgan from "koa-morgan";
import range from "koa-range";
import { npubEncode } from "nostr-tools/nip19";
import { nip19 } from "nostr-tools";
@ -38,6 +39,9 @@ morgan.token("host", (req) => req.headers.host ?? "");
app.use(morgan(":method :host:url :status :response-time ms - :res[content-length]"));
// add range request support
app.use(range);
// set CORS headers
app.use(
cors({
@ -114,7 +118,13 @@ app.use(async (ctx, next) => {
if (servers.length === 0) throw new Error("Failed to find blossom servers");
try {
const res = await streamBlob(event.sha256, servers);
// Prepare headers for range requests
const requestHeaders: Record<string, string> = {};
if (ctx.headers.range) {
requestHeaders.range = ctx.headers.range;
}
const res = await streamBlob(event.sha256, servers, requestHeaders);
if (!res) {
ctx.status = 502;
ctx.body = `Failed to find blob\npath: ${event.path}\nsha256: ${event.sha256}\nservers: ${servers.join(", ")}`;
@ -128,6 +138,10 @@ app.use(async (ctx, next) => {
// pass headers along
if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]);
// handle range response headers
if (res.headers["accept-ranges"]) ctx.set("accept-ranges", res.headers["accept-ranges"]);
if (res.headers["content-range"]) ctx.set("content-range", res.headers["content-range"]);
// set Onion-Location header
if (ONION_HOST) {
const url = new URL(ONION_HOST);
@ -140,7 +154,8 @@ app.use(async (ctx, next) => {
ctx.set("Cache-Control", "public, max-age=3600");
ctx.set("Last-Modified", res.headers["last-modified"] || new Date(event.created_at * 1000).toUTCString());
ctx.status = 200;
// set appropriate status code (206 for partial content, 200 for full content)
ctx.status = res.statusCode === 206 ? 206 : 200;
ctx.body = res;
return;
} catch (error) {