add status for redis

This commit is contained in:
abearxiong 2025-02-27 18:54:24 +08:00
parent 0b47b38060
commit 1662fd4dfa
9 changed files with 822 additions and 90 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "page-proxy", "name": "page-proxy",
"version": "0.0.1", "version": "0.0.2-beta.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@ -16,12 +16,15 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"watch": "rollup -c --watch",
"dev": "cross-env NODE_ENV=development nodemon --ignore upload --exec tsx src/index.ts", "dev": "cross-env NODE_ENV=development nodemon --ignore upload --exec tsx src/index.ts",
"dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ",
"build": "rimraf dist && rollup -c", "build": "rimraf dist && rollup -c",
"start": "pm2 start dist/app.mjs --name page-proxy", "start": "pm2 start dist/app.mjs --name page-proxy",
"release": "node ./scripts/release/index.mjs", "release": "node ./scripts/release/index.mjs",
"deploy": "envision switch root && envision pack -p -u", "deploy": "envision switch root && envision pack -p -u",
"pub": "npm run build && npm run deploy" "pub": "npm run build && npm run deploy",
"ssl": "ssh -L 6379:localhost:6379 light"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -33,6 +36,7 @@
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.2",
"@types/http-proxy": "^1.17.16", "@types/http-proxy": "^1.17.16",
"@types/node": "^22.13.5", "@types/node": "^22.13.5",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"rollup": "^4.34.8", "rollup": "^4.34.8",
@ -42,6 +46,7 @@
"dependencies": { "dependencies": {
"@kevisual/router": "0.0.7", "@kevisual/router": "0.0.7",
"@kevisual/use-config": "^1.0.8", "@kevisual/use-config": "^1.0.8",
"archiver": "^7.0.1",
"ioredis": "^5.5.0", "ioredis": "^5.5.0",
"nanoid": "^5.1.2" "nanoid": "^5.1.2"
}, },

