import { useFileStore } from '@abearxiong/use-file-store'; import { PageModel } from '../models/index.ts'; import { ContainerModel } from '@/routes/container/models/index.ts'; import { Op } from 'sequelize'; import { getContainerData } from './get-container.ts'; import path from 'path'; import fs from 'fs'; import { getHTML, getDataJs } from './file-template.ts'; import { minioClient } from '@/app.ts'; import { bucketName } from '@/modules/minio.ts'; import { getContentType } from '@/utils/get-content-type.ts'; import archiver from 'archiver'; import { CustomError } from '@kevisual/router'; import { nanoid } from 'nanoid'; export const cacheFile = useFileStore('cache-file', { needExists: true, }); export const getDeck = async (page: PageModel) => { const { data } = page; const { nodes = [], edges } = data; const containerList = nodes .map((item) => { const { data } = item; return data?.cid; }) .filter((item) => item); const quchong = Array.from(new Set(containerList)); const containers = await ContainerModel.findAll({ where: { id: { [Op.in]: quchong, }, }, }); const pageData = { page, containerList: containers, }; return pageData; }; export const cachePage = async (page: PageModel, opts: { tokenUser: any; key; version }) => { const _result = await getDeck(page); const result = getContainerData(_result); const key = 'data-' + nanoid(6); const html = getHTML({ rootId: page.id, title: page?.publish?.key, dataKey: key }); const dataJs = getDataJs(result); const htmlPath = path.resolve(cacheFile, `${page.id}.html`); const dataJsPath = path.resolve(cacheFile, `${page.id}.js`); fs.writeFileSync(htmlPath, html); fs.writeFileSync(dataJsPath, dataJs); const minioHTML = await uploadMinio({ ...opts, path: `index.html`, filePath: htmlPath }); const minioData = await uploadMinio({ ...opts, path: `${key || 'data'}.js`, filePath: dataJsPath }); return [ { name: 'index.html', path: minioHTML, }, { name: `${key || 'data'}.js`, path: minioData, }, ]; }; export const uploadMinioContainer = async ({ tokenUser, key, version, code, filePath }) => { if ((filePath as string).includes('..')) { throw new CustomError('file path is invalid'); } const minioKeyVersion = `${tokenUser.username}/${key}/${version}`; const minioPath = path.join(minioKeyVersion, filePath); console.log('minioPath', minioPath); // const isHTML = filePath.endsWith('.html'); const name = minioPath.replace(minioKeyVersion + '/', ''); await minioClient.putObject(bucketName, minioPath, code, code.length, { 'Content-Type': getContentType(filePath), 'app-source': 'user-app', 'Cache-Control': 'no-cache', // 缓存一年 }); return { name, path: minioPath, }; }; export const uploadMinio = async ({ tokenUser, key, version, path, filePath }) => { const minioPath = `${tokenUser.username}/${key}/${version}/${path}`; const isHTML = filePath.endsWith('.html'); await minioClient.fPutObject(bucketName, minioPath, filePath, { 'Content-Type': getContentType(filePath), 'app-source': 'user-app', 'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年 }); fs.unlinkSync(filePath); // 删除临时文件 return minioPath; }; export const uploadMinioTemp = async ({ tokenUser, filePath, path }) => { const minioPath = `${tokenUser.username}/temp/${path}`; const isHTML = filePath.endsWith('.html'); await minioClient.fPutObject(bucketName, minioPath, filePath, { 'Content-Type': getContentType(filePath), 'app-source': 'user-app', 'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年 }); fs.unlinkSync(filePath); // 删除临时文件 return minioPath; }; export const getZip = async (page: PageModel, opts: { tokenUser: any }) => { const _result = await getDeck(page); const result = getContainerData(_result); const html = getHTML({ rootId: page.id, title: page?.publish?.key }); const dataJs = getDataJs(result); const zip = archiver('zip', { zlib: { level: 9 }, }); // 创建 zip 文件的输出流 const zipCache = path.join(cacheFile, `${page.id}.zip`); if (checkFileExistsSync(zipCache)) { throw new CustomError('page is on uploading'); } return await new Promise((resolve, reject) => { const output = fs.createWriteStream(zipCache); // 监听事件 output.on('close', async () => { console.log(`Zip file has been created successfully. Total size: ${zip.pointer()} bytes.`); let time = (new Date().getTime() / 1000).toFixed(0); const name = page.title || page.id; const minioPath = await uploadMinioTemp({ ...opts, filePath: zipCache, path: `${name + '-' + time}.zip` }); resolve(minioPath); }); output.on('end', () => { console.log('Data has been drained.'); // 数据已被耗尽 throw new CustomError('Data has been drained.'); }); zip.on('warning', (err) => { if (err.code === 'ENOENT') { console.warn('File not found:', err); } else { throw err; } }); zip.on('error', (err) => { throw err; }); // 通过管道将 zip 数据流输出到指定文件 zip.pipe(output); // 添加 HTML 字符串作为文件到 zip 中 zip.append(html, { name: 'index.html' }); // 添加 JavaScript 字符串作为文件到 zip 中 zip.append(dataJs, { name: 'data.js' }); zip.append(JSON.stringify(page), { name: 'app.config.json5' }); // 可以继续添加更多内容,文件或目录等 // zip.append('Another content', { name: 'other.txt' }); // 结束归档(必须调用,否则 zip 文件无法完成) zip.finalize(); }); }; export const checkFileExistsSync = (filePath: string) => { try { // 使用 F_OK 检查文件或目录是否存在 fs.accessSync(filePath, fs.constants.F_OK); return true; } catch (err) { return false; } };