Files
cli/src/module/download/install.ts

291 lines
8.0 KiB
TypeScript

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<Package>, 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<Result<AiList[]>> => {
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;
};