mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-18 06:33:43 +08:00
419 lines
14 KiB
JavaScript
419 lines
14 KiB
JavaScript
/**
|
||
* 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 { getConnInfo } from '@hono/node-server/conninfo';
|
||
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync } from 'fs';
|
||
import { join, resolve, dirname } from 'path';
|
||
import { fileURLToPath, pathToFileURL } from 'url';
|
||
import { SqliteD1 } from './sqliteD1.js';
|
||
import { LocalR2Storage } from './r2Storage.js';
|
||
import { dockerImageProcessor } from './imageProcessor.js';
|
||
|
||
const NativeResponse = globalThis.Response;
|
||
|
||
// ==================== 模拟 Cloudflare 全局 API ====================
|
||
|
||
// 模拟 Cloudflare Cache API(Node.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,
|
||
IMAGE_PROCESSOR: dockerImageProcessor,
|
||
};
|
||
}
|
||
|
||
// ==================== 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; 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);
|
||
}
|
||
// Windows 上 ESM 动态 import 必须使用 file:// URL,不能直接用磁盘路径
|
||
const mod = await import(pathToFileURL(filePath).href);
|
||
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 {
|
||
// 获取客户端真实 IP 并注入到请求 header 中
|
||
// 因为 Node.js 环境没有 cf-connecting-ip 等 CDN header
|
||
let request = c.req.raw;
|
||
try {
|
||
const info = getConnInfo(c);
|
||
let clientIp = info.remote?.address;
|
||
// 去掉 IPv6-mapped IPv4 前缀,如 ::ffff:127.0.0.1 → 127.0.0.1
|
||
if (clientIp && clientIp.startsWith('::ffff:')) {
|
||
clientIp = clientIp.slice(7);
|
||
}
|
||
if (clientIp && !request.headers.get('x-real-ip')) {
|
||
const newHeaders = new Headers(request.headers);
|
||
newHeaders.set('x-real-ip', clientIp);
|
||
request = new Request(request.url, {
|
||
method: request.method,
|
||
headers: newHeaders,
|
||
body: request.body,
|
||
duplex: 'half',
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// 获取 IP 失败不影响请求处理
|
||
}
|
||
const response = await handleFunctionRequest(request, 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: './frontend-dist',
|
||
rewriteRequestPath: (path) => path,
|
||
}));
|
||
|
||
// 默认返回 index.html(SPA 支持)
|
||
app.get('*', async (c) => {
|
||
const indexPath = join(ROOT_DIR, 'frontend-dist', 'index.html');
|
||
if (existsSync(indexPath)) {
|
||
const content = readFileSync(indexPath, 'utf8');
|
||
return c.html(content);
|
||
}
|
||
return c.text('Not Found', 404);
|
||
});
|
||
|
||
// ==================== 启动服务器 ====================
|
||
|
||
async function fetchWithNativeResponse(request, env, executionCtx) {
|
||
const response = await app.fetch(request, env, executionCtx);
|
||
if (!response) {
|
||
return response;
|
||
}
|
||
return new NativeResponse(response.body, {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: response.headers,
|
||
});
|
||
}
|
||
|
||
serve({
|
||
fetch: fetchWithNativeResponse,
|
||
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)`);
|
||
});
|