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(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'); const _fileSize: string = params.get('size'); let fileSize: number | undefined = undefined; if (_fileSize) { fileSize = parseInt(_fileSize, 10) } 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 { await oss.putObject( objectName, file, { ...statMeta, ...getMetadata(pathname), ...meta, }, { check: false, isStream: true, size: fileSize }, ); 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> = 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); };