Files
alist-proxy/proxy.js
2025-09-05 17:02:19 +08:00

1213 lines
46 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/';
// 创建缓存目录结构
const cacheDir = pathModule.join(__dirname, CACHE_DIR_NAME);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
console.log(`Cache directory created: ${cacheDir}`);
}
// 根据缓存键创建子目录,避免单个目录文件过多
function getCacheSubDir(cacheKey) {
// 使用缓存键的前两个字符作为子目录名
const subDir = cacheKey.substring(0, 2);
const fullPath = pathModule.join(cacheDir, subDir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
}
return fullPath;
}
// 缓存配置
const META_CACHE_EXPIRY_MS = 30 * 60 * 1000; // 30分钟
const metaCache = {}; // 存储meta信息的内存缓存
const contentCache = {}; // 存储内容的内存索引
// 下载任务管理和并发控制
const MAX_CONCURRENT_DOWNLOADS = 5; // 最大并发下载数
const MAX_QUEUE_SIZE = 50; // 最大队列长度
const MAX_CLIENTS_PER_TASK = 10; // 每个任务最大客户端数
const REQUEST_TIMEOUT = 30000; // 请求超时时间30秒
const downloadTasks = new Map(); // 下载任务管理器
const downloadQueue = []; // 下载队列
let activeDownloads = 0; // 当前活跃下载数
// 服务器启动时间
const serverStartTime = Date.now();
// 下载任务状态
const DOWNLOAD_STATUS = {
PENDING: 'pending',
DOWNLOADING: 'downloading',
COMPLETED: 'completed',
FAILED: 'failed',
PAUSED: 'paused'
};
// 下载任务类
class DownloadTask {
constructor(url, cacheKey, metaCacheFile, contentCacheFile) {
this.id = `${cacheKey}_${Date.now()}`;
this.url = url;
this.cacheKey = cacheKey;
this.metaCacheFile = metaCacheFile;
this.contentCacheFile = contentCacheFile;
this.status = DOWNLOAD_STATUS.PENDING;
this.waitingClients = [];
this.error = null;
this.createdAt = Date.now();
// 进度跟踪
this.progress = {
totalSize: 0,
downloadedSize: 0,
percentage: 0,
speed: 0, // bytes/second
eta: 0, // estimated time remaining in seconds
startTime: null,
lastUpdateTime: null,
lastDownloadedSize: 0
};
// 状态管理
this.retryCount = 0;
this.maxRetries = 3;
this.lastError = null;
this.pauseRequested = false;
}
addClient(res, rangeHeader) {
this.waitingClients.push({ res, rangeHeader, addedAt: Date.now() });
}
removeClient(res) {
this.waitingClients = this.waitingClients.filter(client => client.res !== res);
}
hasClients() {
return this.waitingClients.length > 0;
}
// 更新下载进度
updateProgress(downloadedBytes, totalBytes) {
const now = Date.now();
if (!this.progress.startTime) {
this.progress.startTime = now;
}
this.progress.totalSize = totalBytes;
this.progress.downloadedSize = downloadedBytes;
this.progress.percentage = totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : 0;
// 计算下载速度
if (this.progress.lastUpdateTime) {
const timeDiff = (now - this.progress.lastUpdateTime) / 1000; // seconds
const sizeDiff = downloadedBytes - this.progress.lastDownloadedSize;
if (timeDiff > 0) {
this.progress.speed = sizeDiff / timeDiff;
// 计算预计剩余时间
const remainingBytes = totalBytes - downloadedBytes;
this.progress.eta = this.progress.speed > 0 ? remainingBytes / this.progress.speed : 0;
}
}
this.progress.lastUpdateTime = now;
this.progress.lastDownloadedSize = downloadedBytes;
}
// 获取进度信息
getProgressInfo() {
return {
id: this.id,
url: this.url,
status: this.status,
progress: { ...this.progress },
clientCount: this.waitingClients.length,
retryCount: this.retryCount,
error: this.lastError,
createdAt: this.createdAt
};
}
// 设置错误状态
setError(error) {
this.lastError = error;
this.error = error;
this.status = DOWNLOAD_STATUS.FAILED;
}
// 请求暂停
requestPause() {
this.pauseRequested = true;
}
// 检查是否应该重试
shouldRetry() {
return this.retryCount < this.maxRetries && this.status === DOWNLOAD_STATUS.FAILED;
}
// 增加重试次数
incrementRetry() {
this.retryCount++;
}
}
// 下载任务管理器函数
function getOrCreateDownloadTask(url, cacheKey, metaCacheFile, contentCacheFile) {
let task = downloadTasks.get(cacheKey);
if (!task) {
task = new DownloadTask(url, cacheKey, metaCacheFile, contentCacheFile);
downloadTasks.set(cacheKey, task);
}
return task;
}
function startNextDownload() {
if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) {
return;
}
const task = downloadQueue.shift();
if (task && task.status === DOWNLOAD_STATUS.PENDING) {
activeDownloads++;
task.status = DOWNLOAD_STATUS.DOWNLOADING;
console.log(`Starting download: ${task.url} (${activeDownloads}/${MAX_CONCURRENT_DOWNLOADS})`);
executeDownload(task);
}
}
function completeDownload(task, success = true) {
activeDownloads = Math.max(0, activeDownloads - 1);
task.status = success ? DOWNLOAD_STATUS.COMPLETED : DOWNLOAD_STATUS.FAILED;
if (!success) {
// 清理失败的下载任务
downloadTasks.delete(task.cacheKey);
}
console.log(`Download ${success ? 'completed' : 'failed'}: ${task.url} (${activeDownloads}/${MAX_CONCURRENT_DOWNLOADS})`);
// 启动下一个下载任务
setTimeout(startNextDownload, 100);
}
function cleanupExpiredTasks() {
const now = Date.now();
const expiredTasks = [];
downloadTasks.forEach((task, key) => {
// 清理超过1小时且没有客户端的已完成任务
if (task.status === DOWNLOAD_STATUS.COMPLETED &&
!task.hasClients() &&
(now - task.startTime) > 3600000) {
expiredTasks.push(key);
}
// 清理超过10分钟的失败任务
else if (task.status === DOWNLOAD_STATUS.FAILED &&
(now - task.startTime) > 600000) {
expiredTasks.push(key);
}
});
expiredTasks.forEach(key => {
downloadTasks.delete(key);
});
if (expiredTasks.length > 0) {
console.log(`Cleaned up ${expiredTasks.length} expired download tasks`);
}
}
// 定期清理过期任务
setInterval(cleanupExpiredTasks, 300000); // 每5分钟清理一次
// 创建HTTP服务器
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const clientIP = req.connection.remoteAddress || req.socket.remoteAddress || '127.0.0.1';
// 处理favicon请求
if (path === '/favicon.ico') {
res.writeHead(204);
res.end();
return;
}
// 处理状态查询请求
if (path === '/status') {
const tasks = Array.from(downloadTasks.values()).map(task => task.getProgressInfo());
const status = {
activeDownloads,
queueLength: downloadQueue.length,
totalTasks: downloadTasks.size,
tasks: tasks.slice(0, 20), // 只返回前20个任务
serverInfo: {
maxConcurrentDownloads: MAX_CONCURRENT_DOWNLOADS,
maxQueueSize: MAX_QUEUE_SIZE,
maxClientsPerTask: MAX_CLIENTS_PER_TASK,
requestTimeout: REQUEST_TIMEOUT,
uptime: Date.now() - serverStartTime
}
};
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify(status, null, 2));
return;
}
// 处理任务控制请求
if (path.startsWith('/control/')) {
const action = path.split('/')[2]; // pause, resume, cancel
const taskId = parsedUrl.query.taskId;
if (!taskId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing taskId parameter' }));
return;
}
const task = downloadTasks.get(taskId);
if (!task) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Task not found' }));
return;
}
let result = { success: false, message: '' };
switch (action) {
case 'pause':
if (task.status === DOWNLOAD_STATUS.DOWNLOADING) {
task.requestPause();
result = { success: true, message: 'Pause requested' };
} else {
result = { success: false, message: 'Task is not downloading' };
}
break;
case 'resume':
if (task.status === DOWNLOAD_STATUS.PAUSED || task.status === DOWNLOAD_STATUS.FAILED) {
task.status = DOWNLOAD_STATUS.PENDING;
task.pauseRequested = false;
if (!downloadQueue.includes(task)) {
downloadQueue.push(task);
}
startNextDownload();
result = { success: true, message: 'Resume requested' };
} else {
result = { success: false, message: 'Task cannot be resumed' };
}
break;
case 'cancel':
task.requestPause();
task.status = DOWNLOAD_STATUS.FAILED;
task.setError('Cancelled by user');
// 通知所有等待的客户端
task.waitingClients.forEach(({ res: clientRes }) => {
if (!clientRes.headersSent && !clientRes.destroyed) {
clientRes.writeHead(499, { 'Content-Type': 'text/plain' });
clientRes.end('Download cancelled');
}
});
task.waitingClients = [];
result = { success: true, message: 'Task cancelled' };
break;
default:
result = { success: false, message: 'Invalid action' };
}
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify(result));
return;
}
// 设置请求超时
const timeout = setTimeout(() => {
if (!res.headersSent) {
res.writeHead(408, { 'Content-Type': 'text/plain' });
res.end('Request Timeout');
}
}, REQUEST_TIMEOUT);
// 设置CORS头自动获取Origin
const origin = req.headers.origin || req.headers.referer || '*';
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, HEAD');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
// 处理预检请求
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// 生成缓存键并创建分层目录结构
const cacheKey = crypto.createHash('md5').update(req.url).digest('hex');
const cacheSubDir = getCacheSubDir(cacheKey);
const metaCacheFile = pathModule.join(cacheSubDir, `${cacheKey}.meta`);
const contentCacheFile = pathModule.join(cacheSubDir, `${cacheKey}.content`);
// 检查meta缓存是否有效
const now = Date.now();
let metaInfo = metaCache[cacheKey];
let metaExpired = !metaInfo || (now - metaInfo.timestamp > META_CACHE_EXPIRY_MS);
// 如果meta缓存过期尝试从文件读取
if (metaExpired && fs.existsSync(metaCacheFile)) {
try {
const metaData = JSON.parse(fs.readFileSync(metaCacheFile, 'utf8'));
if (now - metaData.timestamp <= META_CACHE_EXPIRY_MS) {
metaInfo = metaData;
metaCache[cacheKey] = metaInfo;
metaExpired = false;
console.log(`Meta cache loaded from file for: ${req.url}`);
}
} catch (err) {
console.error(`Error reading meta cache file: ${err.message}`);
}
}
// 如果meta缓存有效且内容缓存存在直接返回缓存内容
if (!metaExpired && fs.existsSync(contentCacheFile)) {
console.log(`Serving from cache: ${req.url}`);
const rangeHeader = req.headers.range;
// 检查是否可以从完整缓存文件提供Range请求
if (rangeHeader && fs.existsSync(contentCacheFile)) {
try {
const stat = fs.statSync(contentCacheFile);
const expectedSize = parseInt(metaInfo.headers['content-length'] || '0', 10);
if (stat.size === expectedSize) {
const ranges = parseRange(rangeHeader, expectedSize);
if (ranges) {
console.log(`Serving range request from complete cache: ${contentCacheFile}`);
serveFromCache(res, metaInfo, contentCacheFile, rangeHeader, req);
return;
}
} else if (stat.size > 0 && stat.size < expectedSize) {
// 部分下载的文件,检查是否可以提供请求的范围
const ranges = parseRange(rangeHeader, expectedSize);
if (ranges && !Array.isArray(ranges)) {
const { start, end } = ranges;
if (end < stat.size) {
// 请求的范围在已下载的部分内
console.log(`Serving range request from partial cache: ${contentCacheFile}`);
serveFromCache(res, metaInfo, contentCacheFile, rangeHeader);
return;
}
}
}
} catch (err) {
console.error(`Error checking cached file for range request: ${err.message}`);
}
}
serveFromCache(res, metaInfo, contentCacheFile, rangeHeader, req);
return;
}
// 需要请求原始API - 使用下载任务管理器
console.log(`Fetching from API: ${req.url}`);
const rangeHeader = req.headers.range;
// 获取或创建下载任务
const task = getOrCreateDownloadTask(req.url, cacheKey, metaCacheFile, contentCacheFile);
// 检查队列是否已满
if (downloadQueue.length >= MAX_QUEUE_SIZE && task.status === DOWNLOAD_STATUS.PENDING) {
console.log(`Download queue full, rejecting request: ${req.url}`);
clearTimeout(timeout);
res.writeHead(503, { 'Content-Type': 'text/plain' });
res.end('Service Unavailable - Queue Full');
return;
}
// 检查任务客户端数量限制
if (task.waitingClients.length >= MAX_CLIENTS_PER_TASK) {
console.log(`Too many clients for task, rejecting: ${req.url}`);
clearTimeout(timeout);
res.writeHead(503, { 'Content-Type': 'text/plain' });
res.end('Service Unavailable - Too Many Clients');
return;
}
// 添加客户端到等待列表
task.addClient(res, rangeHeader);
// 处理客户端断开连接
res.on('close', () => {
clearTimeout(timeout);
task.removeClient(res);
console.log(`Client disconnected from task: ${task.id}`);
});
// 处理响应完成
res.on('finish', () => {
clearTimeout(timeout);
});
if (task.status === DOWNLOAD_STATUS.COMPLETED) {
// 任务已完成,直接从缓存提供内容
console.log(`Serving completed download from cache: ${req.url}`);
serveFromCache(res, metaCache[cacheKey], contentCacheFile, rangeHeader, req);
task.removeClient(res);
} else if (task.status === DOWNLOAD_STATUS.DOWNLOADING) {
// 任务正在下载中,客户端等待
console.log(`Client waiting for ongoing download: ${req.url}`);
} else if (task.status === DOWNLOAD_STATUS.PENDING) {
// 将任务加入下载队列
if (!downloadQueue.includes(task)) {
downloadQueue.push(task);
console.log(`Added to download queue: ${req.url}`);
}
startNextDownload();
} else if (task.status === DOWNLOAD_STATUS.FAILED) {
// 任务失败,重新尝试
task.status = DOWNLOAD_STATUS.PENDING;
task.error = null;
if (!downloadQueue.includes(task)) {
downloadQueue.push(task);
}
startNextDownload();
}
});
// 从缓存提供内容支持多段Range和HTTP缓存
function serveFromCache(res, metaInfo, contentCacheFile, rangeHeader, req) {
// 设置响应头保持CORS头
const headers = { ...metaInfo.headers };
// 确保CORS头不被覆盖
if (!headers['access-control-allow-origin']) {
headers['access-control-allow-origin'] = res.getHeader('access-control-allow-origin') || '*';
}
// 确保Content-Type正确设置特别是对于视频文件
if (!headers['content-type']) {
const ext = pathModule.extname(contentCacheFile).toLowerCase();
const mimeTypes = {
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.ogg': 'video/ogg',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.wmv': 'video/x-ms-wmv',
'.flv': 'video/x-flv',
'.mkv': 'video/x-matroska',
'.m4v': 'video/mp4',
'.3gp': 'video/3gpp',
'.ts': 'video/mp2t'
};
headers['content-type'] = mimeTypes[ext] || 'application/octet-stream';
}
// 确保Accept-Ranges头存在这对视频播放很重要
headers['accept-ranges'] = 'bytes';
// 处理Range请求
if (rangeHeader && fs.existsSync(contentCacheFile)) {
const stat = fs.statSync(contentCacheFile);
const fileSize = stat.size;
// 生成ETag和Last-Modified
const etag = `"${stat.mtime.getTime()}-${fileSize}"`;
const lastModified = stat.mtime.toUTCString();
// 设置缓存相关头
headers['etag'] = etag;
headers['last-modified'] = lastModified;
headers['cache-control'] = 'public, max-age=3600, must-revalidate'; // 1小时缓存必须重新验证
headers['vary'] = 'Accept-Encoding, Range'; // 根据编码和范围请求变化
// 检查条件请求
const ifNoneMatch = req && req.headers['if-none-match'];
const ifModifiedSince = req && req.headers['if-modified-since'];
// 304 Not Modified 检查
if ((ifNoneMatch && ifNoneMatch === etag) ||
(ifModifiedSince && new Date(ifModifiedSince) >= stat.mtime)) {
res.writeHead(304, headers);
res.end();
return;
}
// 解析Range头
const ranges = parseRange(rangeHeader, fileSize);
if (ranges) {
// 单段Range请求
if (!Array.isArray(ranges)) {
const { start, end } = ranges;
const chunkSize = (end - start) + 1;
// 设置206 Partial Content响应头
headers['content-range'] = `bytes ${start}-${end}/${fileSize}`;
headers['accept-ranges'] = 'bytes';
headers['content-length'] = chunkSize;
res.writeHead(206, headers);
// 创建范围读取流
const fileStream = fs.createReadStream(contentCacheFile, { start, end });
fileStream.pipe(res);
fileStream.on('error', (err) => {
console.error(`Error reading cache file: ${err.message}`);
res.statusCode = 500;
res.end('Internal Server Error');
});
return;
}
// 多段Range请求 - 暂时不支持,返回完整文件
console.log('Multi-range request detected, serving full file instead');
}
}
// 普通请求或Range解析失败
const stat = fs.statSync(contentCacheFile);
// 生成ETag和Last-Modified
const etag = `"${stat.mtime.getTime()}-${stat.size}"`;
const lastModified = stat.mtime.toUTCString();
// 设置缓存相关头
headers['etag'] = etag;
headers['last-modified'] = lastModified;
headers['cache-control'] = 'public, max-age=3600, must-revalidate'; // 1小时缓存必须重新验证
headers['vary'] = 'Accept-Encoding, Range'; // 根据编码和范围请求变化
// 检查条件请求
const ifNoneMatch = req && req.headers['if-none-match'];
const ifModifiedSince = req && req.headers['if-modified-since'];
// 304 Not Modified 检查
if ((ifNoneMatch && ifNoneMatch === etag) ||
(ifModifiedSince && new Date(ifModifiedSince) >= stat.mtime)) {
res.writeHead(304, headers);
res.end();
return;
}
res.writeHead(200, headers);
// 创建文件读取流并直接管道到响应
const fileStream = fs.createReadStream(contentCacheFile);
fileStream.pipe(res);
// 处理错误
fileStream.on('error', (err) => {
console.error(`Error reading cache file: ${err.message}`);
res.statusCode = 500;
res.end('Internal Server Error');
});
}
// 获取Content-Type
function getContentType(url) {
const ext = pathModule.extname(url).toLowerCase();
const mimeTypes = {
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.ogg': 'video/ogg',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.wmv': 'video/x-ms-wmv',
'.flv': 'video/x-flv',
'.mkv': 'video/x-matroska',
'.m4v': 'video/mp4',
'.3gp': 'video/3gpp',
'.ts': 'video/mp2t'
};
return mimeTypes[ext] || 'application/octet-stream';
}
// 解析Range头支持多段Range
function parseRange(rangeHeader, fileSize) {
if (!rangeHeader || !rangeHeader.startsWith('bytes=')) {
return null;
}
const rangeSpec = rangeHeader.substring(6);
const ranges = [];
// 支持多个范围,如 bytes=0-499,1000-1499
const rangeParts = rangeSpec.split(',');
for (const part of rangeParts) {
const trimmed = part.trim();
const [start, end] = trimmed.split('-').map(s => s ? parseInt(s, 10) : undefined);
let rangeStart, rangeEnd;
if (start !== undefined && end !== undefined) {
// 完整范围: 0-499
rangeStart = start;
rangeEnd = end;
} else if (start !== undefined && end === undefined) {
// 从某位置到结尾: 500-
rangeStart = start;
rangeEnd = fileSize - 1;
} else if (start === undefined && end !== undefined) {
// 最后几个字节: -500
rangeStart = Math.max(0, fileSize - end);
rangeEnd = fileSize - 1;
} else {
continue; // 无效范围
}
// 验证范围有效性
if (rangeStart >= fileSize || rangeEnd >= fileSize || rangeStart > rangeEnd || rangeStart < 0) {
continue;
}
ranges.push({ start: rangeStart, end: rangeEnd });
}
return ranges.length > 0 ? (ranges.length === 1 ? ranges[0] : ranges) : null;
}
// 执行下载任务
function executeDownload(task) {
const targetUrl = new URL(task.url, DEFAULT_API_ENDPOINT);
const protocol = targetUrl.protocol === 'https:' ? https : http;
const options = {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; AlistProxy/1.0)'
}
};
// 检查是否有缓存文件,如果有则添加条件请求头
if (fs.existsSync(task.contentCacheFile)) {
try {
const stat = fs.statSync(task.contentCacheFile);
const etag = `"${stat.mtime.getTime()}-${stat.size}"`;
const lastModified = stat.mtime.toUTCString();
// 添加条件请求头,让上游服务器判断是否需要重新下载
options.headers['if-none-match'] = etag;
options.headers['if-modified-since'] = lastModified;
} catch (err) {
console.log(`Error reading cache file stats: ${err.message}`);
}
}
const proxyReq = protocol.request(targetUrl, options, (proxyRes) => {
console.log(`Response status: ${proxyRes.statusCode} for ${task.url}`);
// 处理304 Not Modified响应
if (proxyRes.statusCode === 304) {
console.log(`304 Not Modified, serving from cache: ${task.url}`);
// 从缓存提供内容
if (fs.existsSync(task.contentCacheFile) && fs.existsSync(task.metaCacheFile)) {
try {
const cachedMeta = JSON.parse(fs.readFileSync(task.metaCacheFile, 'utf8'));
// 为所有等待的客户端提供缓存内容
task.waitingClients.forEach(({ res: clientRes, rangeHeader }) => {
if (!clientRes.headersSent && !clientRes.destroyed) {
serveFromCache(clientRes, cachedMeta, task.contentCacheFile, rangeHeader, null);
}
});
task.status = DOWNLOAD_STATUS.COMPLETED;
completeDownload(task, true);
return;
} catch (err) {
console.error(`Error serving 304 from cache: ${err.message}`);
}
}
// 如果缓存文件不存在,继续正常下载
console.log(`Cache file missing for 304 response, continuing download`);
}
const metaInfo = {
timestamp: Date.now(),
headers: proxyRes.headers,
statusCode: proxyRes.statusCode
};
// 更新内存缓存
metaCache[task.cacheKey] = metaInfo;
// 写入meta缓存文件
fs.writeFile(task.metaCacheFile, JSON.stringify(metaInfo), (err) => {
if (err) console.error(`Error writing meta cache: ${err.message}`);
});
if (proxyRes.statusCode >= 200 && proxyRes.statusCode < 300) {
const contentLength = parseInt(proxyRes.headers['content-length'] || '0', 10);
task.updateProgress(0, contentLength);
// 创建临时文件用于断点续传
const tempFile = `${task.contentCacheFile}.tmp`;
const fileStream = fs.createWriteStream(tempFile);
let downloadedBytes = 0;
// 进度跟踪变量
let lastLoggedSize = 0;
let lastLogTime = Date.now();
// 监听数据流
proxyRes.on('data', (chunk) => {
downloadedBytes += chunk.length;
task.updateProgress(downloadedBytes, contentLength);
// 每1MB或每5秒输出一次进度日志
const now = Date.now();
if (downloadedBytes - lastLoggedSize >= 1024 * 1024 || now - lastLogTime >= 5000) {
const progress = task.getProgressInfo();
console.log(`Download progress [${task.id}]: ${progress.progress.percentage.toFixed(1)}% ` +
`(${formatBytes(progress.progress.downloadedSize)}/${formatBytes(progress.progress.totalSize)}) ` +
`Speed: ${formatBytes(progress.progress.speed)}/s ` +
`ETA: ${formatTime(progress.progress.eta)}`);
lastLoggedSize = downloadedBytes;
lastLogTime = now;
}
// 检查是否请求暂停
if (task.pauseRequested) {
console.log(`Download paused for task: ${task.id}`);
task.status = DOWNLOAD_STATUS.PAUSED;
proxyRes.destroy();
if (fileStream && !fileStream.destroyed) {
fileStream.end();
}
return;
}
// 检查等待的Range请求客户端
const clientsToServe = [];
task.waitingClients.forEach((client, index) => {
if (client.res.destroyed) {
return;
}
if (client.rangeHeader) {
const range = parseRange(client.rangeHeader, contentLength);
if (range && !Array.isArray(range)) {
const { start, end } = range;
// 检查请求的范围是否已经下载完成
if (downloadedBytes > end) {
console.log(`Serving range to waiting client: ${start}-${end} (downloaded: ${downloadedBytes})`);
clientsToServe.push({ client, range, index });
}
}
}
});
// 为可以服务的客户端提供Range响应
clientsToServe.forEach(({ client, range, index }) => {
try {
const { start, end } = range;
const chunkSize = (end - start) + 1;
// 设置响应头
const headers = {
'content-type': getContentType(task.url),
'accept-ranges': 'bytes',
'content-range': `bytes ${start}-${end}/${contentLength}`,
'content-length': chunkSize,
'access-control-allow-origin': '*',
'cache-control': 'public, max-age=3600'
};
client.res.writeHead(206, headers);
// 直接从临时文件读取指定范围的数据
const rangeStream = fs.createReadStream(tempFile, { start, end });
rangeStream.pipe(client.res);
rangeStream.on('error', (err) => {
console.error(`Error reading range from temp file: ${err.message}`);
if (!client.res.headersSent) {
client.res.writeHead(500);
client.res.end('Internal Server Error');
}
});
// 从等待列表中移除该客户端
task.waitingClients.splice(index, 1);
console.log(`Served range ${start}-${end} from temp file to waiting client`);
} catch (err) {
console.error(`Error serving range to client: ${err.message}`);
}
});
// 向非Range请求的等待客户端发送数据
task.waitingClients.forEach(client => {
if (!client.res.destroyed && !client.rangeHeader) {
// 设置响应头(仅第一次)
if (!client.headersSent) {
const headers = { ...proxyRes.headers };
if (!headers['access-control-allow-origin']) {
headers['access-control-allow-origin'] = client.res.getHeader('access-control-allow-origin') || '*';
}
headers['accept-ranges'] = 'bytes';
client.res.writeHead(200, headers);
client.headersSent = true;
}
// 发送数据块
try {
client.res.write(chunk);
} catch (err) {
console.error(`Error writing to client: ${err.message}`);
task.removeClient(client.res);
}
}
});
});
// 将数据写入文件
proxyRes.pipe(fileStream);
proxyRes.on('end', () => {
// 结束所有客户端响应
task.waitingClients.forEach(client => {
if (!client.res.destroyed) {
try {
client.res.end();
} catch (err) {
console.error(`Error ending client response: ${err.message}`);
}
}
});
task.waitingClients = [];
// 完成文件写入
fileStream.end();
});
fileStream.on('finish', () => {
fs.rename(tempFile, task.contentCacheFile, (err) => {
if (err) {
console.error(`Error saving cache file: ${err.message}`);
completeDownload(task, false);
} else {
console.log(`Cache saved for: ${task.url}`);
completeDownload(task, true);
}
});
});
fileStream.on('error', (err) => {
console.error(`Error writing cache file: ${err.message}`);
fs.unlink(tempFile, () => {});
completeDownload(task, false);
});
} else {
// 非成功响应
task.waitingClients.forEach(client => {
if (!client.res.destroyed) {
const headers = { ...proxyRes.headers };
if (!headers['access-control-allow-origin']) {
headers['access-control-allow-origin'] = client.res.getHeader('access-control-allow-origin') || '*';
}
client.res.writeHead(proxyRes.statusCode, headers);
proxyRes.pipe(client.res);
}
});
completeDownload(task, false);
}
});
proxyReq.on('error', (err) => {
console.error(`Download request error: ${err.message}`);
task.error = err.message;
// 向所有等待的客户端发送错误响应
task.waitingClients.forEach(client => {
if (!client.res.destroyed) {
client.res.statusCode = 502;
client.res.end('Bad Gateway');
}
});
task.waitingClients = [];
completeDownload(task, false);
});
proxyReq.end();
}
// 从API获取内容保留用于非大文件请求
function fetchFromApi(req, res, cacheKey, metaCacheFile, contentCacheFile, rangeHeader) {
// 构建目标URL
const targetUrl = new URL(req.url, DEFAULT_API_ENDPOINT);
// 选择合适的协议模块
const protocol = targetUrl.protocol === 'https:' ? https : http;
// 准备请求选项
const options = {
method: req.method,
headers: { ...req.headers }
};
// 删除可能导致问题的头信息
delete options.headers.host;
// 检查是否有缓存文件,如果有则添加条件请求头
if (fs.existsSync(contentCacheFile)) {
try {
const stat = fs.statSync(contentCacheFile);
const etag = `"${stat.mtime.getTime()}-${stat.size}"`;
const lastModified = stat.mtime.toUTCString();
// 添加条件请求头,让上游服务器判断是否需要重新下载
options.headers['if-none-match'] = etag;
options.headers['if-modified-since'] = lastModified;
} catch (err) {
console.log(`Error reading cache file stats: ${err.message}`);
}
}
// 检查是否有正在进行的下载任务
const existingTask = downloadTasks.get(cacheKey);
if (existingTask && rangeHeader) {
const range = parseRange(rangeHeader, existingTask.totalSize || Number.MAX_SAFE_INTEGER);
if (range && !Array.isArray(range)) {
const { start, end } = range;
// 检查请求的范围是否已经下载
if (fs.existsSync(contentCacheFile)) {
const stat = fs.statSync(contentCacheFile);
if (stat.size > end) {
// 请求的范围已经下载完成,从部分文件提供内容
console.log(`Serving range from partial download: ${req.url} (${start}-${end}, downloaded: ${stat.size})`);
// 创建临时的meta信息
const tempMeta = {
headers: {
'content-type': getContentType(req.url),
'accept-ranges': 'bytes',
'access-control-allow-origin': '*'
},
statusCode: 200
};
serveFromCache(res, tempMeta, contentCacheFile, rangeHeader, req);
return;
}
}
// 如果请求的范围还没下载完,将客户端添加到等待列表
console.log(`Adding client to waiting list for ongoing download: ${req.url}`);
existingTask.addClient(res, rangeHeader);
return;
}
}
// 如果是Range请求且缓存文件已存在检查是否可以从缓存提供部分内容
if (rangeHeader && fs.existsSync(contentCacheFile)) {
const stat = fs.statSync(contentCacheFile);
const range = parseRange(rangeHeader, stat.size);
if (range) {
// 尝试从缓存读取meta信息来验证文件完整性
try {
const metaData = JSON.parse(fs.readFileSync(metaCacheFile, 'utf8'));
if (metaData.headers && metaData.headers['content-length']) {
const expectedSize = parseInt(metaData.headers['content-length'], 10);
if (stat.size === expectedSize) {
// 缓存文件完整直接提供Range响应
console.log(`Serving range from complete cache: ${req.url}`);
serveFromCache(res, metaData, contentCacheFile, rangeHeader, req);
return;
}
}
} catch (err) {
// 忽略meta文件读取错误继续正常流程
}
}
}
// 创建请求
const proxyReq = protocol.request(targetUrl, options, (proxyRes) => {
// 处理304 Not Modified响应
if (proxyRes.statusCode === 304) {
console.log(`304 Not Modified, serving from cache: ${req.url}`);
// 从缓存提供内容
if (fs.existsSync(contentCacheFile) && fs.existsSync(metaCacheFile)) {
try {
const cachedMeta = JSON.parse(fs.readFileSync(metaCacheFile, 'utf8'));
serveFromCache(res, cachedMeta, contentCacheFile, rangeHeader, req);
return;
} catch (err) {
console.error(`Error serving 304 from cache: ${err.message}`);
}
}
// 如果缓存文件不存在返回304给客户端
res.writeHead(304, {
'Access-Control-Allow-Origin': res.getHeader('access-control-allow-origin') || '*',
'Cache-Control': 'public, max-age=3600'
});
res.end();
return;
}
// 记录meta信息
const metaInfo = {
timestamp: Date.now(),
headers: proxyRes.headers,
statusCode: proxyRes.statusCode
};
// 更新内存缓存
metaCache[cacheKey] = metaInfo;
// 写入meta缓存文件
fs.writeFile(metaCacheFile, JSON.stringify(metaInfo), (err) => {
if (err) console.error(`Error writing meta cache: ${err.message}`);
});
// 设置响应头保持CORS头
const responseHeaders = { ...proxyRes.headers };
// 确保CORS头不被覆盖
if (!responseHeaders['access-control-allow-origin']) {
responseHeaders['access-control-allow-origin'] = res.getHeader('access-control-allow-origin') || '*';
}
// 确保Content-Type正确设置特别是对于视频文件
if (!responseHeaders['content-type']) {
const ext = pathModule.extname(req.url).toLowerCase();
const mimeTypes = {
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.ogg': 'video/ogg',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.wmv': 'video/x-ms-wmv',
'.flv': 'video/x-flv',
'.mkv': 'video/x-matroska',
'.m4v': 'video/mp4',
'.3gp': 'video/3gpp',
'.ts': 'video/mp2t'
};
responseHeaders['content-type'] = mimeTypes[ext] || 'application/octet-stream';
}
// 确保Accept-Ranges头存在
responseHeaders['accept-ranges'] = 'bytes';
res.writeHead(proxyRes.statusCode, responseHeaders);
// 如果是成功的响应,缓存内容
if (proxyRes.statusCode >= 200 && proxyRes.statusCode < 300) {
// 对于Range请求的206响应不进行缓存直接转发
if (proxyRes.statusCode === 206 || rangeHeader) {
console.log(`Streaming range response: ${req.url}`);
proxyRes.pipe(res);
} else {
// 完整内容响应,进行缓存
const tempFile = `${contentCacheFile}.tmp`;
const fileStream = fs.createWriteStream(tempFile);
// 将响应写入文件并同时发送给客户端
proxyRes.pipe(fileStream);
proxyRes.pipe(res);
// 完成后重命名临时文件
fileStream.on('finish', () => {
fs.rename(tempFile, contentCacheFile, (err) => {
if (err) console.error(`Error saving cache file: ${err.message}`);
else console.log(`Cache saved for: ${req.url}`);
});
});
// 处理错误
fileStream.on('error', (err) => {
console.error(`Error writing cache file: ${err.message}`);
fs.unlink(tempFile, () => {}); // 删除临时文件
});
}
} else {
// 非成功响应直接转发,不缓存内容
proxyRes.pipe(res);
}
});
// 处理请求错误
proxyReq.on('error', (err) => {
console.error(`Proxy request error: ${err.message}`);
res.statusCode = 502;
res.end('Bad Gateway');
});
// 如果原始请求有body转发它
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
// 格式化字节数
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 格式化时间(秒)
function formatTime(seconds) {
if (!seconds || seconds === Infinity) return 'Unknown';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
// 启动服务器
server.listen(DEFAULT_PORT, () => {
console.log(`Proxy server running at http://localhost:${DEFAULT_PORT}/`);
console.log(`Proxying requests to: ${DEFAULT_API_ENDPOINT}`);
console.log(`Cache directory: ${cacheDir}`);
console.log(`Meta cache expiry: ${META_CACHE_EXPIRY_MS / 60000} minutes`);
});