import path from 'path'; import fs from 'fs'; import { storage, baseURL } from '../query.ts'; import { chalk } from '../chalk.ts'; import { Result } from '@kevisual/query'; import { fileIsExist } from '@/uitls/file.ts'; import { glob } from 'fast-glob'; import inquirer from 'inquirer'; type DownloadTask = { downloadPath: string; downloadUrl: string; user: string; key: string; version: string; }; export type Package = { id: string; name?: string; version?: string; description?: string; title?: string; user?: string; key?: string; [key: string]: any; }; type Options = { check?: boolean; returnContent?: boolean; setToken?: boolean; hash?: string; [key: string]: any; }; export const fetchLink = async (url: string = '', opts?: Options) => { const token = process.env.KEVISUAL_TOKEN || storage.getItem('token'); const fetchURL = new URL(url); const check = opts?.check ?? false; const isKevisual = !!url.includes('kevisual'); const setToken = opts?.setToken ?? isKevisual; if (check) { if (!url.startsWith(baseURL)) { throw new Error('url must start with ' + baseURL); } } if (token && setToken) { fetchURL.searchParams.set('token', token); } if (opts?.hash) { fetchURL.searchParams.set('hash', opts.hash); } fetchURL.searchParams.set('download', 'true'); const res = await fetch(fetchURL.toString()); const blob = await res.blob(); const type = blob.type; let content: Buffer | undefined; if (opts?.returnContent) { content = Buffer.from(await blob.arrayBuffer()); } const pathname = fetchURL.pathname; const filename = pathname.split('/').pop(); return { status: res.status, filename, blob, type, content, }; }; const checkDelete = async (opts?: { force?: boolean; dir?: string; yes?: boolean }) => { const { force = false, dir = '', yes = false } = opts || {}; if (force) { try { if (fileIsExist(dir)) { const files = await glob(`${dir}/**/*`, { onlyFiles: true }); const answers = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `是否你需要删除 【${opts?.dir}】 目录下的文件. [${files.length}] 个?`, when: () => files.length > 0 && !yes, // 当 username 为空时,提示用户输入 }, ]); if (answers?.confirm || yes) { fs.rmSync(dir, { recursive: true }); console.log(chalk.green('删除成功', dir)); } else { console.log(chalk.red('取消删除', dir)); } } } catch (error) { console.error(error); } finally { fs.mkdirSync(dir, { recursive: true }); } } }; export const rewritePkg = (packagePath: string, pkg: Package) => { const readJsonFile = (filePath: string) => { try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch (error) { return {}; } }; try { const dirname = path.dirname(packagePath); if (!fs.existsSync(dirname)) { fs.mkdirSync(dirname, { recursive: true }); } const json = readJsonFile(packagePath); json.id = pkg?.id; json.appInfo = pkg; fs.writeFileSync(packagePath, JSON.stringify(json, null, 2)); } catch (error) { fs.writeFileSync(packagePath, JSON.stringify({ appInfo: pkg, id: pkg?.id }, null, 2)); } return pkg; }; type InstallAppOpts = { appDir?: string; kevisualUrl?: string; /** * 是否强制覆盖, 下载前删除已有的 */ force?: boolean; yes?: boolean; }; export const installApp = async (app: Package, opts: InstallAppOpts = {}) => { // const _app = demoData; const { appDir = '', kevisualUrl = 'https://kevisual.cn', } = opts; const _app = app; try { let files = _app.data.files || []; const version = _app.version; const user = _app.user; const key = _app.key; const downloadDirPath = path.join(appDir, user, key); await checkDelete({ force: opts?.force, yes: opts?.yes, dir: downloadDirPath }); const packagePath = path.join(appDir, `${user}/${key}/package.json`); const downFiles = files .filter((file: any) => file?.path) .map((file: any) => { const name = file?.name || ''; const noVersionPath = file.path.replace(`/${version}`, ''); let downloadPath = noVersionPath; let downloadUrl = ''; if (file.path.startsWith('http')) { downloadUrl = file.path; } else { downloadUrl = `${kevisualUrl}/${noVersionPath}`; } return { ...file, downloadPath: path.join(appDir, downloadPath), downloadUrl: downloadUrl, }; }); const downloadTasks: DownloadTask[] = downFiles as any; console.log('downloadTasks', downloadTasks); for (const file of downloadTasks) { const downloadPath = file.downloadPath; const downloadUrl = file.downloadUrl; const dir = path.dirname(downloadPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } console.log('downloadUrl', downloadUrl); const { blob, type } = await fetchLink(downloadUrl); if (type.includes('text/html')) { const html = await blob.text(); if (html === 'fetchRes is error') { console.log(chalk.red('fetchRes is error'), '下载失败', downloadUrl); throw new Error('fetchRes is error'); } } fs.writeFileSync(downloadPath, Buffer.from(await blob.arrayBuffer())); } let indexHtml = files.find((file: any) => file.name === 'index.html'); // if (!indexHtml) { // files.push({ // name: 'index.html', // path: `${user}/${key}/index.html`, // }); // fs.writeFileSync(path.join(appDir, `${user}/${key}/index.html`), JSON.stringify(app, null, 2)); // } _app.data.files = files; rewritePkg(packagePath, _app); return { code: 200, data: _app, message: 'Install app success', }; } catch (error) { console.error(error); return { code: 500, message: 'Install app failed', }; } }; /** * 检查是否为空,如果为空则删除 * @param appDir */ export const checkAppDir = (appDir: string) => { try { const files = fs.readdirSync(appDir); if (files.length === 0) { fs.rmSync(appDir, { recursive: true }); } } catch (error) { } }; export const checkFileExists = (path: string) => { try { fs.accessSync(path); return true; } catch (error) { return false; } }; type UninstallAppOpts = { appDir?: string; type?: 'app' | 'web'; }; export const uninstallApp = async (app: Partial, opts: UninstallAppOpts = {}) => { const { appDir = '' } = opts; try { const { user, key } = app; const keyDir = path.join(appDir, user, key); const parentDir = path.join(appDir, user); if (!checkFileExists(appDir) || !checkFileExists(keyDir)) { return { code: 200, message: 'uninstall app success', }; } try { // 删除appDir和文件 fs.rmSync(keyDir, { recursive: true }); } catch (error) { console.error(error); } checkAppDir(parentDir); return { code: 200, message: 'Uninstall app success', }; } catch (error) { console.error(error); return { code: 500, message: 'Uninstall app failed', }; } }; export type AiList = { name?: string; lastModified?: string; etag?: string; size?: number; path: string; pathname?: string; url?: string; }; export const fetchAiList = async (url: string, opts?: { recursive: boolean }): Promise> => { const token = process.env.KEVISUAL_TOKEN || storage.getItem('token'); const _url = new URL(url); const dir = _url.searchParams.get('dir'); if (!dir) { _url.searchParams.set('dir', 'true'); } if (opts?.recursive) { _url.searchParams.set('recursive', 'true'); } if (!_url.pathname.endsWith('/')) { _url.pathname += '/'; } const res = await fetch(_url.toString(), { method: 'GET', headers: { Authorization: 'Bearer ' + token, }, }); const data = await res.json(); return data; };