mirror of
https://github.com/hzrd149/nsite-gateway.git
synced 2025-06-23 20:05:03 +00:00
improve site caching
This commit is contained in:
parent
a262539366
commit
34d373e532
4
.env
4
.env
@ -1,2 +1,4 @@
|
|||||||
NOSTR_RELAYS=wss://nostrue.com
|
NOSTR_RELAYS=wss://nostrue.com,wss://nos.lol,wss://relay.damus.io
|
||||||
BLOSSOM_SERVERS=https://cdn.hzrd149.com
|
BLOSSOM_SERVERS=https://cdn.hzrd149.com
|
||||||
|
|
||||||
|
MAX_FILE_SIZE='2 MB'
|
||||||
|
8
.env.example
Normal file
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# A list of nostr relays to search
|
||||||
|
NOSTR_RELAYS=wss://nostrue.com,wss://nos.lol,wss://relay.damus.io
|
||||||
|
|
||||||
|
# A list of fallback blossom servers
|
||||||
|
BLOSSOM_SERVERS=https://cdn.satellite.earth
|
||||||
|
|
||||||
|
# The max file size to serve
|
||||||
|
MAX_FILE_SIZE='2 MB'
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
.env
|
.env
|
||||||
|
data
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"admin/dist"
|
"admin/dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@keyv/sqlite": "^4.0.1",
|
||||||
"@koa/cors": "^5.0.0",
|
"@koa/cors": "^5.0.0",
|
||||||
"@koa/router": "^12.0.1",
|
"@koa/router": "^12.0.1",
|
||||||
"@nostr-dev-kit/ndk": "^2.10.0",
|
"@nostr-dev-kit/ndk": "^2.10.0",
|
||||||
@ -29,14 +30,17 @@
|
|||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"http-errors": "1",
|
"http-errors": "1",
|
||||||
|
"keyv": "^5.0.1",
|
||||||
"koa": "^2.15.3",
|
"koa": "^2.15.3",
|
||||||
"koa-mount": "^4.0.0",
|
"koa-mount": "^4.0.0",
|
||||||
|
"koa-send": "^5.0.1",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"socks-proxy-agent": "^8.0.4",
|
"socks-proxy-agent": "^8.0.4",
|
||||||
"websocket-polyfill": "^1.0.0",
|
"websocket-polyfill": "^1.0.0",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0",
|
||||||
|
"xbytes": "^1.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.27.1",
|
"@changesets/cli": "^2.27.1",
|
||||||
@ -49,6 +53,7 @@
|
|||||||
"@types/koa": "^2.14.0",
|
"@types/koa": "^2.14.0",
|
||||||
"@types/koa-basic-auth": "^2.0.6",
|
"@types/koa-basic-auth": "^2.0.6",
|
||||||
"@types/koa-mount": "^4.0.5",
|
"@types/koa-mount": "^4.0.5",
|
||||||
|
"@types/koa-send": "^4.1.6",
|
||||||
"@types/koa-static": "^4.0.4",
|
"@types/koa-static": "^4.0.4",
|
||||||
"@types/koa__cors": "^5.0.0",
|
"@types/koa__cors": "^5.0.0",
|
||||||
"@types/koa__router": "^12.0.4",
|
"@types/koa__router": "^12.0.4",
|
||||||
|
16
src/cache.ts
Normal file
16
src/cache.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Keyv from "keyv";
|
||||||
|
import KeyvSqlite from "@keyv/sqlite";
|
||||||
|
import pfs from "fs/promises";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pfs.mkdir("data");
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
const keyvSqlite = new KeyvSqlite({ dialect: "sqlite", uri: "./data/cache.db" });
|
||||||
|
keyvSqlite.on("error", (err) => {
|
||||||
|
console.log("Connection Error", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const files = new Keyv({ store: keyvSqlite, ttl: 1000 * 60 * 60 * 24, namespace: "files" });
|
||||||
|
export const downloaded = new Keyv({ store: keyvSqlite, ttl: 1000 * 30, namespace: "downloaded" });
|
79
src/downloader.ts
Normal file
79
src/downloader.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import pfs from "fs/promises";
|
||||||
|
|
||||||
|
import { NSITE_KIND } from "./const.js";
|
||||||
|
import ndk from "./ndk.js";
|
||||||
|
import { BLOSSOM_SERVERS, MAX_FILE_SIZE } from "./env.js";
|
||||||
|
import { makeRequestWithAbort } from "./helpers/http.js";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { downloaded, files } from "./cache.js";
|
||||||
|
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
|
||||||
|
|
||||||
|
// TODO: download the file to /tmp and verify it
|
||||||
|
async function downloadFile(sha256: string, servers = BLOSSOM_SERVERS) {
|
||||||
|
for (const server of servers) {
|
||||||
|
try {
|
||||||
|
const { response } = await makeRequestWithAbort(new URL(sha256, server));
|
||||||
|
if (!response.statusCode) throw new Error("Missing headers or status code");
|
||||||
|
|
||||||
|
const size = response.headers["content-length"];
|
||||||
|
if (size && parseInt(size) > MAX_FILE_SIZE) {
|
||||||
|
throw new Error("File too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
// Consume response data to free up memory
|
||||||
|
response.resume();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore error, try next server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No server found");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadSite(pubkey: string) {
|
||||||
|
if (await downloaded.get(pubkey)) return;
|
||||||
|
|
||||||
|
const user = await ndk.getUser({ pubkey });
|
||||||
|
|
||||||
|
const blossomServers = await ndk.fetchEvent([{ kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] }]);
|
||||||
|
const servers = blossomServers ? getServersFromServerListEvent(blossomServers).map((u) => u.toString()) : [];
|
||||||
|
|
||||||
|
const nsiteEvents = await ndk.fetchEvents([{ kinds: [NSITE_KIND], authors: [pubkey] }]);
|
||||||
|
|
||||||
|
servers.push(...BLOSSOM_SERVERS);
|
||||||
|
|
||||||
|
console.log(`Found ${nsiteEvents.size} events for ${pubkey}`);
|
||||||
|
|
||||||
|
for (const event of nsiteEvents) {
|
||||||
|
const path = event.dTag;
|
||||||
|
const sha256 = event.tagValue("x") || event.tagValue("sha256");
|
||||||
|
|
||||||
|
if (!path || !sha256) continue;
|
||||||
|
|
||||||
|
const current = await files.get(join(pubkey, path));
|
||||||
|
if (sha256 === current) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pfs.mkdir(dirname(join("data/sites", pubkey, path)), { recursive: true });
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await downloadFile(sha256, servers);
|
||||||
|
|
||||||
|
console.log(`Downloading ${pubkey}${path}`);
|
||||||
|
res.pipe(fs.createWriteStream(join("data/sites", pubkey, path)));
|
||||||
|
|
||||||
|
await files.set(join(pubkey, path), sha256);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Failed to download ${join(pubkey, path)}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Finished downloading ${pubkey}`);
|
||||||
|
await downloaded.set(pubkey, true);
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import xbytes from "xbytes";
|
||||||
|
|
||||||
const NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? [];
|
const NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? [];
|
||||||
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",") ?? [];
|
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",") ?? [];
|
||||||
|
|
||||||
if (NOSTR_RELAYS.length === 0) throw new Error("Requires at least one relay in NOSTR_RELAYS");
|
const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.MAX_FILE_SIZE) : Infinity;
|
||||||
if (BLOSSOM_SERVERS.length === 0) throw new Error("Requires at least one server in BLOSSOM_SERVERS");
|
|
||||||
|
|
||||||
export { NOSTR_RELAYS, BLOSSOM_SERVERS };
|
if (NOSTR_RELAYS.length === 0) throw new Error("Requires at least one relay in NOSTR_RELAYS");
|
||||||
|
|
||||||
|
export { NOSTR_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE };
|
||||||
|
48
src/index.ts
48
src/index.ts
@ -2,19 +2,17 @@
|
|||||||
import "./polyfill.js";
|
import "./polyfill.js";
|
||||||
import Koa from "koa";
|
import Koa from "koa";
|
||||||
import serve from "koa-static";
|
import serve from "koa-static";
|
||||||
import path from "node:path";
|
import path, { join } from "node:path";
|
||||||
import cors from "@koa/cors";
|
import cors from "@koa/cors";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import HttpErrors from "http-errors";
|
import send from "koa-send";
|
||||||
|
|
||||||
import logger from "./logger.js";
|
import logger from "./logger.js";
|
||||||
import { isHttpError } from "./helpers/error.js";
|
import { isHttpError } from "./helpers/error.js";
|
||||||
import { resolveNpubFromHostname } from "./helpers/dns.js";
|
import { resolveNpubFromHostname } from "./helpers/dns.js";
|
||||||
import ndk from "./ndk.js";
|
import { downloadSite } from "./downloader.js";
|
||||||
import { NSITE_KIND } from "./const.js";
|
import { downloaded } from "./cache.js";
|
||||||
import { BLOSSOM_SERVERS } from "./env.js";
|
|
||||||
import { makeRequestWithAbort } from "./helpers/http.js";
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@ -47,46 +45,22 @@ app.use(async (ctx, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// serve nsite files
|
// map pubkeys to folders in sites dir
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
const pubkey = (ctx.state.pubkey = await resolveNpubFromHostname(ctx.hostname));
|
const pubkey = (ctx.state.pubkey = await resolveNpubFromHostname(ctx.hostname));
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
const event = await ndk.fetchEvent([
|
if (!(await downloaded.get(pubkey))) {
|
||||||
{ kinds: [NSITE_KIND], "#d": [ctx.path, ctx.path.replace(/^\//, "")], authors: [pubkey] },
|
await downloadSite(pubkey);
|
||||||
]);
|
|
||||||
if (!event) throw new HttpErrors.NotFound("Failed to find event for path");
|
|
||||||
|
|
||||||
const sha256 = event.tags.find((t) => t[0] === "x" || t[0] === "sha256")?.[1];
|
|
||||||
if (!sha256) throw new HttpErrors.BadGateway("Failed to find file for path");
|
|
||||||
|
|
||||||
for (const server of BLOSSOM_SERVERS) {
|
|
||||||
try {
|
|
||||||
const { response } = await makeRequestWithAbort(new URL(sha256, server));
|
|
||||||
const { headers, statusCode } = response;
|
|
||||||
|
|
||||||
if (!headers || !statusCode) throw new Error("Missing headers or status code");
|
|
||||||
|
|
||||||
if (statusCode >= 200 && statusCode < 300) {
|
|
||||||
ctx.status = statusCode;
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
ctx.set(headers);
|
|
||||||
|
|
||||||
ctx.response.body = response;
|
|
||||||
} else {
|
|
||||||
// Consume response data to free up memory
|
|
||||||
response.resume();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// ignore error, try next server
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// throw new HttpErrors.NotFound(`Unable to find ${sha256} on blossom servers`);
|
await send(ctx, join(pubkey, ctx.path), { root: "data/sites", index: "index.html" });
|
||||||
} else await next();
|
} else await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// serve static sites
|
||||||
|
app.use(serve("sites"));
|
||||||
|
|
||||||
// serve static files from public
|
// serve static files from public
|
||||||
try {
|
try {
|
||||||
const www = path.resolve(process.cwd(), "public");
|
const www = path.resolve(process.cwd(), "public");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user