update
This commit is contained in:
@@ -76,7 +76,9 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.969.0",
|
||||||
"@kevisual/ha-api": "^0.0.6",
|
"@kevisual/ha-api": "^0.0.6",
|
||||||
|
"@kevisual/oss": "^0.0.16",
|
||||||
"@kevisual/video-tools": "^0.0.13",
|
"@kevisual/video-tools": "^0.0.13",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
|
|||||||
@@ -117,3 +117,5 @@ export const httpProxy = (req: http.IncomingMessage, res: http.ServerResponse, p
|
|||||||
// req.pipe(proxyReq, { end: true });
|
// req.pipe(proxyReq, { end: true });
|
||||||
pipeProxyReq(req, proxyReq, res);
|
pipeProxyReq(req, proxyReq, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { isBun } from './utils.ts';
|
import { isBun } from './utils.ts';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件流管道传输函数
|
* 文件流管道传输函数
|
||||||
@@ -25,7 +26,7 @@ export const pipeFileStream = (filePath: string, res: http.ServerResponse) => {
|
|||||||
* @param readStream 可读流对象
|
* @param readStream 可读流对象
|
||||||
* @param res HTTP服务器响应对象
|
* @param res HTTP服务器响应对象
|
||||||
*/
|
*/
|
||||||
export const pipeStream = (readStream: fs.ReadStream, res: http.ServerResponse) => {
|
export const pipeStream = (readStream: fs.ReadStream | Readable, res: http.ServerResponse) => {
|
||||||
if (isBun) {
|
if (isBun) {
|
||||||
// Bun环境下的流处理方式
|
// Bun环境下的流处理方式
|
||||||
res.pipe(readStream as any);
|
res.pipe(readStream as any);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import http from 'node:http';
|
||||||
|
import { httpProxy } from './http-proxy.ts';
|
||||||
|
import { s3Proxy } from './s3.ts';
|
||||||
export type ProxyInfo = {
|
export type ProxyInfo = {
|
||||||
/**
|
/**
|
||||||
* 代理路径, 比如/root/home, 匹配的路径
|
* 代理路径, 比如/root/home, 匹配的路径
|
||||||
@@ -10,7 +13,7 @@ export type ProxyInfo = {
|
|||||||
/**
|
/**
|
||||||
* 类型
|
* 类型
|
||||||
*/
|
*/
|
||||||
type?: 'file' | 'dynamic' | 'minio' | 'http';
|
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3';
|
||||||
/**
|
/**
|
||||||
* 目标的 pathname, 默认为请求的url.pathname, 设置了pathname,则会使用pathname作为请求的url.pathname
|
* 目标的 pathname, 默认为请求的url.pathname, 设置了pathname,则会使用pathname作为请求的url.pathname
|
||||||
* @default undefined
|
* @default undefined
|
||||||
@@ -32,31 +35,21 @@ export type ProxyInfo = {
|
|||||||
* 根路径, 默认是process.cwd(), type为fileProxy代理有用,必须为绝对路径
|
* 根路径, 默认是process.cwd(), type为fileProxy代理有用,必须为绝对路径
|
||||||
*/
|
*/
|
||||||
rootPath?: string;
|
rootPath?: string;
|
||||||
|
s3?: {
|
||||||
|
bucket: string;
|
||||||
|
region: string;
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
endpoint?: string;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiList = {
|
export const proxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||||
path: string;
|
if (proxyApi.type === 'http' || !proxyApi.type) {
|
||||||
/**
|
return httpProxy(req, res, proxyApi);
|
||||||
* url或者相对路径
|
}
|
||||||
*/
|
console.log('proxyApi', proxyApi);
|
||||||
target: string;
|
if (proxyApi.type === 's3') {
|
||||||
/**
|
return s3Proxy(req, res, proxyApi);
|
||||||
* 目标地址
|
}
|
||||||
*/
|
}
|
||||||
ws?: boolean;
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
type?: 'static' | 'dynamic' | 'minio';
|
|
||||||
}[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
path: '/api/v1/user',
|
|
||||||
target: 'http://localhost:3000/api/v1/user',
|
|
||||||
type: 'dynamic',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
71
assistant/src/module/assistant/proxy/s3.ts
Normal file
71
assistant/src/module/assistant/proxy/s3.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { S3Client } from '@aws-sdk/client-s3';
|
||||||
|
import { ProxyInfo } from './proxy.ts';
|
||||||
|
import http from 'http';
|
||||||
|
import { OssBase } from '@kevisual/oss/s3.ts';
|
||||||
|
import { pipeStream } from './pipe.ts';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
const mapS3 = new Map<string, S3Client>();
|
||||||
|
|
||||||
|
export const s3Proxy = async (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||||
|
const s3 = proxyApi.s3;
|
||||||
|
if (!s3) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('S3 config not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let findClient = mapS3.get(s3.accessKeyId);
|
||||||
|
let s3Client: S3Client;
|
||||||
|
if (findClient) {
|
||||||
|
s3Client = findClient;
|
||||||
|
} else {
|
||||||
|
s3Client = new S3Client({
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: s3.accessKeyId,
|
||||||
|
secretAccessKey: s3.secretAccessKey,
|
||||||
|
},
|
||||||
|
region: s3.region,
|
||||||
|
endpoint: s3.endpoint,
|
||||||
|
});
|
||||||
|
mapS3.set(s3.accessKeyId, s3Client);
|
||||||
|
}
|
||||||
|
const oss = new OssBase({
|
||||||
|
client: s3Client!,
|
||||||
|
bucketName: s3.bucket,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestUrl = new URL(req.url, 'http://localhost');
|
||||||
|
const proxyPath = proxyApi.path || '';
|
||||||
|
let objectPath = requestUrl.pathname.replace(proxyPath + '/', '');
|
||||||
|
if (objectPath.startsWith(s3.bucket + '/')) {
|
||||||
|
objectPath = objectPath.replace(s3.bucket + '/', '');
|
||||||
|
}
|
||||||
|
oss.getObject(objectPath).then((response) => {
|
||||||
|
if (!response.Body) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Object not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置响应头
|
||||||
|
if (response.ContentType) {
|
||||||
|
res.setHeader('Content-Type', response.ContentType);
|
||||||
|
}
|
||||||
|
if (response.ContentLength) {
|
||||||
|
res.setHeader('Content-Length', response.ContentLength);
|
||||||
|
}
|
||||||
|
if (response.LastModified) {
|
||||||
|
res.setHeader('Last-Modified', response.LastModified.toUTCString());
|
||||||
|
}
|
||||||
|
if (response.ETag) {
|
||||||
|
res.setHeader('ETag', response.ETag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// response.Body 已经是 Readable 流,直接 pipe 到 res
|
||||||
|
// (response.Body as Readable).pipe(res);
|
||||||
|
pipeStream(response.Body as Readable, res);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('S3 getObject error:', err);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(`S3 error: ${err.message}`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fileProxy, httpProxy, createApiProxy, ProxyInfo } from '@/module/assistant/index.ts';
|
import { fileProxy, httpProxy, createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { LocalProxy } from './local-proxy.ts';
|
import { LocalProxy } from './local-proxy.ts';
|
||||||
import { assistantConfig, app } from '@/app.ts';
|
import { assistantConfig, app } from '@/app.ts';
|
||||||
@@ -110,12 +110,19 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
// client, api, v1, serve 开头的拦截
|
// client, api, v1, serve 开头的拦截
|
||||||
const apiProxy = _assistantConfig?.api?.proxy || [];
|
const apiProxy = _assistantConfig?.api?.proxy || [];
|
||||||
const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn');
|
const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn');
|
||||||
const apiBackendProxy = [...apiProxy, ...defaultApiProxy].find((item) => pathname.startsWith(item.path));
|
const allProxy = [...apiProxy, ...defaultApiProxy];
|
||||||
|
const apiBackendProxy = allProxy.find((item) => pathname.startsWith(item.path));
|
||||||
|
// console.log('apiBackendProxy', allProxy, apiBackendProxy, pathname, apiProxy[0].path);
|
||||||
if (apiBackendProxy) {
|
if (apiBackendProxy) {
|
||||||
log.debug('apiBackendProxy', { apiBackendProxy, url: req.url });
|
log.debug('apiBackendProxy', { apiBackendProxy, url: req.url });
|
||||||
return httpProxy(req, res, {
|
// 设置 CORS 头
|
||||||
|
// res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
// res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
// res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
||||||
|
return proxy(req, res, {
|
||||||
path: apiBackendProxy.path,
|
path: apiBackendProxy.path,
|
||||||
target: apiBackendProxy.target,
|
target: apiBackendProxy.target,
|
||||||
|
...apiBackendProxy
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
logger.debug('proxyRoute handle by router', { url: req.url }, noAdmin);
|
logger.debug('proxyRoute handle by router', { url: req.url }, noAdmin);
|
||||||
|
|||||||
1233
pnpm-lock.yaml
generated
1233
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user