page-proxy/src/module/get-user-app.ts
2024-10-07 22:52:51 +08:00

330 lines
10 KiB
TypeScript

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();
}
});