add status for redis
This commit is contained in:
		@@ -7,17 +7,19 @@ import { nanoid } from 'nanoid';
 | 
			
		||||
import { pipeline } from 'stream';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts';
 | 
			
		||||
import { getAppLoadStatus, setAppLoadStatus, AppLoadStatus } from './redis/get-app-status.ts';
 | 
			
		||||
 | 
			
		||||
const pipelineAsync = promisify(pipeline);
 | 
			
		||||
 | 
			
		||||
const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' };
 | 
			
		||||
const status: { [key: string]: boolean } = {};
 | 
			
		||||
 | 
			
		||||
const demoData = {
 | 
			
		||||
  user: 'root',
 | 
			
		||||
  key: 'codeflow',
 | 
			
		||||
  appType: 'web-single', //
 | 
			
		||||
  version: '1.0.0',
 | 
			
		||||
  domain: null,
 | 
			
		||||
  type: 'local',
 | 
			
		||||
  type: 'local', // local, oss, 默认是oss
 | 
			
		||||
  data: {
 | 
			
		||||
    files: [
 | 
			
		||||
      {
 | 
			
		||||
@@ -51,6 +53,10 @@ export class UserApp {
 | 
			
		||||
      this.isTest = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 是否已经加载到本地了
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async getExist() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
@@ -106,82 +112,106 @@ export class UserApp {
 | 
			
		||||
      user: fetchData.user,
 | 
			
		||||
      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;
 | 
			
		||||
  }
 | 
			
		||||
  async setLoaded() {
 | 
			
		||||
  /**
 | 
			
		||||
   * 加载结束
 | 
			
		||||
   * @param msg
 | 
			
		||||
   */
 | 
			
		||||
  async setLoaded(status: 'running' | 'error' | 'loading', msg?: string) {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:' + app + ':' + user;
 | 
			
		||||
    if (status[key]) {
 | 
			
		||||
      status[key] = false;
 | 
			
		||||
    }
 | 
			
		||||
    await setAppLoadStatus(user, app, {
 | 
			
		||||
      status,
 | 
			
		||||
      message: msg,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  async getLoaded() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:' + app + ':' + user;
 | 
			
		||||
    return status[key];
 | 
			
		||||
    const value = await getAppLoadStatus(user, app);
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 设置缓存数据,当出问题了,就重新加载。
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async setCacheData() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const isTest = this.isTest;
 | 
			
		||||
    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 {
 | 
			
		||||
        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;
 | 
			
		||||
    if (!fetchData.type) {
 | 
			
		||||
      // console.error('fetchData type is error', fetchData);
 | 
			
		||||
      // return false;
 | 
			
		||||
      fetchData.type = 'oss';
 | 
			
		||||
    }
 | 
			
		||||
    if (fetchData.status !== 'running') {
 | 
			
		||||
      console.error('fetchData status is not running', fetchData.user, fetchData.key);
 | 
			
		||||
      this.setLoaded();
 | 
			
		||||
      return {
 | 
			
		||||
        code: 500,
 | 
			
		||||
        message: 'fetchData status is not running',
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    const value = await downloadUserAppFiles(user, app, fetchData);
 | 
			
		||||
    if (value.data.files.length === 0) {
 | 
			
		||||
      console.error('root files length is zero', user, app);
 | 
			
		||||
      this.setLoaded();
 | 
			
		||||
      return { code: 404 };
 | 
			
		||||
    }
 | 
			
		||||
    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];
 | 
			
		||||
    this.setLoaded('loading', 'loading');
 | 
			
		||||
    const loadFilesFn = async () => {
 | 
			
		||||
      const value = await downloadUserAppFiles(user, app, fetchData);
 | 
			
		||||
      if (value.data.files.length === 0) {
 | 
			
		||||
        console.error('root files length is zero', user, app);
 | 
			
		||||
        this.setLoaded('running', 'root files length is zero');
 | 
			
		||||
        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');
 | 
			
		||||
      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天
 | 
			
		||||
      const files = value.data.files;
 | 
			
		||||
      const data = {};
 | 
			
		||||
 | 
			
		||||
      // 将文件名和路径添加到 `data` 对象中
 | 
			
		||||
      files.forEach((file) => {
 | 
			
		||||
        data[file.name] = file.path;
 | 
			
		||||
      });
 | 
			
		||||
      await redis.hset('user:app:set:' + app + ':' + user, data);
 | 
			
		||||
      this.setLoaded('running', 'loaded');
 | 
			
		||||
    };
 | 
			
		||||
    try {
 | 
			
		||||
      loadFilesFn();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error('loadFilesFn error', e);
 | 
			
		||||
      this.setLoaded('error', 'loadFilesFn error');
 | 
			
		||||
    }
 | 
			
		||||
    await redis.set(key, JSON.stringify(value));
 | 
			
		||||
    await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 24小时
 | 
			
		||||
    const files = value.data.files;
 | 
			
		||||
    // await redis.hset(key, 'files', JSON.stringify(files));
 | 
			
		||||
    const data = {};
 | 
			
		||||
 | 
			
		||||
    // 将文件名和路径添加到 `data` 对象中
 | 
			
		||||
    files.forEach((file) => {
 | 
			
		||||
      data[file.name] = file.path;
 | 
			
		||||
    });
 | 
			
		||||
    await redis.hset('user:app:set:' + app + ':' + user, data);
 | 
			
		||||
    this.setLoaded();
 | 
			
		||||
 | 
			
		||||
    return { code: 200, data: valueIndexHtml.path };
 | 
			
		||||
    return {
 | 
			
		||||
      code: 20000,
 | 
			
		||||
      data: 'loading',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  async getAllCacheData() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
@@ -201,6 +231,7 @@ export class UserApp {
 | 
			
		||||
    await redis.del(key);
 | 
			
		||||
    await redis.del('user:app:exist:' + app + ':' + user);
 | 
			
		||||
    await redis.del('user:app:set:' + app + ':' + user);
 | 
			
		||||
    await redis.del('user:app:status:' + app + ':' + user);
 | 
			
		||||
    // 删除所有文件
 | 
			
		||||
    deleteUserAppFiles(user, app);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								src/module/html/create-refresh-html.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/module/html/create-refresh-html.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/module/html/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
@@ -5,17 +5,16 @@ import { config, fileStore } from '../module/config.ts';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
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 domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
 | 
			
		||||
const allowedOrigins = config?.proxy?.allowOrigin || [];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const noProxyUrl = ['/', '/favicon.ico'];
 | 
			
		||||
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
 | 
			
		||||
  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');
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -144,33 +143,24 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
  let isExist = await userApp.getExist();
 | 
			
		||||
  if (!isExist) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { code, loading, data } = await userApp.setCacheData();
 | 
			
		||||
      if (loading) {
 | 
			
		||||
        res.writeHead(200, { 'Content-Type': 'text/html' });
 | 
			
		||||
        res.write('Loading App\n');
 | 
			
		||||
        res.end();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (code !== 200) {
 | 
			
		||||
        res.writeHead(404, { 'Content-Type': 'text/html' });
 | 
			
		||||
        res.write('Not Found App\n');
 | 
			
		||||
        res.end();
 | 
			
		||||
        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();
 | 
			
		||||
      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;
 | 
			
		||||
      }
 | 
			
		||||
      res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
      res.write('Not Found App\n');
 | 
			
		||||
      res.end();
 | 
			
		||||
      // 不存在就一定先返回loading状态。
 | 
			
		||||
      return;
 | 
			
		||||
    } catch (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.end();
 | 
			
		||||
      userApp.setLoaded();
 | 
			
		||||
      userApp.setLoaded('error', 'setCacheData error');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -189,7 +179,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
    const isHTML = contentType.includes('html');
 | 
			
		||||
    const filePath = path.join(fileStore, indexFilePath);
 | 
			
		||||
    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.end();
 | 
			
		||||
      await userApp.clearCacheData();
 | 
			
		||||
@@ -232,7 +222,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
    });
 | 
			
		||||
    if (!userApp.fileCheck(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.end();
 | 
			
		||||
      await userApp.clearCacheData();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								src/module/redis/get-app-status.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/module/redis/get-app-status.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user