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