import { Readable } from 'node:stream'; import { minioResources } from '@/modules/s3.ts'; import { oss } from '@/app.ts'; import fs from 'node:fs'; import { IncomingMessage, ServerResponse } from 'node:http'; import http from 'node:http'; import https from 'node:https'; import { UserApp } from '@/modules/user-app/index.ts'; import { addStat } from '@/modules/html/stat/index.ts'; import path from 'path'; import { getTextContentType } from '@/modules/fm-manager/index.ts'; import { logger } from '@/modules/logger.ts'; import { pipeStream } from '../pipe.ts'; import { GetObjectCommandOutput } from '@aws-sdk/client-s3'; export async function downloadFileFromMinio(fileUrl: string, destFile: string) { const objectName = fileUrl.replace(minioResources + '/', ''); const objectStream = await oss.getObject(objectName) as GetObjectCommandOutput; const body = objectStream.Body as Readable; const chunks: Uint8Array[] = []; for await (const chunk of body) { chunks.push(chunk); } fs.writeFileSync(destFile, Buffer.concat(chunks)); } export const filterKeys = (metaData: Record, clearKeys: string[] = []) => { const keys = Object.keys(metaData); // remove X-Amz- meta data const removeKeys = ['password', 'accesskey', 'secretkey', ...clearKeys]; const filteredKeys = keys.filter((key) => !removeKeys.includes(key)); return filteredKeys.reduce((acc, key) => { acc[key] = metaData[key]; return acc; }, {} as Record); }; export async function minioProxy( req: IncomingMessage, res: ServerResponse, opts: { proxyUrl: string; createNotFoundPage: (msg?: string) => any; isDownload?: boolean; }, ) { const fileUrl = opts.proxyUrl; const { createNotFoundPage, isDownload = false } = opts; const objectName = fileUrl.replace(minioResources + '/', ''); console.log('proxy url objectName', objectName) try { const stat = await oss.statObject(objectName); if (stat?.size === 0) { createNotFoundPage('Invalid proxy url'); return true; } const filterMetaData = filterKeys(stat.metaData, ['size', 'etag', 'last-modified']); const contentLength = stat.size; const etag = stat.etag; const lastModified = stat.lastModified.toISOString(); const fileName = objectName.split('/').pop(); const ext = path.extname(fileName || ''); const objectStreamOutput = await oss.getObject(objectName); const objectStream = objectStreamOutput.Body as Readable; const headers = { 'Content-Length': contentLength, etag, 'last-modified': lastModified, 'file-name': fileName, ...filterMetaData, ...getTextContentType(ext), }; if (objectName.endsWith('.html') && !isDownload) { const { html, contentLength } = await getTextFromStreamAndAddStat(objectStream); res.writeHead(200, { ...headers, 'Content-Length': contentLength, }); res.end(html); } else { res.writeHead(200, { ...headers, }); pipeStream(objectStream as any, res); } return true; } catch (error) { console.error(`Proxy request error: ${error.message}`); createNotFoundPage('Invalid proxy url'); return false; } } // 添加一个辅助函数来从流中获取文本 async function getTextFromStream(stream: Readable | IncomingMessage): Promise { return new Promise((resolve, reject) => { let data = ''; stream.on('data', (chunk) => { data += chunk; }); stream.on('end', () => { resolve(data); }); stream.on('error', (err) => { reject(err); }); }); } export async function getTextFromStreamAndAddStat(stream: Readable | IncomingMessage): Promise<{ html: string; contentLength: number }> { const html = await getTextFromStream(stream); const newHtml = addStat(html); const newContentLength = Buffer.byteLength(newHtml); return { html: newHtml, contentLength: newContentLength }; } export const httpProxy = async ( req: IncomingMessage, res: ServerResponse, opts: { proxyUrl: string; userApp: UserApp; createNotFoundPage: (msg?: string) => any; }, ) => { const { proxyUrl, userApp, createNotFoundPage } = opts; const _u = new URL(req.url, 'http://localhost'); const params = _u.searchParams; const isDownload = params.get('download') === 'true'; if (proxyUrl.startsWith(minioResources)) { // console.log('isMinio', proxyUrl) const isOk = await minioProxy(req, res, { ...opts, isDownload }); if (!isOk) { userApp.clearCacheData(); } return; } let protocol = proxyUrl.startsWith('https') ? https : http; // 代理 const proxyReq = protocol.request(proxyUrl, async (proxyRes) => { const headers = proxyRes.headers; if (proxyRes.statusCode === 404) { userApp.clearCacheData(); return createNotFoundPage('Invalid proxy url'); } if (proxyRes.statusCode === 302) { res.writeHead(302, { Location: proxyRes.headers.location }); return res.end(); } if (proxyUrl.endsWith('.html') && !isDownload) { try { const { html, contentLength } = await getTextFromStreamAndAddStat(proxyRes); res.writeHead(200, { ...headers, 'Content-Length': contentLength, }); res.end(html); } catch (error) { console.error(`Proxy request error: ${error.message}`); return createNotFoundPage('Invalid proxy url:' + error.message); } } else { console.log('Proxying file: headers', headers); res.writeHead(proxyRes.statusCode, { ...headers, }); pipeStream(proxyRes as any, res); } }); proxyReq.on('error', (err) => { console.error(`Proxy request error: ${err.message}`); userApp.clearCacheData(); }); proxyReq.end(); };