alist-proxy/index.js
2024-09-27 18:36:23 +08:00

207 lines
6.9 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 path = require('path');
const requestTimeout = 10000; // 10 seconds
const cacheDir = path.join(__dirname, '.cache');
const args = process.argv.slice(2);
let port = 9001;
let apiEndpoint = 'https://oss.x-php.com/alist/link';
// 解析命令行参数
args.forEach(arg => {
const [key, value] = arg.split('=');
if (key === 'port') {
port = parseInt(value, 10);
} else if (key === 'api') {
apiEndpoint = value;
}
});
// 确保缓存目录存在
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir);
}
const server = http.createServer(async (req, res) => {
if (req.url === '/favicon.ico') {
res.writeHead(204);
res.end();
return;
}
const parsedUrl = url.parse(req.url, true);
const reqPath = parsedUrl.pathname;
const sign = parsedUrl.query.sign || '';
// 只要reqPath的文件名不要路径
const reqName = parsedUrl.pathname.split('/').pop();
const cacheMetaFile = path.join(cacheDir, `${reqName.replace(/\//g, '_')}.meta`);
const cacheContentFile = path.join(cacheDir, `${reqName.replace(/\//g, '_')}.content`);
const tempCacheContentFile = path.join(cacheDir, `${reqName.replace(/\//g, '_')}.temp`);
if (!sign || reqPath === '/') {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad Request: Missing sign or path');
return;
}
if (isCacheValid(cacheMetaFile, cacheContentFile)) {
serveFromCache(cacheMetaFile, cacheContentFile, res);
} else {
try {
const apiData = await fetchApiData(reqPath, sign);
if (apiData.code === 200 && apiData.data && apiData.data.url) {
const { url: realUrl, cloudtype, expiration, path } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path };
if (expiration > 0) {
fs.writeFileSync(cacheMetaFile, JSON.stringify(data));
}
// 如果 cacheContentFile 存在 直接调用它
if (fs.existsSync(cacheContentFile)) {
serveFromCache(cacheMetaFile, cacheContentFile, res);
return;
}
fetchAndServe(data, tempCacheContentFile, cacheContentFile, res);
} else {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end(apiData.message || 'Bad Gateway');
}
} catch (error) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end('Bad Gateway: Failed to decode JSON' + error);
}
}
});
const isCacheValid = (cacheMetaFile, cacheContentFile) => {
if (!fs.existsSync(cacheMetaFile) || !fs.existsSync(cacheContentFile)) return false;
const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8'));
return cacheData.expiration > Date.now();
};
const fetchApiData = (reqPath, sign) => {
return new Promise((resolve, reject) => {
const postData = querystring.stringify({ path: reqPath, sign });
const apiReq = https.request(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'sign': sign
},
timeout: requestTimeout
}, (apiRes) => {
let data = '';
apiRes.on('data', chunk => data += chunk);
apiRes.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
apiReq.on('error', reject);
apiReq.write(postData);
apiReq.end();
});
};
const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, res) => {
https.get(data.realUrl, { timeout: requestTimeout * 10 }, (realRes) => {
// 创建临时缓存文件流
const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: 'w' });
// 通过 data.path 判断是否是视频.mp4
const isVideo = data.path.includes('.mp4');
// realRes.headers['content-type'] 有的话 去掉
if (realRes.headers['content-type']) {
delete realRes.headers['content-type'];
}
res.writeHead(realRes.statusCode, {
...realRes.headers,
'Content-Type': isVideo ? 'video/mp4' : 'application/octet-stream',
'Cloud-Type': data.cloudtype,
'Cloud-Expiration': data.expiration,
});
realRes.pipe(cacheStream);
realRes.pipe(res);
realRes.on('end', () => {
// 下载完成后,将临时文件重命名为最终缓存文件
fs.renameSync(tempCacheContentFile, cacheContentFile);
cacheStream.end();
});
realRes.on('error', (e) => {
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end(`Bad Gateway: ${data.realUrl}`);
}
fs.unlinkSync(tempCacheContentFile); // 删除临时文件
});
}).on('error', (e) => {
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end(`Bad Gateway: ${data.realUrl}`);
}
fs.unlinkSync(tempCacheContentFile); // 删除临时文件
});
};
const serveFromCache = (cacheMetaFile, cacheContentFile, res) => {
const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8'));
const readStream = fs.createReadStream(cacheContentFile);
// 判断是否是视频
const isVideo = cacheData.path.includes('.mp4');
readStream.on('open', () => {
res.writeHead(200, {
'Content-Type': isVideo ? 'video/mp4' : 'application/octet-stream',
'Cloud-Type': cacheData.cloudtype,
'Cloud-Expiration': cacheData.expiration,
});
readStream.pipe(res);
});
readStream.on('error', (err) => {
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error: Unable to read cache content file');
}
});
};
server.listen(port, () => {
console.log(`Proxy server is running on http://localhost:${port}`);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('Received SIGINT. Shutting down gracefully...');
server.close(() => {
console.log('Server closed.');
process.exit(0);
});
// Force shutdown after 10 seconds if not closed
setTimeout(() => {
console.error('Forcing shutdown...');
process.exit(1);
}, 10000);
});