feat: add minio proxy

This commit is contained in:
xion 2025-03-11 12:54:37 +08:00
parent bb86a7c507
commit 07cfa1dded
6 changed files with 122 additions and 34 deletions

3
.gitignore vendored
View File

@ -11,3 +11,6 @@ release/*
!release/.gitkeep
/*.tgz
proxy-upload/*
proxy-upload/.gitkeep

View File

@ -1,6 +1,6 @@
{
api: {
target: 'http://localhost:4002', // 后台代理
host: 'http://localhost:4002', // 后台代理
path: '/api/router',
},
apiList: [

View File

@ -1,6 +1,6 @@
{
"name": "page-proxy",
"version": "0.0.2-beta.3",
"version": "0.0.3",
"description": "",
"main": "index.js",
"type": "module",

View File

@ -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 {

View File

@ -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);
}
};

View File

@ -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: {