feat: add minio proxy
This commit is contained in:
parent
bb86a7c507
commit
07cfa1dded
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,4 +10,7 @@ app.config.json5
|
|||||||
release/*
|
release/*
|
||||||
!release/.gitkeep
|
!release/.gitkeep
|
||||||
|
|
||||||
/*.tgz
|
/*.tgz
|
||||||
|
|
||||||
|
proxy-upload/*
|
||||||
|
proxy-upload/.gitkeep
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
api: {
|
api: {
|
||||||
target: 'http://localhost:4002', // 后台代理
|
host: 'http://localhost:4002', // 后台代理
|
||||||
path: '/api/router',
|
path: '/api/router',
|
||||||
},
|
},
|
||||||
apiList: [
|
apiList: [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "page-proxy",
|
"name": "page-proxy",
|
||||||
"version": "0.0.2-beta.3",
|
"version": "0.0.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -62,7 +62,15 @@ export class UserApp {
|
|||||||
const user = this.user;
|
const user = this.user;
|
||||||
const key = 'user:app:exist:' + app + ':' + user;
|
const key = 'user:app:exist:' + app + ':' + user;
|
||||||
const value = await redis.get(key);
|
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 user = this.user;
|
||||||
const key = 'user:app:set:' + app + ':' + user;
|
const key = 'user:app:set:' + app + ':' + user;
|
||||||
const value = await redis.hget(key, appFileUrl);
|
const value = await redis.hget(key, appFileUrl);
|
||||||
|
// const values = await redis.hgetall(key);
|
||||||
|
// console.log('getFile', values);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
static async getDomainApp(domain: string) {
|
static async getDomainApp(domain: string) {
|
||||||
@ -170,7 +180,28 @@ export class UserApp {
|
|||||||
// return false;
|
// return false;
|
||||||
fetchData.type = 'oss';
|
fetchData.type = 'oss';
|
||||||
}
|
}
|
||||||
|
console.log('fetchData', JSON.stringify(fetchData.data.files, null, 2));
|
||||||
|
|
||||||
this.setLoaded('loading', 'loading');
|
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 loadFilesFn = async () => {
|
||||||
const value = await downloadUserAppFiles(user, app, fetchData);
|
const value = await downloadUserAppFiles(user, app, fetchData);
|
||||||
if (value.data.files.length === 0) {
|
if (value.data.files.length === 0) {
|
||||||
@ -191,15 +222,8 @@ export class UserApp {
|
|||||||
encoding: 'utf-8',
|
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(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 files = value.data.files;
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
@ -211,7 +235,15 @@ export class UserApp {
|
|||||||
this.setLoaded('running', 'loaded');
|
this.setLoaded('running', 'loaded');
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
loadFilesFn();
|
if (fetchData.proxy === true) {
|
||||||
|
await loadProxy();
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: 'loaded',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
loadFilesFn();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('loadFilesFn error', e);
|
console.error('loadFilesFn error', e);
|
||||||
this.setLoaded('error', 'loadFilesFn error');
|
this.setLoaded('error', 'loadFilesFn error');
|
||||||
@ -293,11 +325,15 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type
|
|||||||
}
|
}
|
||||||
if (data.type === 'oss') {
|
if (data.type === 'oss') {
|
||||||
let serverPath = new URL(resources).href + '/';
|
let serverPath = new URL(resources).href + '/';
|
||||||
|
let hasIndexHtml = false;
|
||||||
// server download file
|
// server download file
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const destFile = path.join(uploadFiles, file.name);
|
const destFile = path.join(uploadFiles, file.name);
|
||||||
const destDir = path.dirname(destFile); // 获取目标文件所在的目录路径
|
const destDir = path.dirname(destFile); // 获取目标文件所在的目录路径
|
||||||
|
if (file.name === 'index.html') {
|
||||||
|
hasIndexHtml = true;
|
||||||
|
}
|
||||||
// 检查目录是否存在,如果不存在则创建
|
// 检查目录是否存在,如果不存在则创建
|
||||||
if (!checkFileExistsSync(destDir)) {
|
if (!checkFileExistsSync(destDir)) {
|
||||||
fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
|
fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
|
||||||
@ -310,6 +346,15 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type
|
|||||||
path: destFile.replace(fileStore, '') + '||' + etag,
|
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 {
|
return {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { getDNS, isLocalhost } from '@/utils/dns.ts';
|
import { getDNS, isLocalhost } from '@/utils/dns.ts';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
import https from 'https';
|
||||||
import { UserApp } from './get-user-app.ts';
|
import { UserApp } from './get-user-app.ts';
|
||||||
import { config, fileStore } from '../module/config.ts';
|
import { config, fileStore } from '../module/config.ts';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@ -25,7 +26,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
// 已经代理过了
|
// 已经代理过了
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('req', req.url, 'len', config?.apiList?.length);
|
|
||||||
const proxyApiList = config?.apiList || [];
|
const proxyApiList = config?.apiList || [];
|
||||||
const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
|
const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
|
||||||
if (proxyApi && proxyApi?.type === 'static') {
|
if (proxyApi && proxyApi?.type === 'static') {
|
||||||
@ -33,7 +33,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
}
|
}
|
||||||
if (proxyApi) {
|
if (proxyApi) {
|
||||||
const _u = new URL(req.url, `${proxyApi.target}`);
|
const _u = new URL(req.url, `${proxyApi.target}`);
|
||||||
console.log('proxyApi', req.url, _u.href);
|
|
||||||
// 设置代理请求的目标 URL 和请求头
|
// 设置代理请求的目标 URL 和请求头
|
||||||
let header: any = {};
|
let header: any = {};
|
||||||
if (req.headers?.['Authorization']) {
|
if (req.headers?.['Authorization']) {
|
||||||
@ -160,30 +159,40 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
|
|
||||||
const userApp = new UserApp({ user, app });
|
const userApp = new UserApp({ user, app });
|
||||||
let isExist = await userApp.getExist();
|
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) {
|
if (!isExist) {
|
||||||
try {
|
try {
|
||||||
const { code, loading } = await userApp.setCacheData();
|
const { code, loading } = await userApp.setCacheData();
|
||||||
if (loading || code === 20000) {
|
if (loading || code === 20000) {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
return createRefreshPage();
|
||||||
|
} else if (code !== 200) {
|
||||||
res.end(createRefreshHtml(user, app));
|
return createErrorPage();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
isExist = await userApp.getExist();
|
||||||
res.write('Not Found App\n');
|
|
||||||
res.end();
|
|
||||||
// 不存在就一定先返回loading状态。
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('setCacheData error', error);
|
console.error('setCacheData error', error);
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
createErrorPage();
|
||||||
res.write('Server Error\n');
|
|
||||||
res.end();
|
|
||||||
userApp.setLoaded('error', 'setCacheData error');
|
userApp.setLoaded('error', 'setCacheData error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const indexFile = isExist; // 已经必定存在了
|
if (!isExist) {
|
||||||
|
return createNotFoundPage();
|
||||||
|
}
|
||||||
|
const indexFile = isExist.indexFilePath; // 已经必定存在了
|
||||||
try {
|
try {
|
||||||
let appFileUrl: string;
|
let appFileUrl: string;
|
||||||
if (domainApp) {
|
if (domainApp) {
|
||||||
@ -191,15 +200,47 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
} else {
|
} else {
|
||||||
appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
|
appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const appFile = await userApp.getFile(appFileUrl);
|
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) {
|
if (!appFile) {
|
||||||
const [indexFilePath, etag] = indexFile.split('||');
|
const [indexFilePath, etag] = indexFile.split('||');
|
||||||
const contentType = getContentType(indexFilePath);
|
const contentType = getContentType(indexFilePath);
|
||||||
const isHTML = contentType.includes('html');
|
const isHTML = contentType.includes('html');
|
||||||
const filePath = path.join(fileStore, indexFilePath);
|
const filePath = path.join(fileStore, indexFilePath);
|
||||||
if (!userApp.fileCheck(filePath)) {
|
if (!userApp.fileCheck(filePath)) {
|
||||||
|
// 动态删除文件
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
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();
|
res.end();
|
||||||
await userApp.clearCacheData();
|
await userApp.clearCacheData();
|
||||||
return;
|
return;
|
||||||
@ -255,4 +296,3 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
console.error('getFile error', error);
|
console.error('getFile error', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { config } from '../config.ts';
|
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';
|
const apiPath = api.path || '/api/router';
|
||||||
export const fetchTest = async (id: string) => {
|
export const fetchTest = async (id: string) => {
|
||||||
const fetchUrl = 'http://' + api.host + apiPath;
|
const fetchUrl = api.host + apiPath;
|
||||||
const fetchRes = await fetch(fetchUrl, {
|
const fetchRes = await fetch(fetchUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -19,7 +19,7 @@ export const fetchTest = async (id: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchDomain = async (domain: string) => {
|
export const fetchDomain = async (domain: string) => {
|
||||||
const fetchUrl = 'http://' + api.host + apiPath;
|
const fetchUrl = api.host + apiPath;
|
||||||
const fetchRes = await fetch(fetchUrl, {
|
const fetchRes = await fetch(fetchUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -37,7 +37,7 @@ export const fetchDomain = async (domain: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchApp = async ({ user, app }) => {
|
export const fetchApp = async ({ user, app }) => {
|
||||||
const fetchUrl = 'http://' + api.host + apiPath;
|
const fetchUrl = api.host + apiPath;
|
||||||
const fetchRes = await fetch(fetchUrl, {
|
const fetchRes = await fetch(fetchUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user