1213 lines
46 KiB
JavaScript
1213 lines
46 KiB
JavaScript
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`);
|
||
});
|