feat(缓存处理): 添加缩略图生成功能并重构缓存服务逻辑
- 在package.json中添加webpack相关依赖并更新sharp版本 - 新增webpack配置用于代码混淆和打包优化 - 实现缩略图生成功能,当API返回thumb参数时自动创建缩略图 - 重构缓存服务逻辑,优化响应头处理和错误处理 - 移除不必要的path模块引入并统一代码风格
This commit is contained in:
parent
a211083da5
commit
650a7b8852
23
package.json
23
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
81
source.js
81
source.js
@ -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
30
webpack.config.js
Normal 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 配置来解决警告
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user