634
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,19 @@ import { nanoid } from 'nanoid';
import { pipeline } from 'stream'; import { pipeline } from 'stream';
import { promisify } from 'util'; import { promisify } from 'util';
import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts'; import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts';
import { getAppLoadStatus, setAppLoadStatus, AppLoadStatus } from './redis/get-app-status.ts';
const pipelineAsync = promisify(pipeline); const pipelineAsync = promisify(pipeline);
const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' }; const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' };
const status: { [key: string]: boolean } = {};
const demoData = { const demoData = {
user: 'root', user: 'root',
key: 'codeflow', key: 'codeflow',
appType: 'web-single', // appType: 'web-single', //
version: '1.0.0', version: '1.0.0',
domain: null, domain: null,
type: 'local', type: 'local', // local, oss 默认是oss
data: { data: {
files: [ files: [
{ {
@ -51,6 +53,10 @@ export class UserApp {
this.isTest = true; this.isTest = true;
} }
} }
/**
*
* @returns
*/
async getExist() { async getExist() {
const app = this.app; const app = this.app;
const user = this.user; const user = this.user;
@ -106,60 +112,76 @@ export class UserApp {
user: fetchData.user, user: fetchData.user,
app: fetchData.key, app: fetchData.key,
}; };
redis.set(key, data.user + ':' + data.app, 'EX', 60 * 60 * 24 * 7); // 24小时 redis.set(key, data.user + ':' + data.app, 'EX', 60 * 60 * 24 * 7); // 7天
return data; return data;
} }
async setLoaded() { /**
*
* @param msg
*/
async setLoaded(status: 'running' | 'error' | 'loading', msg?: string) {
const app = this.app; const app = this.app;
const user = this.user; const user = this.user;
const key = 'user:app:' + app + ':' + user; await setAppLoadStatus(user, app, {
if (status[key]) { status,
status[key] = false; message: msg,
} });
} }
async getLoaded() { async getLoaded() {
const app = this.app; const app = this.app;
const user = this.user; const user = this.user;
const key = 'user:app:' + app + ':' + user; const value = await getAppLoadStatus(user, app);
return status[key]; return value;
} }
/**
*
* @returns
*/
async setCacheData() { async setCacheData() {
const app = this.app; const app = this.app;
const user = this.user; const user = this.user;
const isTest = this.isTest; const isTest = this.isTest;
const key = 'user:app:' + app + ':' + user; const key = 'user:app:' + app + ':' + user;
if (status[key]) { const fetchRes = isTest ? await fetchTest(app) : await fetchApp({ user, app });
if (fetchRes?.code !== 200) {
console.log('fetchRes is error', fetchRes, 'user', user, 'app', app);
return { code: 500, message: 'fetchRes is error' };
}
const loadStatus = await getAppLoadStatus(user, app);
if (loadStatus.status === 'loading') {
// 其他情况error或者running都可以重新加载
return { return {
loading: true, loading: true,
}; };
} }
status[key] = true;
const fetchRes = isTest ? await fetchTest(app) : await fetchApp({ user, app });
if (fetchRes?.code !== 200) {
console.log('fetchRes is error', fetchRes);
this.setLoaded();
return { code: 500, message: 'fetchRes is error' };
}
const fetchData = fetchRes.data; const fetchData = fetchRes.data;
if (!fetchData.type) { if (!fetchData.type) {
// console.error('fetchData type is error', fetchData); // console.error('fetchData type is error', fetchData);
// return false; // return false;
fetchData.type = 'oss'; fetchData.type = 'oss';
} }
if (fetchData.status !== 'running') { this.setLoaded('loading', 'loading');
console.error('fetchData status is not running', fetchData.user, fetchData.key); const loadFilesFn = async () => {
this.setLoaded();
return {
code: 500,
message: 'fetchData status is not running',
};
}
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) {
console.error('root files length is zero', user, app); console.error('root files length is zero', user, app);
this.setLoaded(); this.setLoaded('running', 'root files length is zero');
return { code: 404 }; const mockPath = path.join(fileStore, user, app, 'index.html');
value.data.files = [
{
name: 'index.html', // 映射
path: mockPath.replace(fileStore, ''), // 实际
},
];
if (!checkFileExistsSync(path.join(fileStore, user, app))) {
fs.mkdirSync(path.join(fileStore, user, app), { recursive: true });
}
// 自己创建一个index.html
fs.writeFileSync(path.join(fileStore, user, app, 'index.html'), 'not has any app info', {
encoding: 'utf-8',
});
} }
let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html'); let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html');
if (!valueIndexHtml) { if (!valueIndexHtml) {
@ -169,9 +191,8 @@ export class UserApp {
} }
} }
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); // 24小时 await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 7天
const files = value.data.files; const files = value.data.files;
// await redis.hset(key, 'files', JSON.stringify(files));
const data = {}; const data = {};
// 将文件名和路径添加到 `data` 对象中 // 将文件名和路径添加到 `data` 对象中
@ -179,9 +200,18 @@ export class UserApp {
data[file.name] = file.path; data[file.name] = file.path;
}); });
await redis.hset('user:app:set:' + app + ':' + user, data); await redis.hset('user:app:set:' + app + ':' + user, data);
this.setLoaded(); this.setLoaded('running', 'loaded');
};
return { code: 200, data: valueIndexHtml.path }; try {
loadFilesFn();
} catch (e) {
console.error('loadFilesFn error', e);
this.setLoaded('error', 'loadFilesFn error');
}
return {
code: 20000,
data: 'loading',
};
} }
async getAllCacheData() { async getAllCacheData() {
const app = this.app; const app = this.app;
@ -201,6 +231,7 @@ export class UserApp {
await redis.del(key); await redis.del(key);
await redis.del('user:app:exist:' + app + ':' + user); await redis.del('user:app:exist:' + app + ':' + user);
await redis.del('user:app:set:' + app + ':' + user); await redis.del('user:app:set:' + app + ':' + user);
await redis.del('user:app:status:' + app + ':' + user);
// 删除所有文件 // 删除所有文件
deleteUserAppFiles(user, app); deleteUserAppFiles(user, app);
} }

View File

@ -0,0 +1,42 @@
/**
*
* fetch('/api/proxy/refresh?user=user&app=app'), 200
* @param user
* @param app
* @returns
*/
export const createRefreshHtml = (user: string, app: string) => {
return `
<!doctype html>
<html lang="zh-CN" >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App: ${user}/${app}</title>
</head>
<body>
<h1>App: ${user}/${app}</h1>
<p>Loading...</p>
<p> <a href="javascript:void(0)" onclick="window.location.reload()"></a></p>
<p>loadCount: <span id="loadCount">0</span></p>
<script type="module">
let count = 0;
const refresh = () => {
const origin = window.location.origin;
const loadCount = document.getElementById('loadCount');
count++;
loadCount.innerHTML = count.toString();
fetch(origin + '/api/proxy?user=${user}&app=${app}&path=app&key=status').then((res) => {
if (res.status === 200) {
window.location.reload();
} else {
setTimeout(refresh, 3000);
}
});
};
setTimeout(refresh, 2000);
</script>
</body>
</html>
`;
};

BIN
src/module/html/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -5,17 +5,16 @@ import { config, fileStore } from '../module/config.ts';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { getContentType } from './get-content-type.ts'; import { getContentType } from './get-content-type.ts';
import { sleep } from '@/utils/sleep.ts'; import { createRefreshHtml } from './html/create-refresh-html.ts';
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' }; const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me'; const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
const allowedOrigins = config?.proxy?.allowOrigin || []; const allowedOrigins = config?.proxy?.allowOrigin || [];
const noProxyUrl = ['/', '/favicon.ico']; const noProxyUrl = ['/', '/favicon.ico'];
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => { export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
if (req.url === '/favicon.ico') { if (req.url === '/favicon.ico') {
res.writeHead(200, { 'Content-Type': 'text/html' }); res.writeHead(200, { 'Content-Type': 'image/x-icon' });
res.write('proxy no favicon.ico\n'); res.write('proxy no favicon.ico\n');
return; return;
} }
@ -144,33 +143,24 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
let isExist = await userApp.getExist(); let isExist = await userApp.getExist();
if (!isExist) { if (!isExist) {
try { try {
const { code, loading, data } = await userApp.setCacheData(); const { code, loading } = await userApp.setCacheData();
if (loading) { if (loading || code === 20000) {
res.writeHead(200, { 'Content-Type': 'text/html' }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('Loading App\n');
res.end(); res.end(createRefreshHtml(user, app));
return; return;
} }
if (code !== 200) { res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.writeHead(404, { 'Content-Type': 'text/html' });
res.write('Not Found App\n'); res.write('Not Found App\n');
res.end(); res.end();
// 不存在就一定先返回loading状态。
return; return;
}
await sleep(1000);
isExist = data; // 设置缓存后再次获取
if (!isExist) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.write('Not Found App Index Page\n');
res.end();
return;
}
} catch (error) { } catch (error) {
console.error('setCacheData error', error); console.error('setCacheData error', error);
res.writeHead(500, { 'Content-Type': 'text/html' }); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('Server Error\n'); res.write('Server Error\n');
res.end(); res.end();
userApp.setLoaded(); userApp.setLoaded('error', 'setCacheData error');
return; return;
} }
} }
@ -189,7 +179,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
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' }); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('File expired, Not Found\n'); res.write('File expired, Not Found\n');
res.end(); res.end();
await userApp.clearCacheData(); await userApp.clearCacheData();
@ -232,7 +222,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
}); });
if (!userApp.fileCheck(filePath)) { if (!userApp.fileCheck(filePath)) {
console.error('File expired', filePath); console.error('File expired', filePath);
res.writeHead(500, { 'Content-Type': 'text/html' }); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('File expired\n'); res.write('File expired\n');
res.end(); res.end();
await userApp.clearCacheData(); await userApp.clearCacheData();

View File

@ -0,0 +1,30 @@
import { redis } from './redis.ts';
export type AppLoadStatus = {
status: 'running' | 'loading' | 'error' | 'not-exist';
message: string;
};
export const getAppLoadStatus = async (user: string, app: string): Promise<AppLoadStatus> => {
const key = 'user:app:status:' + app + ':' + user;
const value = await redis.get(key);
if (!value) {
return {
status: 'not-exist',
message: 'not-exist',
}; // 没有加载过
}
try {
return JSON.parse(value);
} catch (err) {
return {
status: 'error',
message: 'error',
};
}
};
export const setAppLoadStatus = async (user: string, app: string, status: AppLoadStatus) => {
const key = 'user:app:status:' + app + ':' + user;
const value = JSON.stringify(status);
await redis.set(key, value);
};

View File

@ -78,7 +78,7 @@ app
key: 'get', key: 'get',
}) })
.define(async (ctx) => { .define(async (ctx) => {
const { user, app } = ctx.query.data || {}; const { user, app } = ctx.query;
if (!user || !app) { if (!user || !app) {
if (!user) { if (!user) {
ctx.throw('user is required'); ctx.throw('user is required');
@ -95,3 +95,16 @@ app
ctx.body = cache; ctx.body = cache;
}) })
.addTo(app); .addTo(app);
app
.route({
path: 'app',
key: 'status',
})
.define(async (ctx) => {
const { user, app } = ctx.query;
const userApp = new UserApp({ user, app });
const status = await userApp.getLoaded();
ctx.body = status;
})
.addTo(app);

7
src/scripts/clear.ts Normal file
View File

@ -0,0 +1,7 @@
import { UserApp, clearAllUserApp } from '../module/get-user-app.ts';
const main = async () => {
await clearAllUserApp();
};
main();