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": {
|
"dependencies": {
|
||||||
"sharp": "^0.33.4"
|
"javascript-obfuscator": "^4.1.1",
|
||||||
},
|
"sharp": "^0.34.2"
|
||||||
"devDependencies": {
|
},
|
||||||
"javascript-obfuscator": "^4.1.1"
|
"devDependencies": {
|
||||||
},
|
"webpack": "^5.99.9",
|
||||||
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf",
|
"webpack-cli": "^4.10.0",
|
||||||
"scripts": {
|
"webpack-obfuscator": "^3.5.1"
|
||||||
"start": "node source.js"
|
},
|
||||||
}
|
"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 fs = require('fs');
|
||||||
const pathModule = require('path');
|
const pathModule = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const path = require('path');
|
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
|
||||||
const CACHE_DIR_NAME = '.cache';
|
const CACHE_DIR_NAME = '.cache';
|
||||||
@ -24,7 +23,7 @@ const viewsInfo = {
|
|||||||
cacheReadError: 0,
|
cacheReadError: 0,
|
||||||
fetchApiError: 0,
|
fetchApiError: 0,
|
||||||
fetchApiWarning: 0,
|
fetchApiWarning: 0,
|
||||||
increment: function(key) {
|
increment: function (key) {
|
||||||
if (this.hasOwnProperty(key)) {
|
if (this.hasOwnProperty(key)) {
|
||||||
this[key]++;
|
this[key]++;
|
||||||
}
|
}
|
||||||
@ -141,8 +140,8 @@ async function handleApiRedirect(res, apiData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res) {
|
async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res) {
|
||||||
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid } = apiData.data;
|
const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data;
|
||||||
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid };
|
const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb };
|
||||||
|
|
||||||
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
|
pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() };
|
||||||
const cacheMetaFile = pathModule.join(cacheDir, `${data.uniqid}.meta`);
|
const cacheMetaFile = pathModule.join(cacheDir, `${data.uniqid}.meta`);
|
||||||
@ -160,8 +159,6 @@ async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign
|
|||||||
if (fs.existsSync(cacheContentFile)) {
|
if (fs.existsSync(cacheContentFile)) {
|
||||||
const stats = fs.statSync(cacheContentFile);
|
const stats = fs.statSync(cacheContentFile);
|
||||||
const contentLength = stats.size;
|
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) {
|
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.`);
|
console.warn(`Content length mismatch for ${cacheContentFile}. API: ${data.headers['content-length']}, Cache: ${contentLength}. Re-fetching.`);
|
||||||
fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res);
|
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}` };
|
let errorPayload = { code: apiRes.statusCode, message: `API Error: ${apiRes.statusCode}` };
|
||||||
try {
|
try {
|
||||||
const parsedError = JSON.parse(responseData);
|
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 */ }
|
} catch (e) { /* Ignore if response is not JSON */ }
|
||||||
resolve(errorPayload); // Resolve with error structure for consistency
|
resolve(errorPayload); // Resolve with error structure for consistency
|
||||||
return;
|
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 获取数据并写入缓存
|
// 从真实 URL 获取数据并写入缓存
|
||||||
const REAL_URL_FETCH_TIMEOUT_MS = 0; // 0 means no timeout for the actual file download
|
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);
|
fs.renameSync(tempCacheContentFile, cacheContentFile);
|
||||||
console.log(`Successfully cached: ${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) {
|
} catch (renameError) {
|
||||||
console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameError);
|
console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameError);
|
||||||
// If rename fails, try to remove the temp file to avoid clutter
|
// If rename fails, try to remove the temp file to avoid clutter
|
||||||
@ -477,6 +502,36 @@ function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res) {
|
|||||||
return;
|
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');
|
viewsInfo.increment('cacheCall');
|
||||||
const readStream = fs.createReadStream(cacheContentFile);
|
const readStream = fs.createReadStream(cacheContentFile);
|
||||||
const isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4');
|
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', () => {
|
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 = {
|
const responseHeaders = {
|
||||||
...baseHeaders,
|
...baseHeaders,
|
||||||
@ -523,7 +567,6 @@ function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res) {
|
|||||||
// Merge other headers from cacheData.headers, letting them override base if necessary
|
// Merge other headers from cacheData.headers, letting them override base if necessary
|
||||||
// but ensure our critical headers like Content-Length (if updated) are preserved.
|
// but ensure our critical headers like Content-Length (if updated) are preserved.
|
||||||
...(cacheData.headers || {}),
|
...(cacheData.headers || {}),
|
||||||
'Content-Length': currentContentLength.toString(), // Ensure this is set correctly
|
|
||||||
};
|
};
|
||||||
|
|
||||||
res.writeHead(HTTP_STATUS.OK, responseHeaders);
|
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