Files
code-center/src/modules/fm-manager/proxy/http-proxy.ts

174 lines
5.7 KiB
TypeScript

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<string, string>, 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<string, string>);
};
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<string> {
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();
};