迁移
This commit is contained in:
+168
-28
@@ -2,37 +2,177 @@ const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const hostname = '127.0.0.1';
|
||||
const port = 3000;
|
||||
const hostname = process.env.IMAGE_HOST || '127.0.0.1';
|
||||
const port = Number.parseInt(process.env.IMAGE_PORT || '3000', 10);
|
||||
const directoryPath = path.resolve(__dirname, '../images');
|
||||
const maxCacheBytes = Number.parseInt(process.env.IMAGE_CACHE_MAX_MB || '128', 10) * 1024 * 1024;
|
||||
const maxCacheItemBytes =
|
||||
Number.parseInt(process.env.IMAGE_CACHE_ITEM_MAX_MB || '4', 10) * 1024 * 1024;
|
||||
const browserCacheSeconds = Number.parseInt(
|
||||
process.env.IMAGE_BROWSER_CACHE_SECONDS || '3600',
|
||||
10,
|
||||
);
|
||||
|
||||
const ImageServer = http.createServer((req, res) => {
|
||||
// 图片存储的文件夹路径
|
||||
const directoryPath = path.join(__dirname, '../images');
|
||||
// 请求的图片文件名
|
||||
const filePath = path.join(directoryPath, decodeURIComponent(req.url));
|
||||
// 确保文件存在且是文件,不是目录
|
||||
fs.access(filePath, fs.constants.F_OK | fs.constants.R_OK, (err) => {
|
||||
if (err) {
|
||||
res.writeHead(404, 'Not Found');
|
||||
res.end('Oops. Image not found!');
|
||||
} else {
|
||||
// 读取图片文件并发送响应
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(500, 'Server Error');
|
||||
res.end('Oops. There was a server error!');
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'image/jpeg' });
|
||||
res.end(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
const mimeTypes = {
|
||||
'.avif': 'image/avif',
|
||||
'.bmp': 'image/bmp',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.webp': 'image/webp',
|
||||
};
|
||||
|
||||
/** @type {Map<string, { data: Buffer, contentType: string, etag: string, modified: string }>} */
|
||||
const memoryCache = new Map();
|
||||
/** @type {Map<string, Promise<{ data: Buffer, contentType: string, etag: string, modified: string }>>} */
|
||||
const pendingReads = new Map();
|
||||
let cachedBytes = 0;
|
||||
|
||||
function removeCacheEntry(filePath) {
|
||||
const entry = memoryCache.get(filePath);
|
||||
if (!entry) return;
|
||||
cachedBytes -= entry.data.byteLength;
|
||||
memoryCache.delete(filePath);
|
||||
}
|
||||
|
||||
function cacheEntry(filePath, entry) {
|
||||
if (entry.data.byteLength > maxCacheItemBytes || maxCacheBytes <= 0) return;
|
||||
|
||||
removeCacheEntry(filePath);
|
||||
while (cachedBytes + entry.data.byteLength > maxCacheBytes && memoryCache.size) {
|
||||
const oldestPath = memoryCache.keys().next().value;
|
||||
if (!oldestPath) break;
|
||||
removeCacheEntry(oldestPath);
|
||||
}
|
||||
|
||||
memoryCache.set(filePath, entry);
|
||||
cachedBytes += entry.data.byteLength;
|
||||
}
|
||||
|
||||
function getCachedEntry(filePath) {
|
||||
const entry = memoryCache.get(filePath);
|
||||
if (!entry) return undefined;
|
||||
memoryCache.delete(filePath);
|
||||
memoryCache.set(filePath, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function readImage(filePath) {
|
||||
const cached = getCachedEntry(filePath);
|
||||
if (cached) return cached;
|
||||
|
||||
const pending = pendingReads.get(filePath);
|
||||
if (pending) return pending;
|
||||
|
||||
const readPromise = (async () => {
|
||||
const handle = await fs.promises.open(filePath, 'r');
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
if (!stat.isFile()) {
|
||||
const error = new Error('Not a file');
|
||||
error.code = 'ENOENT';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const data = await handle.readFile();
|
||||
const entry = {
|
||||
data,
|
||||
contentType: mimeTypes[path.extname(filePath).toLowerCase()] || 'application/octet-stream',
|
||||
etag: `W/"${stat.size.toString(16)}-${Math.trunc(stat.mtimeMs).toString(16)}"`,
|
||||
modified: stat.mtime.toUTCString(),
|
||||
};
|
||||
cacheEntry(filePath, entry);
|
||||
return entry;
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
})();
|
||||
|
||||
pendingReads.set(filePath, readPromise);
|
||||
try {
|
||||
return await readPromise;
|
||||
} finally {
|
||||
pendingReads.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveImagePath(requestUrl) {
|
||||
let pathname;
|
||||
try {
|
||||
pathname = decodeURIComponent(new URL(requestUrl, 'http://localhost').pathname);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relativePath = pathname.replace(/^[/\\]+/, '');
|
||||
const filePath = path.resolve(directoryPath, relativePath);
|
||||
const relativeToRoot = path.relative(directoryPath, filePath);
|
||||
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) return undefined;
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function sendText(res, statusCode, message) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(message),
|
||||
});
|
||||
res.end(message);
|
||||
}
|
||||
|
||||
const ImageServer = http.createServer(async (req, res) => {
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
res.setHeader('Allow', 'GET, HEAD');
|
||||
sendText(res, 405, 'Method not allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = resolveImagePath(req.url || '/');
|
||||
if (!filePath) {
|
||||
sendText(res, 400, 'Invalid image path');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await readImage(filePath);
|
||||
const headers = {
|
||||
'Content-Type': entry.contentType,
|
||||
'Content-Length': entry.data.byteLength,
|
||||
'Cache-Control': `public, max-age=${browserCacheSeconds}`,
|
||||
ETag: entry.etag,
|
||||
'Last-Modified': entry.modified,
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
};
|
||||
|
||||
if (req.headers['if-none-match'] === entry.etag) {
|
||||
res.writeHead(304, headers);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, headers);
|
||||
res.end(req.method === 'HEAD' ? undefined : entry.data);
|
||||
} catch (error) {
|
||||
if (error && (error.code === 'ENOENT' || error.code === 'EISDIR')) {
|
||||
sendText(res, 404, 'Image not found');
|
||||
return;
|
||||
}
|
||||
console.error(`Failed to serve ${filePath}:`, error);
|
||||
sendText(res, 500, 'Image server error');
|
||||
}
|
||||
});
|
||||
|
||||
ImageServer.keepAliveTimeout = 65_000;
|
||||
ImageServer.headersTimeout = 66_000;
|
||||
|
||||
ImageServer.listen(port, hostname, () => {
|
||||
console.log(`Server running at http://${hostname}:${port}/`);
|
||||
console.log('Press CTRL+C to stop the server');
|
||||
console.log(`Image server running at http://${hostname}:${port}/`);
|
||||
console.log(
|
||||
`Memory cache: ${Math.round(maxCacheBytes / 1024 / 1024)} MB, browser cache: ${browserCacheSeconds}s`,
|
||||
);
|
||||
console.log('Press CTRL+C to stop the server');
|
||||
});
|
||||
|
||||
exports.ImageServer = ImageServer;
|
||||
exports.ImageServer = ImageServer;
|
||||
|
||||
Reference in New Issue
Block a user