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, api } = useConfig<{ resources: string; api: { host: string; testHost: 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', }, ], }, }; 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; } static async getDomainApp(domain: string) { const key = 'domain:' + domain; const value = await redis.get(key); if (value) { const [_user, _app] = value.split(':'); return { user: _user, app: _app, }; } // 获取域名对应的用户和应用 const isDev = process.env.NODE_ENV === 'development'; const fetchTestUrl = 'http://' + api.testHost + '/api/router'; const fetchUrl = 'http://' + api.host + '/api/router'; const fetchRes = await fetch(isDev ? fetchTestUrl : fetchUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: 'app', key: 'getDomainApp', data: { domain, }, }), }).then((res) => res.json()); if (fetchRes?.code !== 200) { console.log('fetchRes is error', fetchRes); return null; } const fetchData = fetchRes.data; const data = { user: fetchData.user, app: fetchData.key, }; redis.set(key, data.user + ':' + data.app, 'EX', 60 * 60 * 24 * 7); // 24小时 return data; } async setCacheData() { const app = this.app; const user = this.user; const key = 'user:app:' + app + ':' + user; const isDev = process.env.NODE_ENV === 'development'; const fetchTestUrl = 'http://' + api.testHost + '/api/router'; const fetchUrl = 'http://' + api.host + '/api/router'; const fetchRes = await fetch(isDev ? fetchTestUrl : fetchUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: 'app', key: 'getApp', data: { user, key: app, }, }), }).then((res) => res.json()); if (fetchRes?.code !== 200) { console.log('fetchRes is error', fetchRes); return false; } const fetchData = fetchRes.data; if (!fetchData.type) { // console.error('fetchData type is error', fetchData); // return false; fetchData.type = 'oss'; } const value = await downloadUserAppFiles(user, app, fetchData); if (value.data.files.length === 0) { console.error('root files length is zero', user, app); return false; } 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); // 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 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(); } });