284 lines
8.4 KiB
TypeScript
284 lines
8.4 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 } = 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();
|
|
}
|
|
});
|