update
This commit is contained in:
90
src/modules/fm-manager/get-content-type.ts
Normal file
90
src/modules/fm-manager/get-content-type.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import path from 'node:path';
|
||||
export const getTextContentType = (ext: string) => {
|
||||
const textContentTypes = [
|
||||
'.tsx',
|
||||
'.jsx', //
|
||||
'.conf',
|
||||
'.env',
|
||||
'.example',
|
||||
'.log',
|
||||
'.mjs',
|
||||
'.map',
|
||||
'.json5',
|
||||
'.pem',
|
||||
'.crt',
|
||||
];
|
||||
const include = textContentTypes.includes(ext);
|
||||
if (!include) {
|
||||
return {};
|
||||
}
|
||||
const contentType = getContentTypeCore(ext);
|
||||
if (!contentType) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'Content-Type': contentType,
|
||||
};
|
||||
};
|
||||
// 获取文件的 content-type
|
||||
export const getContentTypeCore = (extname: string) => {
|
||||
const contentType = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.tsx': 'text/typescript; charset=utf-8',
|
||||
'.ts': 'text/typescript; charset=utf-8',
|
||||
'.jsx': 'text/javascript; charset=utf-8',
|
||||
'.conf': 'text/plain; charset=utf-8',
|
||||
'.env': 'text/plain; charset=utf-8',
|
||||
'.example': 'text/plain; charset=utf-8',
|
||||
'.log': 'text/plain; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.map': 'application/json; charset=utf-8',
|
||||
|
||||
'.json5': 'application/json5; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.wav': 'audio/wav',
|
||||
'.mp4': 'video/mp4',
|
||||
'.md': 'text/markdown; charset=utf-8', // utf-8配置
|
||||
'.ico': 'image/x-icon', // Favicon 图标
|
||||
'.webp': 'image/webp', // WebP 图像格式
|
||||
'.webm': 'video/webm', // WebM 视频格式
|
||||
'.ogg': 'audio/ogg', // Ogg 音频格式
|
||||
'.mp3': 'audio/mpeg', // MP3 音频格式
|
||||
'.m4a': 'audio/mp4', // M4A 音频格式
|
||||
'.m3u8': 'application/vnd.apple.mpegurl', // HLS 播放列表
|
||||
'.pdf': 'application/pdf', // PDF 文档
|
||||
'.doc': 'application/msword', // Word 文档
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Word 文档 (新版)
|
||||
'.ppt': 'application/vnd.ms-powerpoint', // PowerPoint 演示文稿
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // PowerPoint (新版)
|
||||
'.xls': 'application/vnd.ms-excel', // Excel 表格
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // Excel 表格 (新版)
|
||||
'.csv': 'text/csv; charset=utf-8', // CSV 文件
|
||||
'.xml': 'application/xml; charset=utf-8', // XML 文件
|
||||
'.rtf': 'application/rtf', // RTF 文本文件
|
||||
'.eot': 'application/vnd.ms-fontobject', // Embedded OpenType 字体
|
||||
'.ttf': 'font/ttf', // TrueType 字体
|
||||
'.woff': 'font/woff', // Web Open Font Format 1.0
|
||||
'.woff2': 'font/woff2', // Web Open Font Format 2.0
|
||||
'.otf': 'font/otf', // OpenType 字体
|
||||
'.wasm': 'application/wasm', // WebAssembly 文件
|
||||
'.pem': 'application/x-pem-file', // PEM 证书文件
|
||||
'.crt': 'application/x-x509-ca-cert', // CRT 证书文件
|
||||
'.yaml': 'application/x-yaml; charset=utf-8', // YAML 文件
|
||||
'.yml': 'application/x-yaml; charset=utf-8', // YAML 文件(别名)
|
||||
'.zip': 'application/octet-stream',
|
||||
};
|
||||
return contentType[extname];
|
||||
};
|
||||
|
||||
export const getContentType = (filePath: string) => {
|
||||
const extname = path.extname(filePath).toLowerCase();
|
||||
const contentType = getContentTypeCore(extname);
|
||||
return contentType || 'application/octet-stream';
|
||||
};
|
||||
67
src/modules/fm-manager/get-router.ts
Normal file
67
src/modules/fm-manager/get-router.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { config } from '../config.ts';
|
||||
import { app } from '@/app.ts'
|
||||
const api = config?.api || { host: 'https://kevisual.cn', path: '/api/router' };
|
||||
const apiPath = api.path || '/api/router';
|
||||
export const fetchTest = async (id: string) => {
|
||||
const res = await app.call({
|
||||
path: 'user-app',
|
||||
key: 'test',
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
code: res.code,
|
||||
data: res.body,
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchDomain = async (domain: string): Promise<{ code: number; data: any, message?: string }> => {
|
||||
const res = await app.call({
|
||||
path: 'app',
|
||||
key: 'getDomainApp',
|
||||
payload: {
|
||||
data: {
|
||||
domain,
|
||||
}
|
||||
},
|
||||
});
|
||||
return {
|
||||
code: res.code,
|
||||
data: res.body as any,
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchApp = async (payload: { user: string; app: string }) => {
|
||||
const res = await app.call({
|
||||
path: 'app',
|
||||
key: 'getApp',
|
||||
payload: {
|
||||
data: {
|
||||
user: payload.user,
|
||||
key: payload.app,
|
||||
}
|
||||
},
|
||||
});
|
||||
return {
|
||||
code: res.code,
|
||||
data: res.body,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUserConfig = async (token: string) => {
|
||||
// await queryConfig.getConfigByKey('user.json', { token })
|
||||
const res = await app.call({
|
||||
path: 'config',
|
||||
key: 'defaultConfig',
|
||||
payload: {
|
||||
configKey: 'user.json',
|
||||
token,
|
||||
}
|
||||
});
|
||||
const data = res.body;
|
||||
return {
|
||||
code: res.code,
|
||||
data
|
||||
}
|
||||
}
|
||||
10
src/modules/fm-manager/index.ts
Normal file
10
src/modules/fm-manager/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './proxy/http-proxy.ts'
|
||||
export * from './proxy/file-proxy.ts'
|
||||
export * from './proxy/minio-proxy.ts'
|
||||
export * from './proxy/ai-proxy.ts'
|
||||
|
||||
export * from './get-router.ts'
|
||||
|
||||
export * from './get-content-type.ts'
|
||||
|
||||
export * from './utils.ts'
|
||||
292
src/modules/fm-manager/proxy/ai-proxy.ts
Normal file
292
src/modules/fm-manager/proxy/ai-proxy.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { bucketName, minioClient } from '@/modules/minio.ts';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
import { filterKeys } from './http-proxy.ts';
|
||||
import { getUserFromRequest } from '../utils.ts';
|
||||
import { UserPermission, Permission } from '@kevisual/permission';
|
||||
import { getLoginUser } from '@/modules/auth.ts';
|
||||
import busboy from 'busboy';
|
||||
import { getContentType } from '../get-content-type.ts';
|
||||
import { OssBase } from '@kevisual/oss';
|
||||
import { parseSearchValue } from '@kevisual/router/browser';
|
||||
import { logger } from '@/modules/logger.ts';
|
||||
|
||||
type FileList = {
|
||||
name: string;
|
||||
prefix?: string;
|
||||
size?: number;
|
||||
etag?: string;
|
||||
lastModified?: Date;
|
||||
|
||||
path?: string;
|
||||
url?: string;
|
||||
pathname?: string;
|
||||
};
|
||||
export const getFileList = async (list: any, opts?: { objectName: string; app: string; host?: string }) => {
|
||||
const { app, host } = opts || {};
|
||||
const objectName = opts?.objectName || '';
|
||||
let newObjectName = objectName;
|
||||
const [user] = objectName.split('/');
|
||||
let replaceUser = user + '/';
|
||||
if (app === 'resources') {
|
||||
replaceUser = `${user}/resources/`;
|
||||
newObjectName = objectName.replace(`${user}/`, replaceUser);
|
||||
}
|
||||
return list.map((item: FileList) => {
|
||||
if (item.name) {
|
||||
item.path = item.name?.replace?.(objectName, '');
|
||||
item.pathname = '/' + item.name.replace(`${user}/`, replaceUser);
|
||||
} else {
|
||||
item.path = item.prefix?.replace?.(objectName, '');
|
||||
item.pathname = '/' + item.prefix.replace(`${user}/`, replaceUser);
|
||||
}
|
||||
if (item.name && app === 'ai') {
|
||||
const [_user, _app, _version, ...rest] = item.name.split('/');
|
||||
item.pathname = item.pathname.replace(`/${_user}/${_app}/${_version}/`, `/${_user}/${_app}/`);
|
||||
} else if (app === 'ai') {
|
||||
const [_user, _app, _version, ...rest] = item.prefix?.split('/');
|
||||
item.pathname = item.pathname.replace(`/${_user}/${_app}/${_version}/`, `/${_user}/${_app}/`);
|
||||
}
|
||||
item.url = new URL(item.pathname, `https://${host}`).toString();
|
||||
return item;
|
||||
});
|
||||
};
|
||||
// import { logger } from '@/module/logger.ts';
|
||||
const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
||||
const { createNotFoundPage } = opts;
|
||||
const _u = new URL(req.url, 'http://localhost');
|
||||
const oss = opts.oss;
|
||||
const params = _u.searchParams;
|
||||
const password = params.get('p');
|
||||
const hash = params.get('hash');
|
||||
let dir = !!params.get('dir');
|
||||
const recursive = !!params.get('recursive');
|
||||
const { objectName, app, owner, loginUser, isOwner } = await getObjectName(req);
|
||||
if (!dir && _u.pathname.endsWith('/')) {
|
||||
dir = true; // 如果是目录请求,强制设置为true
|
||||
}
|
||||
logger.debug(`proxy request: ${objectName}`, dir);
|
||||
try {
|
||||
if (dir) {
|
||||
if (!isOwner) {
|
||||
return createNotFoundPage('no dir permission');
|
||||
}
|
||||
const list = await oss.listObjects<true>(objectName, { recursive: recursive });
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
const host = req.headers['host'] || 'localhost';
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
data: await getFileList(list, {
|
||||
objectName: objectName,
|
||||
app: app,
|
||||
host,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const stat = await oss.statObject(objectName);
|
||||
if (!stat) {
|
||||
createNotFoundPage('Invalid proxy url');
|
||||
logger.debug('no stat', objectName, owner, req.url);
|
||||
return true;
|
||||
}
|
||||
const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner });
|
||||
const checkPermission = permissionInstance.checkPermissionSuccess({
|
||||
username: loginUser?.tokenUser?.username || '',
|
||||
password: password,
|
||||
});
|
||||
if (!checkPermission.success) {
|
||||
logger.info('no permission', checkPermission, loginUser, owner);
|
||||
return createNotFoundPage('no permission');
|
||||
}
|
||||
if (hash && stat.etag === hash) {
|
||||
res.writeHead(304); // not modified
|
||||
res.end('not modified');
|
||||
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 objectStream = await minioClient.getObject(bucketName, objectName);
|
||||
const headers = {
|
||||
'Content-Length': contentLength,
|
||||
etag,
|
||||
'last-modified': lastModified,
|
||||
...filterMetaData,
|
||||
};
|
||||
|
||||
res.writeHead(200, {
|
||||
...headers,
|
||||
});
|
||||
objectStream.pipe(res, { end: true });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Proxy request error: ${error.message}`);
|
||||
createNotFoundPage('Invalid ai proxy url');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const getMetadata = (pathname: string) => {
|
||||
let meta: any = { 'app-source': 'user-app' };
|
||||
const isHtml = pathname.endsWith('.html');
|
||||
if (isHtml) {
|
||||
meta = {
|
||||
...meta,
|
||||
'content-type': 'text/html; charset=utf-8',
|
||||
'cache-control': 'no-cache',
|
||||
};
|
||||
} else {
|
||||
meta = {
|
||||
...meta,
|
||||
'content-type': getContentType(pathname),
|
||||
'cache-control': 'max-age=31536000, immutable',
|
||||
};
|
||||
}
|
||||
return meta;
|
||||
};
|
||||
|
||||
export const postProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
||||
const _u = new URL(req.url, 'http://localhost');
|
||||
|
||||
const pathname = _u.pathname;
|
||||
const oss = opts.oss;
|
||||
const params = _u.searchParams;
|
||||
const force = !!params.get('force');
|
||||
const hash = params.get('hash');
|
||||
let meta = parseSearchValue(params.get('meta'), { decode: true });
|
||||
if (!hash && !force) {
|
||||
return opts?.createNotFoundPage?.('no hash');
|
||||
}
|
||||
const { objectName, isOwner } = await getObjectName(req);
|
||||
if (!isOwner) {
|
||||
return opts?.createNotFoundPage?.('no permission');
|
||||
}
|
||||
const end = (data: any, message?: string, code = 200) => {
|
||||
res.writeHead(code, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ code: code, data: data, message: message || 'success' }));
|
||||
};
|
||||
let statMeta: any = {};
|
||||
if (!force) {
|
||||
const check = await oss.checkObjectHash(objectName, hash, meta);
|
||||
statMeta = check?.metaData || {};
|
||||
let isNewMeta = false;
|
||||
if (check.success && JSON.stringify(meta) !== '{}' && !check.equalMeta) {
|
||||
meta = { ...statMeta, ...getMetadata(pathname), ...meta };
|
||||
isNewMeta = true;
|
||||
await oss.replaceObject(objectName, { ...meta });
|
||||
}
|
||||
if (check.success) {
|
||||
return end({ success: true, hash, meta, isNewMeta, equalMeta: check.equalMeta }, '文件已存在');
|
||||
}
|
||||
}
|
||||
const bb = busboy({
|
||||
headers: req.headers,
|
||||
limits: {
|
||||
fileSize: 100 * 1024 * 1024, // 100MB
|
||||
files: 1,
|
||||
},
|
||||
});
|
||||
let fileProcessed = false;
|
||||
bb.on('file', async (name, file, info) => {
|
||||
fileProcessed = true;
|
||||
try {
|
||||
// console.log('file', stat?.metaData);
|
||||
// await sleep(2000);
|
||||
await oss.putObject(
|
||||
objectName,
|
||||
file,
|
||||
{
|
||||
...statMeta,
|
||||
...getMetadata(pathname),
|
||||
...meta,
|
||||
},
|
||||
{ check: false, isStream: true },
|
||||
);
|
||||
end({ success: true, name, info, isNew: true, hash, meta: meta?.metaData, statMeta }, '上传成功', 200);
|
||||
} catch (error) {
|
||||
end({ error: error }, '上传失败', 500);
|
||||
}
|
||||
});
|
||||
|
||||
bb.on('finish', () => {
|
||||
// 只有当没有文件被处理时才执行end
|
||||
if (!fileProcessed) {
|
||||
end({ success: false }, '没有接收到文件', 400);
|
||||
}
|
||||
});
|
||||
bb.on('error', (err) => {
|
||||
console.error('Busboy 错误:', err);
|
||||
end({ error: err }, '文件解析失败', 500);
|
||||
});
|
||||
|
||||
req.pipe(bb);
|
||||
};
|
||||
export const getObjectName = async (req: IncomingMessage, opts?: { checkOwner?: boolean }) => {
|
||||
const _u = new URL(req.url, 'http://localhost');
|
||||
const pathname = decodeURIComponent(_u.pathname);
|
||||
const params = _u.searchParams;
|
||||
const { user, app } = getUserFromRequest(req);
|
||||
const checkOwner = opts?.checkOwner ?? true;
|
||||
let objectName = '';
|
||||
let owner = '';
|
||||
if (app === 'ai') {
|
||||
const version = params.get('version') || '1.0.0'; // root/ai
|
||||
objectName = pathname.replace(`/${user}/${app}/`, `${user}/${app}/${version}/`);
|
||||
} else {
|
||||
objectName = pathname.replace(`/${user}/${app}/`, `${user}/`); // root/resources
|
||||
}
|
||||
owner = user;
|
||||
let isOwner = undefined;
|
||||
let loginUser: Awaited<ReturnType<typeof getLoginUser>> = null;
|
||||
if (checkOwner) {
|
||||
loginUser = await getLoginUser(req);
|
||||
logger.debug('getObjectName', loginUser, user, app);
|
||||
isOwner = loginUser?.tokenUser?.username === owner;
|
||||
}
|
||||
return {
|
||||
objectName,
|
||||
loginUser,
|
||||
owner,
|
||||
isOwner,
|
||||
app,
|
||||
user,
|
||||
};
|
||||
};
|
||||
export const deleteProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
||||
const { objectName, isOwner } = await getObjectName(req);
|
||||
let oss = opts.oss;
|
||||
if (!isOwner) {
|
||||
return opts?.createNotFoundPage?.('no permission');
|
||||
}
|
||||
try {
|
||||
await oss.deleteObject(objectName);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, message: 'delete success', objectName }));
|
||||
} catch (error) {
|
||||
logger.error('deleteProxy error', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: error }));
|
||||
}
|
||||
};
|
||||
|
||||
type ProxyOptions = {
|
||||
createNotFoundPage: (msg?: string) => any;
|
||||
oss?: OssBase;
|
||||
};
|
||||
export const aiProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
||||
const oss = new OssBase({ bucketName, client: minioClient });
|
||||
if (!opts.oss) {
|
||||
opts.oss = oss;
|
||||
}
|
||||
if (req.method === 'POST') {
|
||||
return postProxy(req, res, opts);
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
return deleteProxy(req, res, opts);
|
||||
}
|
||||
|
||||
return getAiProxy(req, res, opts);
|
||||
};
|
||||
25
src/modules/fm-manager/proxy/file-proxy.ts
Normal file
25
src/modules/fm-manager/proxy/file-proxy.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import http from 'node:http';
|
||||
import send from 'send';
|
||||
import { fileIsExist } from '@kevisual/use-config';
|
||||
import path from 'node:path';
|
||||
export type ProxyInfo = {
|
||||
path?: string;
|
||||
target: string;
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
};
|
||||
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
// url开头的文件
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const pathname = url.pathname;
|
||||
// 检测文件是否存在,如果文件不存在,则返回404
|
||||
const filePath = path.join(process.cwd(), proxyApi.target, pathname);
|
||||
if (!fileIsExist(filePath)) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found File');
|
||||
return;
|
||||
}
|
||||
const file = send(req, pathname, {
|
||||
root: proxyApi.target,
|
||||
});
|
||||
file.pipe(res);
|
||||
};
|
||||
165
src/modules/fm-manager/proxy/http-proxy.ts
Normal file
165
src/modules/fm-manager/proxy/http-proxy.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { pipeline, Readable } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import { bucketName, minioClient, minioResources } from '@/modules/minio.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';
|
||||
|
||||
const pipelineAsync = promisify(pipeline);
|
||||
|
||||
export async function downloadFileFromMinio(fileUrl: string, destFile: string) {
|
||||
const objectName = fileUrl.replace(minioResources + '/', '');
|
||||
const objectStream = await minioClient.getObject(bucketName, objectName);
|
||||
const destStream = fs.createWriteStream(destFile);
|
||||
await pipelineAsync(objectStream, destStream);
|
||||
console.log(`minio File downloaded to ${minioResources}/${objectName} \n ${destFile}`);
|
||||
}
|
||||
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 + '/', '');
|
||||
try {
|
||||
const stat = await minioClient.statObject(bucketName, 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 objectStream = await minioClient.getObject(bucketName, objectName);
|
||||
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,
|
||||
});
|
||||
objectStream.pipe(res, { end: true });
|
||||
}
|
||||
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)) {
|
||||
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 {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
...headers,
|
||||
});
|
||||
proxyRes.pipe(res, { end: true });
|
||||
}
|
||||
});
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error(`Proxy request error: ${err.message}`);
|
||||
userApp.clearCacheData();
|
||||
});
|
||||
proxyReq.end();
|
||||
};
|
||||
24
src/modules/fm-manager/proxy/minio-proxy.ts
Normal file
24
src/modules/fm-manager/proxy/minio-proxy.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import http from 'http';
|
||||
import { minioClient } from '@/modules/minio.ts';
|
||||
type ProxyInfo = {
|
||||
path?: string;
|
||||
target: string;
|
||||
type?: 'static' | 'dynamic' | 'minio';
|
||||
};
|
||||
export const minioProxyOrigin = async (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||
try {
|
||||
const requestUrl = new URL(req.url, 'http://localhost');
|
||||
const objectPath = requestUrl.pathname;
|
||||
const bucketName = proxyApi.target;
|
||||
let objectName = objectPath.slice(1);
|
||||
if (objectName.startsWith(bucketName)) {
|
||||
objectName = objectName.slice(bucketName.length);
|
||||
}
|
||||
const objectStream = await minioClient.getObject(bucketName, objectName);
|
||||
objectStream.pipe(res);
|
||||
} catch (error) {
|
||||
console.error('Error fetching object from MinIO:', error);
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
};
|
||||
31
src/modules/fm-manager/utils.ts
Normal file
31
src/modules/fm-manager/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IncomingMessage } from 'node:http';
|
||||
import http from 'node:http';
|
||||
|
||||
export const getUserFromRequest = (req: IncomingMessage) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const pathname = url.pathname;
|
||||
const keys = pathname.split('/');
|
||||
const [_, user, app] = keys;
|
||||
return {
|
||||
user,
|
||||
app,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const getDNS = (req: http.IncomingMessage) => {
|
||||
const hostName = req.headers.host;
|
||||
const ip = req.socket.remoteAddress;
|
||||
return { hostName, ip };
|
||||
};
|
||||
|
||||
export const isLocalhost = (hostName: string) => {
|
||||
return hostName.includes('localhost') || hostName.includes('192.168');
|
||||
};
|
||||
|
||||
export const isIpv4OrIpv6 = (hostName: string) => {
|
||||
const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
const ipv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||
return ipv4.test(hostName) || ipv6.test(hostName);
|
||||
};
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
Reference in New Issue
Block a user