feat(缓存处理): 添加缩略图生成功能并重构缓存服务逻辑

- 在package.json中添加webpack相关依赖并更新sharp版本
- 新增webpack配置用于代码混淆和打包优化
- 实现缩略图生成功能,当API返回thumb参数时自动创建缩略图
- 重构缓存服务逻辑,优化响应头处理和错误处理
- 移除不必要的path模块引入并统一代码风格
This commit is contained in:
XiaoMo 2025-05-27 17:03:38 +08:00
parent a211083da5
commit 650a7b8852
4 changed files with 106 additions and 30 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,15 @@
{
"dependencies": {
"sharp": "^0.33.4"
},
"devDependencies": {
"javascript-obfuscator": "^4.1.1"
},
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf",
"scripts": {
"start": "node source.js"
}
"dependencies": {
"javascript-obfuscator": "^4.1.1",
"sharp": "^0.34.2"
},
"devDependencies": {
"webpack": "^5.99.9",
"webpack-cli": "^4.10.0",
"webpack-obfuscator": "^3.5.1"
},
"scripts": {
"build": "webpack",
"start": "node index.js"
}
}

View File

@ -5,7 +5,6 @@ const querystring = require('querystring');
const fs = require('fs');
const pathModule = require('path');
const crypto = require('crypto');
const path = require('path');
const sharp = require('sharp');
const CACHE_DIR_NAME = '.cache';
@ -24,7 +23,7 @@ const viewsInfo = {
cacheReadError: 0,
fetchApiError: 0,
fetchApiWarning: 0,
increment: function(key) {
increment: function (key) {
if (this.hasOwnProperty(key)) {
this[key]++;
}
@ -141,8 +140,8 @@ async function handleApiRedirect(res, apiData) {
}
async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res) {
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid } = apiData.data;
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid };
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, `${data.uniqid}.meta`);
@ -160,8 +159,6 @@ async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign
if (fs.existsSync(cacheContentFile)) {
const stats = fs.statSync(cacheContentFile);
const contentLength = stats.size;
// If file is very small and content length from API differs, consider re-fetching.
// The 2048 threshold seems arbitrary; could be configurable or based on content type.
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);
@ -351,7 +348,7 @@ async function fetchApiData(reqPath, token, sign) {
let errorPayload = { code: apiRes.statusCode, message: `API Error: ${apiRes.statusCode}` };
try {
const parsedError = JSON.parse(responseData);
if(parsedError && parsedError.message) errorPayload.message = parsedError.message;
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;
@ -379,6 +376,25 @@ async function fetchApiData(reqPath, token, sign) {
});
}
// createThumbnail
function createThumbnail(data, cacheContentFile, thumbCacheFile) {
const { path, thumb } = data;
const isVideo = path && typeof path === 'string' && path.includes('.mp4');
if (isVideo || !thumb) return;
if (fs.existsSync(thumbCacheFile)) return;
const width = thumb.width;
const height = thumb.height;
sharp(cacheContentFile)
.resize(width, height)
.toFile(thumbCacheFile, (err, info) => {
if (err) {
console.error(`Error creating thumbnail for ${cacheContentFile}:`, err);
return;
}
});
}
// 从真实 URL 获取数据并写入缓存
const REAL_URL_FETCH_TIMEOUT_MS = 0; // 0 means no timeout for the actual file download
@ -444,6 +460,15 @@ const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFi
}
fs.renameSync(tempCacheContentFile, cacheContentFile);
console.log(`Successfully cached: ${cacheContentFile}`);
if (data.thumb) {
const thumbCacheFile = pathModule.join(cacheDir, `${data.uniqid}_thumb.jpg`);
if (!fs.existsSync(thumbCacheFile)) {
// 创建缩略图
createThumbnail(data, cacheContentFile, thumbCacheFile);
}
}
} catch (renameError) {
console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameError);
// If rename fails, try to remove the temp file to avoid clutter
@ -477,6 +502,36 @@ function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res) {
return;
}
const baseHeaders = {
'Cloud-Type': cacheData.cloudtype || 'unknown',
'Cloud-Expiration': cacheData.expiration || 'N/A',
'ETag': cacheData.uniqid || crypto.createHash('md5').update(fs.readFileSync(cacheContentFile)).digest('hex'), // Fallback ETag if missing
'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': (cacheData.headers && cacheData.headers['last-modified']) || new Date(fs.statSync(cacheMetaFile).mtime).toUTCString(),
};
if (cacheData.thumb) {
const thumbCacheFile = pathModule.join(cacheDir, `${cacheData.uniqid}_thumb.jpg`);
if (fs.existsSync(thumbCacheFile)) {
cacheData.headers['content-length'] = fs.statSync(thumbCacheFile).size;
const responseHeaders = {
...baseHeaders,
...(cacheData.headers || {}),
'ETag': (cacheData.uniqid || '') + '_thumb',
'Content-Type': 'image/jpeg',
};
res.writeHead(HTTP_STATUS.OK, responseHeaders);
const thumbStream = fs.createReadStream(thumbCacheFile);
thumbStream.pipe(res);
return;
} else {
createThumbnail(cacheData, cacheContentFile, thumbCacheFile)
}
}
viewsInfo.increment('cacheCall');
const readStream = fs.createReadStream(cacheContentFile);
const isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4');
@ -505,17 +560,6 @@ function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res) {
}
readStream.on('open', () => {
const baseHeaders = {
'Cloud-Type': cacheData.cloudtype || 'unknown',
'Cloud-Expiration': cacheData.expiration || 'N/A',
'ETag': cacheData.uniqid || crypto.createHash('md5').update(fs.readFileSync(cacheContentFile)).digest('hex'), // Fallback ETag if missing
'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': (cacheData.headers && cacheData.headers['last-modified']) || new Date(fs.statSync(cacheMetaFile).mtime).toUTCString(),
};
const responseHeaders = {
...baseHeaders,
@ -523,7 +567,6 @@ function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res) {
// 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 || {}),
'Content-Length': currentContentLength.toString(), // Ensure this is set correctly
};
res.writeHead(HTTP_STATUS.OK, responseHeaders);

30
webpack.config.js Normal file
View File

@ -0,0 +1,30 @@
const path = require('path');
const WebpackObfuscator = require('webpack-obfuscator');
module.exports = {
entry: './source.js',
target: 'node',
output: {
filename: 'index.js',
path: path.resolve(__dirname)
},
plugins: [
new WebpackObfuscator({
compact: true,
controlFlowFlattening: true,
deadCodeInjection: true,
numbersToExpressions: true,
simplify: true,
splitStrings: true,
stringArray: true
})
],
optimization: {
minimize: true
},
// 添加以下配置来处理 sharp 模块
externals: {
sharp: 'commonjs sharp'
},
mode: 'production' // 添加 mode 配置来解决警告
};