Files
alist-proxy/source.js
XiaoMo 5e9b894c5c perf(缓存): 优化缓存清理和服务逻辑,使用异步文件操作和并行处理
重构缓存清理逻辑,使用异步文件操作和并行删除提高效率
优化serveFromCache函数,增加缓存失效时的自动重试机制
使用流管道优化文件传输性能,增加缓冲区大小
移除未使用的sharp依赖和缩略图生成逻辑
2025-09-04 10:53:08 +08:00

800 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const http = require('http');
const https = require('https');
const url = require('url');
const querystring = require('querystring');
const fs = require('fs');
const pathModule = require('path');
const crypto = require('crypto');
const CACHE_DIR_NAME = '.cache';
const DEFAULT_PORT = 9001;
const DEFAULT_API_ENDPOINT = 'http://183.6.121.121:9519/api';
const cacheDir = pathModule.join(__dirname, CACHE_DIR_NAME);
const pathIndex = {};
// 访问计数器
const viewsInfo = {
request: 0,
cacheHit: 0,
apiCall: 0,
cacheCall: 0,
cacheReadError: 0,
fetchApiError: 0,
fetchApiWarning: 0,
increment: function (key) {
if (this.hasOwnProperty(key)) {
this[key]++;
}
}
};
let port = DEFAULT_PORT;
let apiEndpoint = DEFAULT_API_ENDPOINT;
// 解析命令行参数函数
function parseArguments() {
const args = process.argv.slice(2);
args.forEach(arg => {
const cleanArg = arg.startsWith('--') ? arg.substring(2) : arg;
const [key, value] = cleanArg.split('=');
if (key === 'port' && value) {
const parsedPort = parseInt(value, 10);
if (!isNaN(parsedPort)) {
port = parsedPort;
}
} else if (key === 'api' && value) {
apiEndpoint = value;
}
});
}
// 初始化函数,包含参数解析和目录创建
function initializeApp() {
parseArguments();
if (!fs.existsSync(cacheDir)) {
try {
fs.mkdirSync(cacheDir, { recursive: true });
console.log(`Cache directory created: ${cacheDir}`);
} catch (err) {
console.error(`Error creating cache directory ${cacheDir}:`, err);
process.exit(1); // Exit if cache directory cannot be created
}
}
}
initializeApp();
const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
const CACHE_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
const HTTP_STATUS = {
OK: 200,
NO_CONTENT: 204,
REDIRECT: 302,
NOT_MODIFIED: 304,
BAD_REQUEST: 400,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
BAD_GATEWAY: 502,
};
// 定时清理过期缓存数据
setInterval(async () => {
const currentTime = Date.now();
const keysToDelete = [];
const filesToDelete = [];
// 第一步:收集需要删除的键和文件
for (const key in pathIndex) {
if (currentTime - pathIndex[key].timestamp > CACHE_EXPIRY_MS) {
keysToDelete.push(key);
const cacheMetaFile = pathModule.join(cacheDir, `${key}.meta`);
const cacheContentFile = pathModule.join(cacheDir, `${pathIndex[key].uniqid}.content`);
filesToDelete.push(cacheMetaFile, cacheContentFile);
}
}
// 第二步:从内存中删除过期索引
keysToDelete.forEach(key => delete pathIndex[key]);
// 第三步:异步删除文件系统中的缓存文件
if (filesToDelete.length > 0) {
console.log(`Cleaning up ${keysToDelete.length} expired cache entries`);
// 并行删除文件但限制并发数为10
const deletePromises = filesToDelete.map(file =>
fs.promises.unlink(file).catch(err => {
if (err.code !== 'ENOENT') { // 忽略文件不存在的错误
console.warn(`Failed to delete cache file ${file}:`, err.message);
}
})
);
// 使用Promise.all处理所有删除操作
await Promise.all(deletePromises);
}
}, CACHE_CLEANUP_INTERVAL_MS);
// 统一发送错误响应
function sendErrorResponse(res, statusCode, message) {
if (!res.headersSent) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain;charset=UTF-8' });
res.end(message);
}
}
// --- Request Handling Logic ---
async function handleFavicon(req, res) {
res.writeHead(HTTP_STATUS.NO_CONTENT);
res.end();
}
async function handleEndpoint(req, res, parsedUrl) {
if (parsedUrl.query.api) {
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/;
if (urlRegex.test(parsedUrl.query.api)) {
apiEndpoint = parsedUrl.query.api;
console.log(`API endpoint updated to: ${apiEndpoint}`);
}
}
res.writeHead(HTTP_STATUS.OK, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
code: HTTP_STATUS.OK,
data: {
api: apiEndpoint,
port: port,
cacheDir: cacheDir,
pathIndexCount: Object.keys(pathIndex).length,
viewsInfo: {
request: viewsInfo.request,
cacheHit: viewsInfo.cacheHit,
apiCall: viewsInfo.apiCall,
cacheCall: viewsInfo.cacheCall,
cacheReadError: viewsInfo.cacheReadError,
fetchApiError: viewsInfo.fetchApiError,
fetchApiWarning: viewsInfo.fetchApiWarning,
}
}
}));
}
async function handleApiRedirect(res, apiData) {
res.writeHead(HTTP_STATUS.REDIRECT, { Location: apiData.data.url });
res.end();
}
async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res) {
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb };
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
const cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`);
const cacheContentFile = pathModule.join(cacheDir, `${data.uniqid}.content`);
const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`);
try {
fs.writeFileSync(cacheMetaFile, JSON.stringify(data));
} catch (writeError) {
console.error(`Error writing meta file ${cacheMetaFile}:`, writeError);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Failed to write cache metadata.');
return;
}
if (fs.existsSync(cacheContentFile)) {
const stats = fs.statSync(cacheContentFile);
const contentLength = stats.size;
if (contentLength < 2048 && data.headers['content-length'] && parseInt(data.headers['content-length'], 10) !== contentLength) {
console.warn(`Content length mismatch for ${cacheContentFile}. API: ${data.headers['content-length']}, Cache: ${contentLength}. Re-fetching.`);
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
} else {
serveFromCache(data, cacheContentFile, cacheMetaFile, res, reqPath, token, sign, uniqidhex);
}
} else {
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
}
}
async function tryServeFromStaleCacheOrError(uniqidhex, res, errorMessage) {
if (pathIndex[uniqidhex]) {
const cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`);
const cacheContentFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.content`);
if (fs.existsSync(cacheMetaFile) && fs.existsSync(cacheContentFile)) {
console.warn(`API call failed or returned non-200. Serving stale cache for ${uniqidhex}`);
try {
const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8'));
serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, null, null, null, uniqidhex);
return;
} catch (parseError) {
console.error(`Error parsing stale meta file ${cacheMetaFile}:`, parseError);
// Fall through to generic error if stale cache is also broken
}
}
}
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, errorMessage || 'Bad Gateway');
}
async function handleMainRequest(req, res) {
req.url = req.url.replace(/\/{2,}/g, '/');
const parsedUrl = url.parse(req.url, true);
const sign = parsedUrl.query.sign || '';
let reqPath = parsedUrl.pathname.split('/')[1] || ''; // Ensure reqPath is not undefined
let token = parsedUrl.pathname.split('/').slice(2).join('/');
if (reqPath === 'favicon.ico') return handleFavicon(req, res);
if (reqPath === 'endpoint') return handleEndpoint(req, res, parsedUrl);
if (!token && reqPath) { // If token is empty but reqPath is not, assume reqPath is the token
token = reqPath;
reqPath = 'app'; // Default to 'app' if only one path segment is provided
}
const ALLOWED_PATHS = ['avatar', 'go', 'bbs', 'www', 'url', 'thumb', 'app'];
if (!ALLOWED_PATHS.includes(reqPath) || !token) {
return sendErrorResponse(res, HTTP_STATUS.BAD_REQUEST, `Bad Request: Invalid path or missing token.`);
}
viewsInfo.increment('request');
const uniqidhex = crypto.createHash('md5').update(reqPath + token + sign).digest('hex');
let cacheMetaFile = '';
let cacheContentFile = '';
if (pathIndex[uniqidhex]) {
cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`);
cacheContentFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.content`);
}
if (pathIndex[uniqidhex] && isCacheValid(cacheMetaFile, cacheContentFile)) {
const { cacheData, isNotModified } = await checkCacheHeaders(req, cacheMetaFile);
if (isNotModified) {
res.writeHead(HTTP_STATUS.NOT_MODIFIED);
res.end();
} else {
viewsInfo.increment('cacheHit');
serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, reqPath, token, sign, uniqidhex);
}
} else {
try {
viewsInfo.increment('apiCall');
const apiData = await fetchApiData(reqPath, token, sign);
if (apiData.code === HTTP_STATUS.REDIRECT || apiData.code === 301) {
return handleApiRedirect(res, apiData);
}
if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) {
await processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res);
} else {
viewsInfo.increment('fetchApiWarning');
await tryServeFromStaleCacheOrError(uniqidhex, res, apiData.message);
}
} catch (error) {
viewsInfo.increment('fetchApiError');
console.error('Error in API call or processing:', error);
await tryServeFromStaleCacheOrError(uniqidhex, res, `Bad Gateway: API request failed. ${error.message}`);
}
}
}
const server = http.createServer(handleMainRequest);
// 检查缓存头并返回是否为304
async function checkCacheHeaders(req, cacheMetaFile) {
try {
const metaContent = await fs.promises.readFile(cacheMetaFile, 'utf8');
const cacheData = JSON.parse(metaContent);
const ifNoneMatch = req.headers['if-none-match'];
const ifModifiedSince = req.headers['if-modified-since'];
// Check ETag first
if (ifNoneMatch && cacheData.uniqid && ifNoneMatch === cacheData.uniqid) {
return { cacheData, isNotModified: true };
}
// Check If-Modified-Since
if (ifModifiedSince && cacheData.headers && cacheData.headers['last-modified']) {
try {
const lastModifiedDate = new Date(cacheData.headers['last-modified']);
const ifModifiedSinceDate = new Date(ifModifiedSince);
// The time resolution of an HTTP date is one second.
// If If-Modified-Since is at least as new as Last-Modified, send 304.
if (lastModifiedDate.getTime() <= ifModifiedSinceDate.getTime()) {
return { cacheData, isNotModified: true };
}
} catch (dateParseError) {
console.warn(`Error parsing date for cache header check (${cacheMetaFile}):`, dateParseError);
// Proceed as if not modified check failed if dates are invalid
}
}
return { cacheData, isNotModified: false };
} catch (error) {
console.error(`Error reading or parsing cache meta file ${cacheMetaFile} in checkCacheHeaders:`, error);
return { cacheData: null, isNotModified: false }; // Indicate failure to load cacheData
}
}
// 检查缓存是否有效
async function isCacheValid(cacheMetaFile, cacheContentFile) {
try {
// 使用Promise.all并行检查文件是否存在
const [metaExists, contentExists] = await Promise.all([
fs.promises.access(cacheMetaFile).then(() => true).catch(() => false),
fs.promises.access(cacheContentFile).then(() => true).catch(() => false)
]);
if (!metaExists || !contentExists) {
return false;
}
const metaContent = await fs.promises.readFile(cacheMetaFile, 'utf8');
const cacheData = JSON.parse(metaContent);
// Ensure expiration is a number and in the future
return typeof cacheData.expiration === 'number' && cacheData.expiration > Date.now();
} catch (error) {
console.warn(`Error reading or parsing cache meta file ${cacheMetaFile} for validation:`, error);
return false; // If meta file is corrupt or unreadable, cache is not valid
}
}
// 从 API 获取数据
const API_TIMEOUT_MS = 5000;
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36';
async function fetchApiData(reqPath, token, sign) {
const queryParams = querystring.stringify({
type: reqPath,
sign: sign
});
const apiUrl = `${apiEndpoint}?${queryParams}`;
const parsedApiUrl = new URL(apiUrl);
const protocol = parsedApiUrl.protocol === 'https:' ? https : http;
const options = {
method: 'GET',
headers: {
'Accept': 'application/json; charset=utf-8',
'User-Agent': USER_AGENT,
'token': token
},
timeout: API_TIMEOUT_MS,
rejectUnauthorized: false, // Allow self-signed certificates, use with caution
};
return new Promise((resolve, reject) => {
const apiReq = protocol.request(apiUrl, options, (apiRes) => {
let responseData = '';
apiRes.setEncoding('utf8');
apiRes.on('data', chunk => responseData += chunk);
apiRes.on('end', () => {
try {
if (apiRes.statusCode >= 400) {
// Treat HTTP errors from API as rejections for easier handling
console.error(`API request to ${apiUrl} failed with status ${apiRes.statusCode}: ${responseData}`);
// Attempt to parse for a message, but prioritize status code for error type
let errorPayload = { code: apiRes.statusCode, message: `API Error: ${apiRes.statusCode}` };
try {
const parsedError = JSON.parse(responseData);
if (parsedError && parsedError.message) errorPayload.message = parsedError.message;
} catch (e) { /* Ignore if response is not JSON */ }
resolve(errorPayload); // Resolve with error structure for consistency
return;
}
resolve(JSON.parse(responseData));
} catch (parseError) {
console.error(`Error parsing JSON response from ${apiUrl}:`, parseError, responseData);
reject(new Error(`Failed to parse API response: ${parseError.message}`));
}
});
});
apiReq.on('timeout', () => {
apiReq.destroy(); // Destroy the request to free up resources
console.error(`API request to ${apiUrl} timed out after ${API_TIMEOUT_MS}ms`);
reject(new Error('API request timed out'));
});
apiReq.on('error', (networkError) => {
console.error(`API request to ${apiUrl} failed:`, networkError);
reject(networkError);
});
apiReq.end();
});
}
// 从真实 URL 获取数据并写入缓存
const REAL_URL_FETCH_TIMEOUT_MS = 0; // 0 means no timeout for the actual file download
const fetchAndServe = async (data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res) => {
const protocol = data.realUrl.startsWith('https:') ? https : http;
protocol.get(data.realUrl, { timeout: REAL_URL_FETCH_TIMEOUT_MS, rejectUnauthorized: false }, (realRes) => {
const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: 'w', highWaterMark: 64 * 1024 }); // 增加缓冲区大小到64KB
let isVideo = data.path && typeof data.path === 'string' && data.path.includes('.mp4');
// 确保 content-length 是有效的
const contentLength = realRes.headers['content-length'];
if (contentLength) {
// contentLength 小于 2KB 且与缓存文件大小不一致时,重新获取
if (contentLength < 2048 && data.headers['content-length'] !== contentLength) {
console.warn('Warning: content-length is different for the response from:', data.realUrl);
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, `Bad Gateway: Content-Length mismatch for ${data.realUrl}`);
// Clean up temp file if stream hasn't started or failed early
fs.promises.access(tempCacheContentFile)
.then(() => fs.promises.unlink(tempCacheContentFile))
.catch(() => {}); // 忽略文件不存在的错误
return;
}
data.headers['content-length'] = contentLength;
// 异步更新 data 到缓存 cacheMetaFile
fs.promises.writeFile(cacheMetaFile, JSON.stringify(data))
.catch(err => console.error(`Error writing meta file ${cacheMetaFile}:`, err));
} else {
console.warn('Warning: content-length is undefined for the response from:', data.realUrl);
}
const baseHeaders = {
'Cloud-Type': data.cloudtype,
'Cloud-Expiration': data.expiration,
'ETag': data.uniqid || '',
'Cache-Control': 'public, max-age=31536000', // 1 year
'Expires': new Date(Date.now() + 31536000000).toUTCString(),
'Accept-Ranges': 'bytes',
'Connection': 'keep-alive',
'Date': new Date().toUTCString(),
'Last-Modified': data.headers['last-modified'] || new Date().toUTCString(),
};
const responseHeaders = {
...baseHeaders,
'Content-Type': realRes.headers['content-type'] || (isVideo ? 'video/mp4' : 'application/octet-stream'),
...data.headers,
};
res.writeHead(realRes.statusCode, responseHeaders);
// 使用管道优化流传输
const pipeline = require('stream').pipeline;
// 创建一个流分支,同时写入缓存和响应
const { PassThrough } = require('stream');
const passThrough = new PassThrough();
passThrough.pipe(cacheStream);
passThrough.pipe(res);
// 使用pipeline处理流错误
pipeline(
realRes,
passThrough,
(err) => {
if (err) {
console.error(`Pipeline error for ${data.realUrl}:`, err);
handleResponseError(res, tempCacheContentFile, data.realUrl);
return;
}
// 流处理完成后,重命名临时文件
fs.promises.access(tempCacheContentFile)
.then(() => {
// 确保目标目录存在
return fs.promises.mkdir(pathModule.dirname(cacheContentFile), { recursive: true })
.then(() => fs.promises.rename(tempCacheContentFile, cacheContentFile))
.then(() => console.log(`Successfully cached: ${cacheContentFile}`))
.catch(renameError => {
console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameError);
return fs.promises.unlink(tempCacheContentFile).catch(() => {});
});
})
.catch(() => {
console.warn(`Temp cache file ${tempCacheContentFile} not found after stream end for ${data.realUrl}`);
});
}
);
}).on('error', (requestError) => {
console.error(`Error making GET request to ${data.realUrl}:`, requestError);
handleResponseError(res, tempCacheContentFile, data.realUrl);
});
};
// 从缓存中读取数据并返回
async function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, reqPath, token, sign, uniqidhex) {
if (!cacheData) { // 缓存数据不可用,尝试重新获取
console.warn(`Cache metadata unavailable for ${cacheContentFile}, attempting to fetch fresh data`);
// 如果提供了请求参数,尝试重新获取数据
if (reqPath && token) {
try {
viewsInfo.increment('apiCall');
const apiData = await fetchApiData(reqPath, token, sign);
if (apiData.code === HTTP_STATUS.REDIRECT || apiData.code === 301) {
res.writeHead(HTTP_STATUS.REDIRECT, { Location: apiData.data.url });
res.end();
return;
}
if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) {
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb };
// 更新索引
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
// 写入新的元数据
await fs.promises.mkdir(pathModule.dirname(cacheMetaFile), { recursive: true });
await fs.promises.writeFile(cacheMetaFile, JSON.stringify(data));
// 获取并提供新数据
const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`);
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
return;
} else {
viewsInfo.increment('fetchApiWarning');
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API');
return;
}
} catch (error) {
viewsInfo.increment('fetchApiError');
console.error('Error fetching fresh data:', error);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${error.message}`);
return;
}
} else {
// 如果没有提供请求参数,无法重新获取
console.error(`serveFromCache called with null cacheData and insufficient request info for ${cacheContentFile}`);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Cache metadata unavailable and cannot fetch fresh data.');
return;
}
}
// 使用异步方式获取ETag和Last-Modified
let etag = cacheData.uniqid;
let lastModified = cacheData.headers && cacheData.headers['last-modified'];
if (!etag || !lastModified) {
try {
const [fileStats, metaStats] = await Promise.all([
fs.promises.stat(cacheContentFile).catch(() => null),
fs.promises.stat(cacheMetaFile).catch(() => null)
]);
if (!etag && fileStats) {
// 使用文件大小和修改时间作为ETag的一部分避免读取整个文件计算MD5
etag = crypto.createHash('md5')
.update(`${fileStats.size}-${fileStats.mtime.getTime()}`)
.digest('hex');
}
if (!lastModified && metaStats) {
lastModified = new Date(metaStats.mtime).toUTCString();
}
} catch (error) {
console.warn(`Error getting file stats for cache: ${error.message}`);
}
}
const baseHeaders = {
'Cloud-Type': cacheData.cloudtype || 'unknown',
'Cloud-Expiration': cacheData.expiration || 'N/A',
'ETag': etag || '',
'Cache-Control': 'public, max-age=31536000', // 1 year
'Expires': new Date(Date.now() + 31536000000).toUTCString(),
'Accept-Ranges': 'bytes',
'Connection': 'keep-alive',
'Date': new Date().toUTCString(),
'Last-Modified': lastModified || new Date().toUTCString(),
};
viewsInfo.increment('cacheCall');
// 先检查缓存文件是否存在且可读
try {
await fs.promises.access(cacheContentFile, fs.constants.R_OK);
} catch (error) {
console.warn(`Cache content file ${cacheContentFile} not accessible: ${error.message}`);
// 如果提供了请求参数,尝试重新获取数据
if (reqPath && token) {
console.log(`Attempting to fetch fresh data for ${cacheContentFile}`);
try {
viewsInfo.increment('apiCall');
const apiData = await fetchApiData(reqPath, token, sign);
if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) {
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb };
// 更新索引
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
// 获取并提供新数据
const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`);
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
return;
} else {
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API');
return;
}
} catch (fetchError) {
console.error(`Error fetching fresh data: ${fetchError.message}`);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${fetchError.message}`);
return;
}
} else {
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Unable to read cache content file and cannot fetch fresh data.');
return;
}
}
const readStream = fs.createReadStream(cacheContentFile, { highWaterMark: 64 * 1024 }); // 增加读取缓冲区大小
const isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4');
let currentContentLength = cacheData.headers && cacheData.headers['content-length'] ? parseInt(cacheData.headers['content-length'], 10) : 0;
if (!currentContentLength || currentContentLength === 0) {
try {
const stats = fs.statSync(cacheContentFile);
currentContentLength = stats.size;
if (currentContentLength > 0) {
if (!cacheData.headers) cacheData.headers = {};
cacheData.headers['content-length'] = currentContentLength.toString();
// Update meta file if content-length was missing or zero
fs.writeFileSync(cacheMetaFile, JSON.stringify(cacheData));
console.log(`Updated content-length in ${cacheMetaFile} to ${currentContentLength}`);
} else {
console.warn(`Cached content file ${cacheContentFile} has size 0 or stat failed.`);
// Potentially treat as an error or serve as is if 0 length is valid for some files
}
} catch (statError) {
console.error(`Error stating cache content file ${cacheContentFile}:`, statError);
handleCacheReadError(res, cacheContentFile, reqPath, token, sign, uniqidhex); // Treat stat error as read error
return;
}
}
readStream.on('open', () => {
const responseHeaders = {
...baseHeaders,
'Content-Type': (cacheData.headers && cacheData.headers['content-type']) || (isVideo ? 'video/mp4' : 'application/octet-stream'),
// Merge other headers from cacheData.headers, letting them override base if necessary
// but ensure our critical headers like Content-Length (if updated) are preserved.
...(cacheData.headers || {}),
};
res.writeHead(HTTP_STATUS.OK, responseHeaders);
readStream.pipe(res);
});
readStream.on('error', (err) => {
console.error(`Read stream error for ${cacheContentFile}:`, err);
// 如果提供了请求参数,尝试重新获取数据而不是直接报错
if (reqPath && token) {
console.log(`Read stream error, attempting to fetch fresh data for ${cacheContentFile}`);
viewsInfo.increment('apiCall');
fetchApiData(reqPath, token, sign)
.then(apiData => {
if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) {
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb };
// 更新索引
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
// 获取并提供新数据
const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`);
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
} else {
viewsInfo.increment('fetchApiWarning');
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API');
}
})
.catch(fetchError => {
viewsInfo.increment('fetchApiError');
console.error(`Error fetching fresh data after read stream error: ${fetchError.message}`);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${fetchError.message}`);
});
} else {
// 如果没有提供请求参数,使用原始的错误处理
handleCacheReadError(res, cacheContentFile, reqPath, token, sign, uniqidhex);
}
});
// Handle cases where client closes connection prematurely
res.on('close', () => {
if (!res.writableEnded) {
console.log(`Client closed connection prematurely for ${cacheContentFile}. Destroying read stream.`);
readStream.destroy();
}
});
}
// 处理响应错误
const handleResponseError = (res, tempCacheContentFile, realUrl) => {
viewsInfo.increment('fetchApiError');
console.error(`Error fetching from real URL: ${realUrl}`);
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, `Bad Gateway: Failed to fetch from ${realUrl}`);
if (fs.existsSync(tempCacheContentFile)) {
try {
fs.unlinkSync(tempCacheContentFile);
} catch (unlinkErr) {
console.error(`Error unlinking temp file ${tempCacheContentFile}:`, unlinkErr);
}
}
};
// 处理缓存读取错误
const handleCacheReadError = (res, filePath, reqPath, token, sign, uniqidhex) => {
viewsInfo.increment('cacheReadError');
console.error(`Error reading cache file: ${filePath}`);
// 如果提供了请求参数,尝试重新获取数据
if (reqPath && token) {
console.log(`Cache read error, attempting to fetch fresh data for ${filePath}`);
viewsInfo.increment('apiCall');
fetchApiData(reqPath, token, sign)
.then(apiData => {
if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) {
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb };
// 更新索引
if (uniqidhex) {
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
}
// 获取并提供新数据
const cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`);
const cacheContentFile = pathModule.join(cacheDir, `${data.uniqid}.content`);
const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`);
// 写入新的元数据
fs.promises.mkdir(pathModule.dirname(cacheMetaFile), { recursive: true })
.then(() => fs.promises.writeFile(cacheMetaFile, JSON.stringify(data)))
.then(() => {
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
})
.catch(writeError => {
console.error(`Error writing meta file after cache read error: ${writeError.message}`);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Failed to write cache metadata');
});
} else {
viewsInfo.increment('fetchApiWarning');
sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API');
}
})
.catch(fetchError => {
viewsInfo.increment('fetchApiError');
console.error(`Error fetching fresh data after cache read error: ${fetchError.message}`);
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${fetchError.message}`);
});
} else {
// 如果没有提供请求参数,返回错误
sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Internal Server Error: Unable to read cache content file');
}
};
// 启动服务器
server.listen(port, () => {
console.log(`Proxy server is running on http://localhost:${port}`);
});
// 处理 SIGINT 信号Ctrl+C
process.on('SIGINT', () => {
console.log('Received SIGINT. Shutting down gracefully...');
server.close(() => {
console.log('Server closed.');
process.exit(0);
});
setTimeout(() => {
console.error('Forcing shutdown...');
process.exit(1);
}, 10000);
});