feat: 初始化proxy代理请求
This commit is contained in:
		
							
								
								
									
										18
									
								
								src/module/get-content-type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/module/get-content-type.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import path from 'path';
 | 
			
		||||
// 获取文件的 content-type
 | 
			
		||||
export const getContentType = (filePath: string) => {
 | 
			
		||||
  const extname = path.extname(filePath);
 | 
			
		||||
  const contentType = {
 | 
			
		||||
    '.html': 'text/html',
 | 
			
		||||
    '.js': 'text/javascript',
 | 
			
		||||
    '.css': 'text/css',
 | 
			
		||||
    '.json': 'application/json',
 | 
			
		||||
    '.png': 'image/png',
 | 
			
		||||
    '.jpg': 'image/jpg',
 | 
			
		||||
    '.gif': 'image/gif',
 | 
			
		||||
    '.svg': 'image/svg+xml',
 | 
			
		||||
    '.wav': 'audio/wav',
 | 
			
		||||
    '.mp4': 'video/mp4',
 | 
			
		||||
  };
 | 
			
		||||
  return contentType[extname] || 'application/octet-stream';
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										283
									
								
								src/module/get-user-app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								src/module/get-user-app.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,283 @@
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { redis, subscriber } from './redis/redis.ts';
 | 
			
		||||
import { useFileStore } from '@abearxiong/use-file-store';
 | 
			
		||||
import { useConfig } from '@abearxiong/use-config';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import { nanoid } from 'nanoid';
 | 
			
		||||
import { pipeline } from 'stream';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
const pipelineAsync = promisify(pipeline);
 | 
			
		||||
 | 
			
		||||
const { resources } = useConfig<{ resources: string }>();
 | 
			
		||||
const fileStore = useFileStore('upload');
 | 
			
		||||
 | 
			
		||||
const demoData = {
 | 
			
		||||
  user: 'root',
 | 
			
		||||
  key: 'codeflow',
 | 
			
		||||
  appType: 'web-single', //
 | 
			
		||||
  version: '1.0.0',
 | 
			
		||||
  domain: null,
 | 
			
		||||
  type: 'local',
 | 
			
		||||
  data: {
 | 
			
		||||
    files: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'index.html',
 | 
			
		||||
        path: 'codeflow/index.html',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'assets/index-14y4J8dP.js',
 | 
			
		||||
        path: 'codeflow/assets/index-14y4J8dP.js',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'assets/index-C-libw4a.css',
 | 
			
		||||
        path: 'codeflow/assets/index-C-libw4a.css',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const demoData2 = {
 | 
			
		||||
  user: 'root',
 | 
			
		||||
  key: 'codeflow',
 | 
			
		||||
  appType: 'web-single', //
 | 
			
		||||
  version: '0.0.1',
 | 
			
		||||
  domain: null,
 | 
			
		||||
  type: 'oss', // 是否使用oss
 | 
			
		||||
  data: {
 | 
			
		||||
    files: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'index.html',
 | 
			
		||||
        path: 'root/codeflow/0.0.1/index.html',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'assets/index-14y4J8dP.js',
 | 
			
		||||
        path: 'root/codeflow/0.0.1/assets/index-14y4J8dP.js',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'assets/index-C-libw4a.css',
 | 
			
		||||
        path: 'root/codeflow/0.0.1/assets/index-C-libw4a.css',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
type UserAppOptions = {
 | 
			
		||||
  user: string;
 | 
			
		||||
  app: string;
 | 
			
		||||
};
 | 
			
		||||
export class UserApp {
 | 
			
		||||
  user: string;
 | 
			
		||||
  app: string;
 | 
			
		||||
  constructor(options: UserAppOptions) {
 | 
			
		||||
    this.user = options.user;
 | 
			
		||||
    this.app = options.app;
 | 
			
		||||
  }
 | 
			
		||||
  async getExist() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:exist:' + app + ':' + user;
 | 
			
		||||
    const value = await redis.get(key);
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  async getCache() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:' + app + ':' + user;
 | 
			
		||||
    const value = await redis.get(key);
 | 
			
		||||
    if (!value) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async getFile(appFileUrl: string) {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:set:' + app + ':' + user;
 | 
			
		||||
    const value = await redis.hget(key, appFileUrl);
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  async setCacheData() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:' + app + ':' + user;
 | 
			
		||||
    // 如果demoData 不存在则返回
 | 
			
		||||
    if (!demoData2) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    const value = await downloadUserAppFiles(user, app, demoData2);
 | 
			
		||||
    const valueIndexHtml = value.data.files.find((file) => file.name === 'index.html');
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  async getAllCacheData() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:' + app + ':' + user;
 | 
			
		||||
    const value = await redis.get(key);
 | 
			
		||||
    console.log('getAllCacheData', JSON.parse(value));
 | 
			
		||||
    const exist = await redis.get('user:app:exist:' + app + ':' + user);
 | 
			
		||||
    console.log('getAllCacheData:exist', exist);
 | 
			
		||||
    const files = await redis.hgetall('user:app:set:' + app + ':' + user);
 | 
			
		||||
    console.log('getAllCacheData:files', files);
 | 
			
		||||
  }
 | 
			
		||||
  async clearCacheData() {
 | 
			
		||||
    const app = this.app;
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:' + app + ':' + user;
 | 
			
		||||
    await redis.del(key);
 | 
			
		||||
    await redis.del('user:app:exist:' + app + ':' + user);
 | 
			
		||||
    await redis.del('user:app:set:' + app + ':' + user);
 | 
			
		||||
    console.log('clear user data', key);
 | 
			
		||||
    // 删除所有文件
 | 
			
		||||
    deleteUserAppFiles(user, app);
 | 
			
		||||
  }
 | 
			
		||||
  async getData() {
 | 
			
		||||
    return demoData;
 | 
			
		||||
  }
 | 
			
		||||
  async close() {
 | 
			
		||||
    // 关闭连接
 | 
			
		||||
    await redis.quit();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
export const downloadUserAppFiles = async (user: string, app: string, data: typeof demoData) => {
 | 
			
		||||
  const {
 | 
			
		||||
    data: { files, ...dataRest },
 | 
			
		||||
    ...rest
 | 
			
		||||
  } = data;
 | 
			
		||||
  const uploadFiles = path.join(fileStore, user, app);
 | 
			
		||||
  if (!checkFileExistsSync(uploadFiles)) {
 | 
			
		||||
    fs.mkdirSync(uploadFiles, { recursive: true });
 | 
			
		||||
  }
 | 
			
		||||
  const newFiles = [];
 | 
			
		||||
  if (data.type === 'local') {
 | 
			
		||||
    // local copy file
 | 
			
		||||
    for (let i = 0; i < files.length; i++) {
 | 
			
		||||
      const file = files[i];
 | 
			
		||||
      const copyFile = path.join(fileStore, file.path);
 | 
			
		||||
      const destFile = path.join(uploadFiles, file.name);
 | 
			
		||||
      const destDir = path.dirname(destFile); // 获取目标文件所在的目录路径
 | 
			
		||||
      // 检查目录是否存在,如果不存在则创建
 | 
			
		||||
      if (!checkFileExistsSync(destDir)) {
 | 
			
		||||
        fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
 | 
			
		||||
      }
 | 
			
		||||
      fs.copyFileSync(copyFile, destFile);
 | 
			
		||||
      // const etag = await setEtag(fs.readFileSync(destFile, 'utf-8'));
 | 
			
		||||
      const etag = nanoid();
 | 
			
		||||
      newFiles.push({
 | 
			
		||||
        name: file.name,
 | 
			
		||||
        path: destFile.replace(fileStore, '') + '||' + etag,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (data.type === 'oss') {
 | 
			
		||||
    const serverPath = 'https://' + resources + '/';
 | 
			
		||||
    // 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 (!checkFileExistsSync(destDir)) {
 | 
			
		||||
        fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
 | 
			
		||||
      }
 | 
			
		||||
      // 下载文件到 destFile
 | 
			
		||||
      await downloadFile(serverPath + file.path, destFile);
 | 
			
		||||
      const etag = nanoid();
 | 
			
		||||
      newFiles.push({
 | 
			
		||||
        name: file.name,
 | 
			
		||||
        path: destFile.replace(fileStore, '') + '||' + etag,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...rest,
 | 
			
		||||
    data: {
 | 
			
		||||
      ...dataRest,
 | 
			
		||||
      files: newFiles,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
export const checkFileExistsSync = (filePath: string) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 使用 F_OK 检查文件或目录是否存在
 | 
			
		||||
    fs.accessSync(filePath, fs.constants.F_OK);
 | 
			
		||||
    return true;
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
export const deleteUserAppFiles = async (user: string, app: string) => {
 | 
			
		||||
  const uploadFiles = path.join(fileStore, user, app);
 | 
			
		||||
  try {
 | 
			
		||||
    fs.rmSync(uploadFiles, { recursive: true });
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error('deleteUserAppFiles', err);
 | 
			
		||||
  }
 | 
			
		||||
  // console.log('deleteUserAppFiles', res);
 | 
			
		||||
};
 | 
			
		||||
async function downloadFile(fileUrl: string, destFile: string) {
 | 
			
		||||
  const res = await fetch(fileUrl);
 | 
			
		||||
 | 
			
		||||
  if (!res.ok) {
 | 
			
		||||
    throw new Error(`Failed to fetch ${fileUrl}: ${res.statusText}`);
 | 
			
		||||
  }
 | 
			
		||||
  console.log('destFile', destFile);
 | 
			
		||||
  const destStream = fs.createWriteStream(destFile);
 | 
			
		||||
 | 
			
		||||
  // 使用 `pipeline` 将 `res.body` 中的数据传递给 `destStream`
 | 
			
		||||
  await pipelineAsync(res.body, destStream);
 | 
			
		||||
 | 
			
		||||
  console.log(`File downloaded to ${destFile}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const clearAllUserApp = async () => {
 | 
			
		||||
  // redis 删除 所有的 user:app:*
 | 
			
		||||
  const keys = await redis.keys('user:app:*');
 | 
			
		||||
  console.log('clearAllUserApp', keys);
 | 
			
		||||
  if (keys.length > 0) {
 | 
			
		||||
    const pipeline = redis.pipeline();
 | 
			
		||||
    keys.forEach((key) => pipeline.del(key)); // 将每个键的删除操作添加到 pipeline 中
 | 
			
		||||
    await pipeline.exec(); // 执行 pipeline 中的所有命令
 | 
			
		||||
    console.log('All keys deleted successfully using pipeline');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
export const setEtag = async (fileContent: string) => {
 | 
			
		||||
  const eTag = crypto.createHash('md5').update(fileContent).digest('hex');
 | 
			
		||||
  return eTag;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// redis 监听 user:app:exist:*的过期
 | 
			
		||||
subscriber.on('ready', () => {
 | 
			
		||||
  console.log('Subscriber is ready and connected.');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 订阅 Redis 频道
 | 
			
		||||
subscriber.subscribe('__keyevent@0__:expired', (err, count) => {
 | 
			
		||||
  if (err) {
 | 
			
		||||
    console.error('Failed to subscribe: ', err);
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log(`Subscribed to ${count} channel(s). Waiting for expired events...`);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监听消息事件
 | 
			
		||||
subscriber.on('message', (channel, message) => {
 | 
			
		||||
  // 检查是否匹配 user:app:exist:* 模式
 | 
			
		||||
  if (message.startsWith('user:app:exist:')) {
 | 
			
		||||
    const [_user, _app, _exist, app, user] = message.split(':');
 | 
			
		||||
    // 在这里执行你的逻辑,例如清理缓存或通知用户
 | 
			
		||||
    console.log('User app exist key expired:', app, user);
 | 
			
		||||
    const userApp = new UserApp({ user, app });
 | 
			
		||||
    userApp.clearCacheData();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										167
									
								
								src/module/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/module/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
import { getDNS, isLocalhost } from '@/utils/dns.ts';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
import { UserApp } from './get-user-app.ts';
 | 
			
		||||
import { useFileStore } from '@abearxiong/use-file-store';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { useConfig } from '@abearxiong/use-config';
 | 
			
		||||
import { redis } from './redis/redis.ts';
 | 
			
		||||
import { getContentType } from './get-content-type.ts';
 | 
			
		||||
const { api, domain } = useConfig<{
 | 
			
		||||
  api: {
 | 
			
		||||
    host: string;
 | 
			
		||||
  };
 | 
			
		||||
  domain: string;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const fileStore = useFileStore('upload');
 | 
			
		||||
console.log('filePath', fileStore);
 | 
			
		||||
const noProxyUrl = ['/', '/favicon.ico'];
 | 
			
		||||
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
 | 
			
		||||
  const dns = getDNS(req);
 | 
			
		||||
 | 
			
		||||
  let user, app;
 | 
			
		||||
  let domainApp = false;
 | 
			
		||||
  if (isLocalhost(dns.hostName)) {
 | 
			
		||||
    // 本地开发环境 测试
 | 
			
		||||
    // user = 'root';
 | 
			
		||||
    // app = 'codeflow';
 | 
			
		||||
    // domainApp = true;
 | 
			
		||||
  } else {
 | 
			
		||||
    // 生产环境
 | 
			
		||||
    // 验证域名
 | 
			
		||||
    if (dns.hostName !== domain) {
 | 
			
		||||
      // redis获取域名对应的用户和应用
 | 
			
		||||
      domainApp = true;
 | 
			
		||||
      const key = 'domain:' + dns.hostName;
 | 
			
		||||
      const value = await redis.get(key);
 | 
			
		||||
      if (!value) {
 | 
			
		||||
        res.writeHead(404, { 'Content-Type': 'text/plain' });
 | 
			
		||||
        res.write('Invalid domain\n');
 | 
			
		||||
        return res.end();
 | 
			
		||||
      }
 | 
			
		||||
      const [_user, _app] = value.split(':');
 | 
			
		||||
      if (!_user || !_app) {
 | 
			
		||||
        res.writeHead(404, { 'Content-Type': 'text/plain' });
 | 
			
		||||
        res.write('Invalid domain, Config error\n');
 | 
			
		||||
        return res.end();
 | 
			
		||||
      }
 | 
			
		||||
      user = _user;
 | 
			
		||||
      app = _app;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const url = req.url;
 | 
			
		||||
  if (!domainApp && noProxyUrl.includes(req.url)) {
 | 
			
		||||
    res.write('No proxy for this URL\n');
 | 
			
		||||
    return res.end();
 | 
			
		||||
  }
 | 
			
		||||
  if (!domainApp) {
 | 
			
		||||
    // 原始url地址
 | 
			
		||||
    const urls = url.split('/');
 | 
			
		||||
    if (urls.length < 3) {
 | 
			
		||||
      console.log('urls errpr', urls);
 | 
			
		||||
      res.writeHead(404, { 'Content-Type': 'text/html' });
 | 
			
		||||
      res.write('Invalid Proxy URL\n');
 | 
			
		||||
      return res.end();
 | 
			
		||||
    }
 | 
			
		||||
    const [_, _user, _app] = urls;
 | 
			
		||||
    if (!_user || !_app) {
 | 
			
		||||
      res.write('Invalid URL\n');
 | 
			
		||||
      return res.end();
 | 
			
		||||
    }
 | 
			
		||||
    user = _user;
 | 
			
		||||
    app = _app;
 | 
			
		||||
  }
 | 
			
		||||
  const [_, _api] = req.url.split('/');
 | 
			
		||||
  if (_api === 'api') {
 | 
			
		||||
    // 代理到 http://codeflow.xiongxiao.me/api
 | 
			
		||||
    // 设置代理请求的目标 URL 和请求头
 | 
			
		||||
    const options = {
 | 
			
		||||
      host: api.host,
 | 
			
		||||
      path: req.url,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': req.headers['content-type'],
 | 
			
		||||
        Authroization: req.headers?.['authorization'] || '',
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    // 创建代理请求
 | 
			
		||||
    const proxyReq = http.request(options, (proxyRes) => {
 | 
			
		||||
      // 将代理服务器的响应头和状态码返回给客户端
 | 
			
		||||
      res.writeHead(proxyRes.statusCode, proxyRes.headers);
 | 
			
		||||
      // 将代理响应流写入客户端响应
 | 
			
		||||
      proxyRes.pipe(res, { end: true });
 | 
			
		||||
    });
 | 
			
		||||
    // 处理代理请求的错误事件
 | 
			
		||||
    proxyReq.on('error', (err) => {
 | 
			
		||||
      console.error(`Proxy request error: ${err.message}`);
 | 
			
		||||
      res.writeHead(500, { 'Content-Type': 'text/plain' });
 | 
			
		||||
      res.write(`Proxy request error: ${err.message}`);
 | 
			
		||||
    });
 | 
			
		||||
    // 处理 POST 请求的请求体(传递数据到目标服务器)
 | 
			
		||||
    req.pipe(proxyReq, { end: true });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const userApp = new UserApp({ user, app });
 | 
			
		||||
  let isExist = await userApp.getExist();
 | 
			
		||||
  if (!isExist) {
 | 
			
		||||
    try {
 | 
			
		||||
      const hasApp = await userApp.setCacheData();
 | 
			
		||||
      if (!hasApp) {
 | 
			
		||||
        res.writeHead(404, { 'Content-Type': 'text/html' });
 | 
			
		||||
        res.write('Not Found App\n');
 | 
			
		||||
        res.end();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('setCacheData error', error);
 | 
			
		||||
      res.writeHead(500, { 'Content-Type': 'text/html' });
 | 
			
		||||
      res.write('Server Error\n');
 | 
			
		||||
      res.end();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isExist = await userApp.getExist();
 | 
			
		||||
    if (!isExist) {
 | 
			
		||||
      res.writeHead(404, { 'Content-Type': 'text/html' });
 | 
			
		||||
      res.write('Not Found App Index Page\n');
 | 
			
		||||
      res.end();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const indexFile = isExist;
 | 
			
		||||
  let appFileUrl: string;
 | 
			
		||||
  if (domainApp) {
 | 
			
		||||
    appFileUrl = (url + '').replace(`/`, '');
 | 
			
		||||
  } else {
 | 
			
		||||
    appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
 | 
			
		||||
  }
 | 
			
		||||
  const appFile = await userApp.getFile(appFileUrl);
 | 
			
		||||
  if (!appFile) {
 | 
			
		||||
    const [indexFilePath, etag] = indexFile.split('||');
 | 
			
		||||
    // 不存在的文件,返回indexFile的文件
 | 
			
		||||
    res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
 | 
			
		||||
    const filePath = path.join(fileStore, indexFilePath);
 | 
			
		||||
    const readStream = fs.createReadStream(filePath);
 | 
			
		||||
    readStream.pipe(res);
 | 
			
		||||
    return;
 | 
			
		||||
  } else {
 | 
			
		||||
    const [appFilePath, eTag] = appFile.split('||');
 | 
			
		||||
    // 检查 If-None-Match 头判断缓存是否有效
 | 
			
		||||
    if (req.headers['if-none-match'] === eTag) {
 | 
			
		||||
      res.statusCode = 304; // 内容未修改
 | 
			
		||||
      res.end();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const filePath = path.join(fileStore, appFilePath);
 | 
			
		||||
    let contentType = getContentType(filePath);
 | 
			
		||||
    res.writeHead(200, {
 | 
			
		||||
      'Content-Type': contentType,
 | 
			
		||||
      'Cache-Control': 'public, max-age=3600', // 设置缓存时间为 1 小时
 | 
			
		||||
      ETag: eTag,
 | 
			
		||||
    });
 | 
			
		||||
    const readStream = fs.createReadStream(filePath);
 | 
			
		||||
    readStream.pipe(res);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										28
									
								
								src/module/redis/access-time.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/module/redis/access-time.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import { redis } from './redis.ts';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 更新键的访问时间
 | 
			
		||||
 * @param key
 | 
			
		||||
 */
 | 
			
		||||
export const accessKeyWithTimestamp = async (key: string) => {
 | 
			
		||||
  const value = await redis.get(key);
 | 
			
		||||
  if (value !== null) {
 | 
			
		||||
    // 记录上一次访问时间(使用当前 Unix 时间戳)
 | 
			
		||||
    await redis.hset('key_last_access_time', key, Math.floor(Date.now() / 1000));
 | 
			
		||||
  }
 | 
			
		||||
  return value;
 | 
			
		||||
};
 | 
			
		||||
/**
 | 
			
		||||
 * 更新键的访问计数
 | 
			
		||||
 * @param key
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export const accessKeyAndCount = async (key: string) => {
 | 
			
		||||
  const value = await redis.get(key);
 | 
			
		||||
  if (value !== null) {
 | 
			
		||||
    // 增加访问计数
 | 
			
		||||
    await redis.incr(`access_count:${key}`);
 | 
			
		||||
  }
 | 
			
		||||
  return value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								src/module/redis/redis.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/module/redis/redis.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
import { Redis } from 'ioredis';
 | 
			
		||||
import { useConfig } from '@abearxiong/use-config';
 | 
			
		||||
 | 
			
		||||
const config = useConfig<{
 | 
			
		||||
  redis: ConstructorParameters<typeof Redis>;
 | 
			
		||||
}>();
 | 
			
		||||
// 配置 Redis 连接
 | 
			
		||||
export const redis = new Redis({
 | 
			
		||||
  host: 'localhost', // Redis 服务器的主机名或 IP 地址
 | 
			
		||||
  port: 6379, // Redis 服务器的端口号
 | 
			
		||||
  // password: 'your_password', // Redis 的密码 (如果有)
 | 
			
		||||
  db: 0, // 要使用的 Redis 数据库索引 (0-15)
 | 
			
		||||
  keyPrefix: '', // key 前缀
 | 
			
		||||
  retryStrategy(times) {
 | 
			
		||||
    // 连接重试策略
 | 
			
		||||
    return Math.min(times * 50, 2000); // 每次重试时延迟增加
 | 
			
		||||
  },
 | 
			
		||||
  maxRetriesPerRequest: null, // 允许请求重试的次数 (如果需要无限次重试)
 | 
			
		||||
  ...config.redis,
 | 
			
		||||
});
 | 
			
		||||
export const subscriber = redis.duplicate(); // 创建一个订阅者连接
 | 
			
		||||
 | 
			
		||||
async function ensureKeyspaceNotifications() {
 | 
			
		||||
  try {
 | 
			
		||||
    // 获取当前的 `notify-keyspace-events` 配置
 | 
			
		||||
    const currentConfig = (await redis.config('GET', 'notify-keyspace-events')) as string[];
 | 
			
		||||
 | 
			
		||||
    // 检查返回的数组长度是否大于1,表示获取成功
 | 
			
		||||
    if (currentConfig && currentConfig.length > 1) {
 | 
			
		||||
      const currentSetting = currentConfig[1]; // 值在数组的第二个元素
 | 
			
		||||
      // 检查当前配置是否包含 "Ex"
 | 
			
		||||
      if (!currentSetting.includes('E') || !currentSetting.includes('x')) {
 | 
			
		||||
        console.log('Keyspace notifications are not fully enabled. Setting correct value...');
 | 
			
		||||
        await redis.config('SET', 'notify-keyspace-events', 'Ex');
 | 
			
		||||
        console.log('Keyspace notifications enabled with setting "Ex".');
 | 
			
		||||
      } else {
 | 
			
		||||
        // console.log('Keyspace notifications are already correctly configured.');
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error('Failed to get the current notify-keyspace-events setting.');
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error('Error while configuring Redis keyspace notifications:', err);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 确保键空间通知被正确设置
 | 
			
		||||
ensureKeyspaceNotifications().catch(console.error);
 | 
			
		||||
		Reference in New Issue
	
	Block a user