Merge pull request #488 from MarSeventh/copilot/refactor-docker-deployment-architecture

Fix url.origin-based logic in Docker mode: replace URL rewrite with fetch interceptor
This commit is contained in:
叁月柒
2026-03-04 10:54:02 +08:00
committed by GitHub
16 changed files with 1209 additions and 29 deletions
+2 -1
View File
@@ -4,4 +4,5 @@ Dockerfile
.dockerignore
.git
.gitignore
.github
.github
data
+5 -3
View File
@@ -1,14 +1,16 @@
FROM node:22-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates curl && \
apt-get install -y --no-install-recommends ca-certificates curl python3 make g++ && \
update-ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN npm install
RUN npm install --omit=dev && \
apt-get purge -y --auto-remove python3 make g++ && \
rm -rf /root/.npm /tmp/*
EXPOSE 8080
CMD ["npm", "run", "start"]
CMD ["node", "--import", "./server/register.mjs", "server/index.js"]
-1
View File
@@ -6,6 +6,5 @@ services:
ports:
- "7658:8080"
volumes:
- ./wrangler.toml:/app/wrangler.toml
- ./data:/app/data
restart: unless-stopped
+3 -1
View File
@@ -36,7 +36,9 @@ export async function onRequest(context) {
// 获取指定目录下的所有文件
const listUrl = new URL(`${url.origin}/api/manage/list?count=-1&dir=${currentFolder.path}`);
const listRequest = new Request(listUrl, request);
const listRequest = new Request(listUrl, {
headers: request.headers,
});
const listResponse = await fetch(listRequest);
const listData = await listResponse.json();
+3 -1
View File
@@ -33,7 +33,9 @@ export async function onRequest(context) {
// 获取指定目录下的所有文件
const listUrl = new URL(`${url.origin}/api/manage/list?count=-1&dir=${currentFolder.path}`);
const listRequest = new Request(listUrl, request);
const listRequest = new Request(listUrl, {
headers: request.headers,
});
const listResponse = await fetch(listRequest);
const listData = await listResponse.json();
+1 -1
View File
@@ -5,7 +5,7 @@ import { DiscordAPI } from "../utils/discordAPI";
import { HuggingFaceAPI } from "../utils/huggingfaceAPI";
import {
setCommonHeaders, setRangeHeaders, handleHeadRequest, getFileContent, isTgChannel,
returnWithCheck, return404, isDomainAllowed
returnWithCheck, return404, returnBlockImg, isDomainAllowed
} from './fileTools';
import { getDatabase } from '../utils/databaseAdapter.js';
+21 -12
View File
@@ -6,19 +6,28 @@ let cfEmail = "";
let cfApiKey = "";
export async function purgeCFCache(env, cdnUrl) {
// 读取其他设置
othersConfig = await fetchOthersConfig(env);
cfZoneId = othersConfig.cloudflareApiToken.CF_ZONE_ID;
cfEmail = othersConfig.cloudflareApiToken.CF_EMAIL;
cfApiKey = othersConfig.cloudflareApiToken.CF_API_KEY;
try {
// 读取其他设置
othersConfig = await fetchOthersConfig(env);
cfZoneId = othersConfig.cloudflareApiToken.CF_ZONE_ID;
cfEmail = othersConfig.cloudflareApiToken.CF_EMAIL;
cfApiKey = othersConfig.cloudflareApiToken.CF_API_KEY;
// 清除CDN缓存
const options = {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${cfEmail}`, 'X-Auth-Key': `${cfApiKey}`},
body: `{"files":["${ cdnUrl }"]}`
};
await fetch(`https://api.cloudflare.com/client/v4/zones/${ cfZoneId }/purge_cache`, options);
// 如果没有配置Cloudflare API,跳过缓存清除
if (!cfZoneId || !cfEmail || !cfApiKey) {
return;
}
// 清除CDN缓存
const options = {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${cfEmail}`, 'X-Auth-Key': `${cfApiKey}`},
body: `{"files":["${ cdnUrl }"]}`
};
await fetch(`https://api.cloudflare.com/client/v4/zones/${ cfZoneId }/purge_cache`, options);
} catch (error) {
console.error('Failed to purge CF cache:', error.message || error);
}
}
export async function purgeRandomFileListCache(origin, ...dirs) {
+403 -8
View File
@@ -1,5 +1,5 @@
{
"name": "self-imgbed",
"name": "CloudFlare-ImgBed",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -7,7 +7,10 @@
"dependencies": {
"@aws-sdk/client-s3": "3.726.1",
"@cloudflare/pages-plugin-sentry": "^1.1.3",
"@hono/node-server": "^1.13.8",
"@sentry/tracing": "^7.114.0",
"better-sqlite3": "^11.9.1",
"hono": "^4.7.4",
"miniflare": "^3.20240718.0"
},
"devDependencies": {
@@ -1892,6 +1895,18 @@
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.10",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz",
"integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
@@ -3398,6 +3413,37 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3411,6 +3457,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/blake3-wasm": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
@@ -3454,6 +3520,30 @@
"dev": true,
"license": "ISC"
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -3536,6 +3626,12 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -3675,6 +3771,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -3689,7 +3809,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -3727,6 +3846,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/error-stack-parser-es": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
@@ -3863,6 +3991,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-xml-builder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
@@ -3894,6 +4031,12 @@
"fxparser": "src/cli/cli.js"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3972,6 +4115,12 @@
"node": ">= 6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4063,6 +4212,12 @@
"source-map": "^0.6.1"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
@@ -4178,6 +4333,35 @@
"he": "bin/he"
}
},
"node_modules/hono": {
"version": "4.12.4",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.4.tgz",
"integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -4194,7 +4378,12 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/is-binary-path": {
@@ -4386,6 +4575,18 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/miniflare": {
"version": "3.20250718.3",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz",
@@ -4428,12 +4629,17 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mocha": {
"version": "10.8.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
@@ -4517,6 +4723,24 @@
"mustache": "bin/mustache"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4531,7 +4755,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -4606,6 +4829,33 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/printable-characters": {
"version": "1.0.42",
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
@@ -4619,6 +4869,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -4629,6 +4889,44 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4666,7 +4964,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4687,7 +4984,6 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -4764,6 +5060,51 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -4799,6 +5140,15 @@
"npm": ">=6"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -4868,6 +5218,34 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4897,6 +5275,18 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/undici": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
@@ -4919,6 +5309,12 @@
"pathe": "^2.0.3"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wait-on": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
@@ -5204,7 +5600,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
+7 -1
View File
@@ -1,13 +1,19 @@
{
"type": "module",
"scripts": {
"ci-test": "concurrently --kill-others \"npm start\" \"wait-on http://localhost:8080 && mocha\"",
"ci-test:docker": "concurrently --kill-others \"npm run start:docker\" \"wait-on http://localhost:8080 && mocha\"",
"test": "mocha",
"start": "npx wrangler pages dev ./ --kv \"img_url\" --r2 \"img_r2\" --ip 0.0.0.0 --port 8080 --persist-to ./data"
"start": "npx wrangler pages dev ./ --kv \"img_url\" --r2 \"img_r2\" --ip 0.0.0.0 --port 8080 --persist-to ./data",
"start:docker": "node --import ./server/register.mjs server/index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "3.726.1",
"@cloudflare/pages-plugin-sentry": "^1.1.3",
"@sentry/tracing": "^7.114.0",
"better-sqlite3": "^11.9.1",
"hono": "^4.7.4",
"@hono/node-server": "^1.13.8",
"miniflare": "^3.20240718.0"
},
"overrides": {
+376
View File
@@ -0,0 +1,376 @@
/**
* Docker 模式下的原生 Node.js 服务器
* 使用 Hono 作为 Web 框架,代理 Cloudflare Pages Functions 请求
* 使用 SQLite 替代 D1,本地文件系统替代 R2
*/
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { serveStatic } from '@hono/node-server/serve-static';
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { SqliteD1 } from './sqliteD1.js';
import { LocalR2Storage } from './r2Storage.js';
// ==================== 模拟 Cloudflare 全局 API ====================
// 模拟 Cloudflare Cache APINode.js 中不存在)
if (typeof globalThis.caches === 'undefined') {
globalThis.caches = {
default: {
async match() { return undefined; },
async put() {},
async delete() { return false; },
},
};
}
// ==================== 自引用 Fetch 拦截器 ====================
// 解决 Docker 端口映射导致 functions 内部 fetch(url.origin + ...) 失败的问题
// 不再重写请求 URL,而是拦截自引用的 fetch 调用,透明路由到内部端口
// 这样 url.origin 保持为外部 origin,确保 Referer 匹配、返回链接、重定向等功能正常
const selfOrigins = new Set();
const originalFetch = globalThis.fetch;
globalThis.fetch = async function(input, init) {
try {
let urlStr;
if (typeof input === 'string') {
urlStr = input;
} else if (input instanceof URL) {
urlStr = input.toString();
} else if (input instanceof Request) {
urlStr = input.url;
}
if (urlStr) {
const parsed = new URL(urlStr);
const internalOrigin = `http://localhost:${port}`;
// 如果目标 origin 是已知的外部自身 origin,重写为内部地址
if (parsed.origin !== internalOrigin && selfOrigins.has(parsed.origin)) {
const newUrl = `${internalOrigin}${parsed.pathname}${parsed.search}`;
if (input instanceof Request) {
return originalFetch(new Request(newUrl, input), init);
}
return originalFetch(newUrl, init);
}
}
} catch (e) {
// URL 解析失败等异常,回退到原始 fetch
if (!(e instanceof TypeError)) {
console.error('Fetch interceptor error:', e.message);
}
}
return originalFetch(input, init);
};
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = resolve(__dirname, '..');
const FUNCTIONS_DIR = resolve(ROOT_DIR, 'functions');
const DATA_DIR = resolve(ROOT_DIR, 'data');
const port = parseInt(process.env.PORT || '8080', 10);
// 确保数据目录存在
mkdirSync(DATA_DIR, { recursive: true });
// ==================== 初始化数据库 ====================
const sqliteD1 = new SqliteD1(join(DATA_DIR, 'database.sqlite'));
// 执行初始化 SQL
const initSqlPath = join(ROOT_DIR, 'database', 'init.sql');
if (existsSync(initSqlPath)) {
const initSql = readFileSync(initSqlPath, 'utf8');
try {
sqliteD1.exec(initSql);
console.log('Database initialized successfully');
} catch (e) {
console.log('Database init:', e.message);
}
}
// 执行数据库迁移
const migrationsDir = join(ROOT_DIR, 'database', 'migrations');
if (existsSync(migrationsDir)) {
const migrations = readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
for (const migration of migrations) {
try {
const sql = readFileSync(join(migrationsDir, migration), 'utf8');
sqliteD1.exec(sql);
console.log(`Migration ${migration}: OK`);
} catch (e) {
// 忽略已执行的迁移(如列已存在等)
console.log(`Migration ${migration}: ${e.message}`);
}
}
}
// ==================== 初始化 R2 存储 ====================
const r2Storage = new LocalR2Storage(join(DATA_DIR, 'r2'));
// ==================== 创建环境对象 ====================
function createEnv() {
return {
...process.env,
img_d1: sqliteD1,
img_r2: r2Storage,
};
}
// ==================== Functions 路由解析 ====================
/**
* 根据请求路径查找对应的 function 文件
*/
function findFunctionFile(pathname) {
const parts = pathname.split('/').filter(Boolean);
// 1. 尝试精确匹配
if (parts.length > 0) {
const exactFile = join(FUNCTIONS_DIR, ...parts) + '.js';
if (existsSync(exactFile) && statSync(exactFile).isFile()) {
return { file: exactFile, params: {} };
}
}
// 2. 尝试 index.js 匹配
if (parts.length > 0) {
const indexFile = join(FUNCTIONS_DIR, ...parts, 'index.js');
if (existsSync(indexFile) && statSync(indexFile).isFile()) {
return { file: indexFile, params: {} };
}
}
// 3. 尝试 [[path]].js 通配符匹配(从深到浅)
for (let i = parts.length - 1; i >= 0; i--) {
const dirParts = parts.slice(0, i);
const dirPath = join(FUNCTIONS_DIR, ...dirParts);
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
const catchAllFile = join(dirPath, '[[path]].js');
if (existsSync(catchAllFile) && statSync(catchAllFile).isFile()) {
const pathParam = parts.slice(i);
return { file: catchAllFile, params: { path: pathParam } };
}
}
}
return null;
}
/**
* 查找请求路径对应的中间件链
*/
const middlewareCache = new Map();
async function findMiddlewares(pathname) {
const parts = pathname.split('/').filter(Boolean);
const allMiddlewares = [];
// 检查根 functions 目录
const rootMiddleware = join(FUNCTIONS_DIR, '_middleware.js');
if (existsSync(rootMiddleware)) {
const mod = await importModule(rootMiddleware);
if (mod.onRequest) {
const handlers = Array.isArray(mod.onRequest) ? mod.onRequest : [mod.onRequest];
allMiddlewares.push(...handlers);
}
}
// 逐级检查子目录中间件
for (let i = 1; i <= parts.length; i++) {
const dirParts = parts.slice(0, i);
const middlewareFile = join(FUNCTIONS_DIR, ...dirParts, '_middleware.js');
if (existsSync(middlewareFile) && statSync(middlewareFile).isFile()) {
const mod = await importModule(middlewareFile);
if (mod.onRequest) {
const handlers = Array.isArray(mod.onRequest) ? mod.onRequest : [mod.onRequest];
allMiddlewares.push(...handlers);
}
}
}
return allMiddlewares;
}
/**
* 模块导入缓存
*/
const moduleCache = new Map();
async function importModule(filePath) {
if (moduleCache.has(filePath)) {
return moduleCache.get(filePath);
}
const mod = await import(filePath);
moduleCache.set(filePath, mod);
return mod;
}
/**
* 执行中间件链和处理函数
*/
async function executeChain(middlewares, handler, context) {
const chain = [...middlewares, handler];
let index = 0;
context.next = async function () {
if (index < chain.length) {
const fn = chain[index++];
return await fn(context);
}
// 如果链执行完毕,返回 404
return new Response('Not Found', { status: 404 });
};
return await context.next();
}
/**
* 处理 Functions 请求
*/
async function handleFunctionRequest(originalRequest, pathname) {
// 查找对应的 function 文件
const funcInfo = findFunctionFile(pathname);
if (!funcInfo) return null;
// 记录外部 origin,供 fetch 拦截器使用
// 不再重写请求 URL,保持 url.origin 为外部 origin
const request = originalRequest;
const requestUrl = new URL(originalRequest.url);
const internalOrigin = `http://localhost:${port}`;
if (requestUrl.origin !== internalOrigin) {
selfOrigins.add(requestUrl.origin);
}
// 导入模块
const mod = await importModule(funcInfo.file);
// 根据请求方法查找处理函数
const method = request.method.toUpperCase();
const methodHandlerName = 'onRequest' + method.charAt(0) + method.slice(1).toLowerCase();
let handler = null;
if (typeof mod[methodHandlerName] === 'function') {
handler = mod[methodHandlerName];
} else if (mod.onRequest) {
handler = typeof mod.onRequest === 'function'
? mod.onRequest
: mod.onRequest[mod.onRequest.length - 1];
}
if (!handler) {
return new Response('Method Not Allowed', { status: 405 });
}
// 获取中间件
const middlewares = await findMiddlewares(pathname);
// 如果 onRequest 是数组,把前面的加入中间件链
if (Array.isArray(mod.onRequest) && mod.onRequest.length > 1 && handler === mod.onRequest[mod.onRequest.length - 1]) {
middlewares.push(...mod.onRequest.slice(0, -1));
}
// 模拟 Cloudflare 的 request.cf 属性(telemetryData 等中间件依赖该属性)
if (!request.cf) {
request.cf = {
country: 'XX',
city: 'Unknown',
continent: 'XX',
latitude: '0',
longitude: '0',
region: '',
regionCode: '',
timezone: '',
postalCode: '',
asn: 0,
asOrganization: '',
colo: 'LOCAL',
httpProtocol: 'HTTP/1.1',
requestPriority: '',
tlsCipher: '',
tlsVersion: '',
};
}
// 创建 Cloudflare Pages Functions 风格的 context 对象
const env = createEnv();
const context = {
request,
env,
params: funcInfo.params,
waitUntil: (promise) => {
if (promise && typeof promise.catch === 'function') {
promise.catch(err => console.error('waitUntil error:', err));
}
},
next: null, // 由 executeChain 设置
data: {},
};
// 执行中间件链和处理函数
return await executeChain(middlewares, handler, context);
}
// ==================== Hono 应用 ====================
const app = new Hono();
// 判断是否是 function 路径
const FUNCTION_PREFIXES = ['/api/', '/upload', '/file/', '/dav/', '/random'];
function isFunctionPath(pathname) {
return FUNCTION_PREFIXES.some(prefix => pathname.startsWith(prefix));
}
// Functions 路由处理 - 处理所有 HTTP 方法
app.all('*', async (c, next) => {
const url = new URL(c.req.url);
const pathname = url.pathname;
// 检查是否是 function 路径
if (isFunctionPath(pathname)) {
try {
const response = await handleFunctionRequest(c.req.raw, pathname);
if (response) {
return response;
}
} catch (err) {
console.error('Function error:', err);
return new Response(`Internal Server Error: ${err.message}`, { status: 500 });
}
}
// 不是 function 路径,继续到静态文件
await next();
});
// 静态文件服务
app.use('/*', serveStatic({
root: './',
rewriteRequestPath: (path) => path,
}));
// 默认返回 index.htmlSPA 支持)
app.get('*', async (c) => {
const indexPath = join(ROOT_DIR, 'index.html');
if (existsSync(indexPath)) {
const content = readFileSync(indexPath, 'utf8');
return c.html(content);
}
return c.text('Not Found', 404);
});
// ==================== 启动服务器 ====================
serve({
fetch: app.fetch,
port,
}, (info) => {
console.log(`Server running at http://0.0.0.0:${info.port}`);
console.log(`Data directory: ${DATA_DIR}`);
console.log(`Mode: Docker (Native Node.js)`);
});
+59
View File
@@ -0,0 +1,59 @@
/**
* 自定义 ESM Loader
* 1. 自动为无扩展名的相对导入添加 .js 后缀
* 2. 将 Cloudflare 特定的包重定向到本地 mock
*/
import { fileURLToPath, pathToFileURL } from 'url';
import { existsSync } from 'fs';
import { dirname, resolve as pathResolve } from 'path';
// Mock 文件路径
const __dirname = dirname(fileURLToPath(import.meta.url));
const MOCKS = {
'@cloudflare/pages-plugin-sentry': pathResolve(__dirname, 'mocks/sentry-plugin.js'),
'@sentry/tracing': pathResolve(__dirname, 'mocks/sentry-tracing.js'),
};
export async function resolve(specifier, context, nextResolve) {
// 拦截 Cloudflare 特定的包
if (MOCKS[specifier]) {
return {
url: pathToFileURL(MOCKS[specifier]).href,
shortCircuit: true,
};
}
// 处理无扩展名的相对导入
if (specifier.startsWith('.') && !hasExtension(specifier)) {
const parentDir = context.parentURL
? dirname(fileURLToPath(context.parentURL))
: process.cwd();
// 尝试添加 .js 扩展名
const withJs = pathResolve(parentDir, specifier + '.js');
if (existsSync(withJs)) {
return {
url: pathToFileURL(withJs).href,
shortCircuit: true,
};
}
// 尝试作为目录的 index.js
const indexJs = pathResolve(parentDir, specifier, 'index.js');
if (existsSync(indexJs)) {
return {
url: pathToFileURL(indexJs).href,
shortCircuit: true,
};
}
}
return nextResolve(specifier, context);
}
function hasExtension(specifier) {
const lastSlash = specifier.lastIndexOf('/');
const filename = lastSlash >= 0 ? specifier.slice(lastSlash + 1) : specifier;
return filename.includes('.');
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Mock for @cloudflare/pages-plugin-sentry
* Docker 模式下不使用 Sentry,提供空实现
*/
export default function sentryPlugin(config) {
return function(context) {
// 提供一个空的 sentry 对象以兼容 telemetryData 中的调用
if (!context.data) context.data = {};
context.data.sentry = {
setTag: () => {},
setContext: () => {},
startTransaction: () => ({
startChild: () => ({ finish: () => {} }),
finish: () => {}
})
};
return context.next();
};
}
+5
View File
@@ -0,0 +1,5 @@
/**
* Mock for @sentry/tracing
* Docker 模式下不使用 Sentry Tracing
*/
export default {};
+194
View File
@@ -0,0 +1,194 @@
/**
* 本地 R2 存储适配器
* 使用本地文件系统模拟 Cloudflare R2 的 API 接口
*/
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync, rmSync } from 'fs';
import { dirname, join } from 'path';
import { createReadStream } from 'fs';
import { Readable } from 'stream';
import { randomUUID } from 'crypto';
export class LocalR2Storage {
constructor(basePath) {
this.basePath = basePath;
mkdirSync(basePath, { recursive: true });
}
_filePath(key) {
return join(this.basePath, key);
}
/**
* 获取文件(模拟 R2 的 get 方法)
* 支持 Range 请求
*/
async get(key, options) {
const filePath = this._filePath(key);
if (!existsSync(filePath)) return null;
const stats = statSync(filePath);
const size = stats.size;
let body;
let range;
if (options && options.range) {
const offset = options.range.offset || 0;
const length = options.range.length || (size - offset);
const end = offset + length - 1;
const nodeStream = createReadStream(filePath, { start: offset, end: end });
body = Readable.toWeb(nodeStream);
range = { offset, length };
} else {
const nodeStream = createReadStream(filePath);
body = Readable.toWeb(nodeStream);
}
return {
body,
size,
range,
httpMetadata: {},
writeHttpMetadata(headers) {
// 本地存储不设置额外的 HTTP 元数据
}
};
}
/**
* 存储文件(模拟 R2 的 put 方法)
* 支持多种输入类型
*/
async put(key, value) {
const filePath = this._filePath(key);
mkdirSync(dirname(filePath), { recursive: true });
let buffer;
if (value instanceof Blob || (typeof File !== 'undefined' && value instanceof File)) {
buffer = Buffer.from(await value.arrayBuffer());
} else if (value instanceof ArrayBuffer) {
buffer = Buffer.from(value);
} else if (value instanceof Uint8Array || Buffer.isBuffer(value)) {
buffer = Buffer.from(value);
} else if (value instanceof ReadableStream) {
const reader = value.getReader();
const chunks = [];
while (true) {
const { done, value: chunk } = await reader.read();
if (done) break;
chunks.push(chunk);
}
buffer = Buffer.concat(chunks);
} else if (typeof value === 'string') {
buffer = Buffer.from(value, 'utf-8');
} else {
buffer = Buffer.from(String(value), 'utf-8');
}
writeFileSync(filePath, buffer);
}
/**
* 删除文件(模拟 R2 的 delete 方法)
*/
async delete(key) {
const filePath = this._filePath(key);
if (existsSync(filePath)) {
unlinkSync(filePath);
}
}
/**
* 创建分块上传(模拟 R2 的 createMultipartUpload 方法)
*/
async createMultipartUpload(key) {
const uploadId = randomUUID();
const uploadDir = join(this.basePath, '_multipart', uploadId);
mkdirSync(uploadDir, { recursive: true });
// 保存 key 映射
writeFileSync(join(uploadDir, '_key'), key);
return {
uploadId,
key,
...this._createMultipartMethods(key, uploadId)
};
}
/**
* 恢复分块上传(模拟 R2 的 resumeMultipartUpload 方法)
*/
resumeMultipartUpload(key, uploadId) {
return {
uploadId,
key,
...this._createMultipartMethods(key, uploadId)
};
}
_createMultipartMethods(key, uploadId) {
const self = this;
const uploadDir = join(this.basePath, '_multipart', uploadId);
return {
async uploadPart(partNumber, data) {
mkdirSync(uploadDir, { recursive: true });
const partPath = join(uploadDir, `part_${String(partNumber).padStart(6, '0')}`);
let buffer;
if (data instanceof ArrayBuffer) {
buffer = Buffer.from(data);
} else if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
buffer = Buffer.from(data);
} else if (data instanceof Blob) {
buffer = Buffer.from(await data.arrayBuffer());
} else if (data instanceof ReadableStream) {
const reader = data.getReader();
const chunks = [];
while (true) {
const { done, value: chunk } = await reader.read();
if (done) break;
chunks.push(chunk);
}
buffer = Buffer.concat(chunks);
} else {
buffer = Buffer.from(data);
}
writeFileSync(partPath, buffer);
return { etag: `etag_${partNumber}`, partNumber };
},
async complete(parts) {
// 按 partNumber 排序合并所有分块
const sortedParts = parts.sort((a, b) => a.partNumber - b.partNumber);
const chunks = [];
for (const part of sortedParts) {
const partPath = join(uploadDir, `part_${String(part.partNumber).padStart(6, '0')}`);
if (existsSync(partPath)) {
chunks.push(readFileSync(partPath));
}
}
const merged = Buffer.concat(chunks);
await self.put(key, merged);
// 清理分块目录
self._cleanupDir(uploadDir);
},
async abort() {
// 清理分块目录
self._cleanupDir(uploadDir);
}
};
}
_cleanupDir(dir) {
if (existsSync(dir)) {
try {
rmSync(dir, { recursive: true, force: true });
} catch (e) {
console.error('Cleanup error:', e.message);
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* ESM Loader 注册入口
* 通过 node --import ./server/register.mjs 使用
*/
import { register } from 'node:module';
register('./loader.mjs', import.meta.url);
+104
View File
@@ -0,0 +1,104 @@
/**
* SQLite D1 适配器
* 使用 better-sqlite3 模拟 Cloudflare D1 的 API 接口
* 使得现有的 d1Database.js 无需修改即可在 Node.js 中运行
*/
import Database from 'better-sqlite3';
export class SqliteD1 {
constructor(dbPath) {
this.db = new Database(dbPath);
this.db.pragma('journal_mode = WAL');
this.db.pragma('foreign_keys = ON');
}
/**
* 初始化数据库(执行 SQL 脚本)
*/
exec(sql) {
this.db.exec(sql);
}
/**
* 模拟 D1 的 prepare 方法
* 返回一个具有 bind/first/all/run 方法的对象
*/
prepare(sql) {
const db = this.db;
return new SqliteD1Statement(db, sql);
}
}
class SqliteD1Statement {
constructor(db, sql) {
this._db = db;
this._sql = sql;
this._params = [];
}
/**
* 绑定参数(模拟 D1 的 bind 方法)
*/
bind(...params) {
// SQLite 不支持 undefined 和 boolean,转换以兼容 D1 行为
this._params = params.map(p => {
if (p === undefined) return null;
if (typeof p === 'boolean') return p ? 1 : 0;
return p;
});
return this;
}
/**
* 获取第一行结果(模拟 D1 的 first 方法)
* 返回 Promise 以兼容 D1 的异步接口
*/
async first(column) {
try {
const stmt = this._db.prepare(this._sql);
const row = stmt.get(...this._params);
if (!row) return null;
if (column) return row[column];
return row;
} catch (e) {
console.error('SQLite first() error:', e.message, 'SQL:', this._sql);
throw e;
}
}
/**
* 获取所有结果(模拟 D1 的 all 方法)
* 返回 { results: [...] } 格式以兼容 D1
*/
async all() {
try {
const stmt = this._db.prepare(this._sql);
const rows = stmt.all(...this._params);
return { results: rows };
} catch (e) {
console.error('SQLite all() error:', e.message, 'SQL:', this._sql);
throw e;
}
}
/**
* 执行 SQL(模拟 D1 的 run 方法)
*/
async run() {
try {
const stmt = this._db.prepare(this._sql);
const result = stmt.run(...this._params);
return {
success: true,
meta: {
changes: result.changes,
last_row_id: result.lastInsertRowid
}
};
} catch (e) {
console.error('SQLite run() error:', e.message, 'SQL:', this._sql);
throw e;
}
}
}