From 07cfa1dded315e6bb66f2aee807346efe2b00177 Mon Sep 17 00:00:00 2001 From: xion Date: Tue, 11 Mar 2025 12:54:37 +0800 Subject: [PATCH] feat: add minio proxy --- .gitignore | 5 ++- app.config.json5 | 2 +- package.json | 2 +- src/module/get-user-app.ts | 65 ++++++++++++++++++++++++----- src/module/index.ts | 74 ++++++++++++++++++++++++++-------- src/module/query/get-router.ts | 8 ++-- 6 files changed, 122 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index e01c9f9..1957851 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ app.config.json5 release/* !release/.gitkeep -/*.tgz \ No newline at end of file +/*.tgz + +proxy-upload/* +proxy-upload/.gitkeep \ No newline at end of file diff --git a/app.config.json5 b/app.config.json5 index 8be320b..a19ebb6 100644 --- a/app.config.json5 +++ b/app.config.json5 @@ -1,6 +1,6 @@ { api: { - target: 'http://localhost:4002', // 后台代理 + host: 'http://localhost:4002', // 后台代理 path: '/api/router', }, apiList: [ diff --git a/package.json b/package.json index 624a47c..39a47bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "page-proxy", - "version": "0.0.2-beta.3", + "version": "0.0.3", "description": "", "main": "index.js", "type": "module", diff --git a/src/module/get-user-app.ts b/src/module/get-user-app.ts index 269041c..fb99b85 100644 --- a/src/module/get-user-app.ts +++ b/src/module/get-user-app.ts @@ -62,7 +62,15 @@ export class UserApp { const user = this.user; const key = 'user:app:exist:' + app + ':' + user; const value = await redis.get(key); - return value; + if (!value) { + return false; + } + const [indexFilePath, etag, proxy] = value.split('||'); + return { + indexFilePath, + etag, + proxy: proxy === 'true', + }; } /** * 获取缓存数据,不存在不会加载 @@ -83,6 +91,8 @@ export class UserApp { const user = this.user; const key = 'user:app:set:' + app + ':' + user; const value = await redis.hget(key, appFileUrl); + // const values = await redis.hgetall(key); + // console.log('getFile', values); return value; } static async getDomainApp(domain: string) { @@ -170,7 +180,28 @@ export class UserApp { // return false; fetchData.type = 'oss'; } + console.log('fetchData', JSON.stringify(fetchData.data.files, null, 2)); + this.setLoaded('loading', 'loading'); + const loadProxy = async () => { + const value = fetchData; + await redis.set(key, JSON.stringify(value)); + const version = value.version; + let indexHtml = resources + '/' + user + '/' + app + '/' + version + '/index.html'; + const files = value?.data?.files || []; + const data = {}; + + // 将文件名和路径添加到 `data` 对象中 + files.forEach((file) => { + if (file.name === 'index.html') { + indexHtml = resources + '/' + file.path; + } + data[file.name] = resources + '/' + file.path; + }); + await redis.set('user:app:exist:' + app + ':' + user, indexHtml + '||etag||true', 'EX', 60 * 60 * 24 * 7); // 7天 + await redis.hset('user:app:set:' + app + ':' + user, data); + this.setLoaded('running', 'loaded'); + }; const loadFilesFn = async () => { const value = await downloadUserAppFiles(user, app, fetchData); if (value.data.files.length === 0) { @@ -191,15 +222,8 @@ export class UserApp { encoding: 'utf-8', }); } - let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html'); - if (!valueIndexHtml) { - valueIndexHtml = value.data.files.find((file) => file.name === 'index.js'); - if (!valueIndexHtml) { - valueIndexHtml = value.data.files[0]; - } - } await redis.set(key, JSON.stringify(value)); - await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 7天 + await redis.set('user:app:exist:' + app + ':' + user, 'index.html||etag||false', 'EX', 60 * 60 * 24 * 7); // 7天 const files = value.data.files; const data = {}; @@ -211,7 +235,15 @@ export class UserApp { this.setLoaded('running', 'loaded'); }; try { - loadFilesFn(); + if (fetchData.proxy === true) { + await loadProxy(); + return { + code: 200, + data: 'loaded', + }; + } else { + loadFilesFn(); + } } catch (e) { console.error('loadFilesFn error', e); this.setLoaded('error', 'loadFilesFn error'); @@ -293,11 +325,15 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type } if (data.type === 'oss') { let serverPath = new URL(resources).href + '/'; + let hasIndexHtml = false; // server download file for (let i = 0; i < files.length; i++) { const file = files[i]; const destFile = path.join(uploadFiles, file.name); const destDir = path.dirname(destFile); // 获取目标文件所在的目录路径 + if (file.name === 'index.html') { + hasIndexHtml = true; + } // 检查目录是否存在,如果不存在则创建 if (!checkFileExistsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录 @@ -310,6 +346,15 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type path: destFile.replace(fileStore, '') + '||' + etag, }); } + if (!hasIndexHtml) { + newFiles.push({ + name: 'index.html', + path: path.join(uploadFiles, 'index.html'), + }); + fs.writeFileSync(path.join(uploadFiles, 'index.html'), JSON.stringify(files), { + encoding: 'utf-8', + }); + } } return { diff --git a/src/module/index.ts b/src/module/index.ts index 1dbd0d4..51829ed 100644 --- a/src/module/index.ts +++ b/src/module/index.ts @@ -1,5 +1,6 @@ import { getDNS, isLocalhost } from '@/utils/dns.ts'; import http from 'http'; +import https from 'https'; import { UserApp } from './get-user-app.ts'; import { config, fileStore } from '../module/config.ts'; import path from 'path'; @@ -25,7 +26,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR // 已经代理过了 return; } - console.log('req', req.url, 'len', config?.apiList?.length); const proxyApiList = config?.apiList || []; const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path)); if (proxyApi && proxyApi?.type === 'static') { @@ -33,7 +33,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR } if (proxyApi) { const _u = new URL(req.url, `${proxyApi.target}`); - console.log('proxyApi', req.url, _u.href); // 设置代理请求的目标 URL 和请求头 let header: any = {}; if (req.headers?.['Authorization']) { @@ -160,30 +159,40 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR const userApp = new UserApp({ user, app }); let isExist = await userApp.getExist(); + const createRefreshPage = () => { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(createRefreshHtml(user, app)); + }; + const createErrorPage = () => { + res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); + res.write('Server Error\n'); + res.end(); + }; + const createNotFoundPage = (msg?: string) => { + res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' }); + res.write(msg || 'Not Found App\n'); + res.end(); + }; if (!isExist) { try { const { code, loading } = await userApp.setCacheData(); if (loading || code === 20000) { - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - - res.end(createRefreshHtml(user, app)); - return; + return createRefreshPage(); + } else if (code !== 200) { + return createErrorPage(); } - res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); - res.write('Not Found App\n'); - res.end(); - // 不存在就一定先返回loading状态。 - return; + isExist = await userApp.getExist(); } catch (error) { console.error('setCacheData error', error); - res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); - res.write('Server Error\n'); - res.end(); + createErrorPage(); userApp.setLoaded('error', 'setCacheData error'); return; } } - const indexFile = isExist; // 已经必定存在了 + if (!isExist) { + return createNotFoundPage(); + } + const indexFile = isExist.indexFilePath; // 已经必定存在了 try { let appFileUrl: string; if (domainApp) { @@ -191,15 +200,47 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR } else { appFileUrl = (url + '').replace(`/${user}/${app}/`, ''); } + const appFile = await userApp.getFile(appFileUrl); + if (isExist.proxy) { + let proxyUrl = appFile || isExist.indexFilePath; + if (!proxyUrl.startsWith('http')) { + return createNotFoundPage('Invalid proxy url'); + } + let protocol = proxyUrl.startsWith('https') ? https : http; + // 代理 + const proxyReq = protocol.request(proxyUrl, (proxyRes) => { + res.writeHead(proxyRes.statusCode, { + ...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(); + } + proxyRes.pipe(res, { end: true }); + }); + proxyReq.on('error', (err) => { + console.error(`Proxy request error: ${err.message}`); + userApp.clearCacheData(); + }); + proxyReq.end(); + // userApp.clearCacheData() + return; + } + console.log('appFile', appFile, appFileUrl); if (!appFile) { const [indexFilePath, etag] = indexFile.split('||'); const contentType = getContentType(indexFilePath); const isHTML = contentType.includes('html'); const filePath = path.join(fileStore, indexFilePath); if (!userApp.fileCheck(filePath)) { + // 动态删除文件 res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); - res.write('File expired, Not Found\n'); + res.write('App Cache expired, Please refresh\n'); res.end(); await userApp.clearCacheData(); return; @@ -255,4 +296,3 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR console.error('getFile error', error); } }; - diff --git a/src/module/query/get-router.ts b/src/module/query/get-router.ts index 5b7d7a7..faafc2f 100644 --- a/src/module/query/get-router.ts +++ b/src/module/query/get-router.ts @@ -1,9 +1,9 @@ import { config } from '../config.ts'; -const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' }; +const api = config?.api || { host: 'https://kevisual.xiongxiao.me', path: '/api/router' }; const apiPath = api.path || '/api/router'; export const fetchTest = async (id: string) => { - const fetchUrl = 'http://' + api.host + apiPath; + const fetchUrl = api.host + apiPath; const fetchRes = await fetch(fetchUrl, { method: 'POST', headers: { @@ -19,7 +19,7 @@ export const fetchTest = async (id: string) => { }; export const fetchDomain = async (domain: string) => { - const fetchUrl = 'http://' + api.host + apiPath; + const fetchUrl = api.host + apiPath; const fetchRes = await fetch(fetchUrl, { method: 'POST', headers: { @@ -37,7 +37,7 @@ export const fetchDomain = async (domain: string) => { }; export const fetchApp = async ({ user, app }) => { - const fetchUrl = 'http://' + api.host + apiPath; + const fetchUrl = api.host + apiPath; const fetchRes = await fetch(fetchUrl, { method: 'POST', headers: {