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

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

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);