Files
cli/src/command/deploy.ts
2025-11-28 02:49:52 +08:00

292 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { program as app, Command } from '@/program.ts';
import glob from 'fast-glob';
import path from 'path';
import fs from 'fs';
import FormData from 'form-data';
import { getBaseURL, query, storage } from '@/module/query.ts';
import inquirer from 'inquirer';
import chalk from 'chalk';
import { upload } from '@/module/download/upload.ts';
import { getHash } from '@/uitls/hash.ts';
import { queryAppVersion } from '@/query/app-manager/query-app.ts';
import { logger } from '@/module/logger.ts';
/**
* 获取package.json 中的 basename, version, user, appKey
* @returns
*/
export const getPackageJson = (opts?: { version?: string; appKey?: string }) => {
const filePath = path.join(process.cwd(), 'package.json');
if (!fs.existsSync(filePath)) {
return null;
}
try {
const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const basename = packageJson.basename || '';
const version = packageJson.version || '';
const app = packageJson.app as { key: string };
const userAppArry = basename.split('/');
if (userAppArry.length <= 2 && !opts?.appKey) {
console.error(chalk.red('basename is error, 请输入正确的路径, packages.json中basename例如 /root/appKey'));
return null;
}
const [user, appKey] = userAppArry;
return { basename, version, pkg: packageJson, user, appKey: appKey || opts?.appKey, app };
} catch (error) {
return null;
}
};
const command = new Command('deploy')
.description('把前端文件传到服务器')
.argument('<filePath>', 'Path to the file to be uploaded, filepath or directory') // 定义文件路径参数
.option('-v, --version <version>', 'verbose')
.option('-k, --key <key>', 'key')
.option('-y, --yes <yes>', 'yes')
.option('-o, --org <org>', 'org')
.option('-u, --update', 'load current app. set current version in product。 redis 缓存更新')
.option('-s, --showBackend', 'show backend url, 部署的后端应用显示执行的cli命令')
.option('-c, --noCheck', '是否受app manager控制的模块。默认检测')
.option('-d, --dot', '是否上传隐藏文件')
.option('--dir, --directory <directory>', '上传的默认路径')
.action(async (filePath, options) => {
try {
let { version, key, yes, update, org, showBackend } = options;
const noCheck = !options.noCheck;
const dot = !!options.dot;
// 获取当前目录是否存在package.json, 如果有从package.json 获取 version 和basename
const pkgInfo = getPackageJson({ version, appKey: key });
if (!version && pkgInfo?.version) {
version = pkgInfo?.version || '';
}
if (!key && pkgInfo?.appKey) {
key = pkgInfo?.appKey || '';
}
console.log('start deploy');
if (!version || !key) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'version',
message: 'Enter your version:',
when: () => !version,
},
{
type: 'input',
name: 'key',
message: 'Enter your key:',
when: () => !key,
},
]);
version = answers.version || version;
key = answers.key || key;
}
const pwd = process.cwd();
const directory = path.join(pwd, filePath);
// 获取directory如果是文件夹获取文件夹下所有文件如果是文件获取文件
const stat = fs.statSync(directory);
let _relativeFiles = [];
let isDirectory = false;
if (stat.isDirectory()) {
isDirectory = true;
const files = await glob('**/*', {
cwd: directory,
ignore: ['node_modules/**/*', '.git/**/*', '.DS_Store'],
onlyFiles: true,
dot,
absolute: true,
});
// console.log('files', files);
// 添加一个工具函数来统一处理路径
const normalizeFilePath = (filePath: string) => {
return filePath.split(path.sep).join('/');
};
_relativeFiles = files.map((file) => {
const relativePath = path.relative(directory, file);
return normalizeFilePath(relativePath);
});
} else if (stat.isFile()) {
const filename = path.basename(directory);
_relativeFiles = [filename];
}
console.log('upload Files', _relativeFiles);
console.log('upload Files Key', key, version);
if (!yes) {
// 确认是否上传
const confirm = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Do you want to upload these files?',
},
]);
if (!confirm.confirm) {
return;
}
}
const uploadDirectory = isDirectory ? directory : path.dirname(directory);
const res = await uploadFiles(_relativeFiles, uploadDirectory, { key, version, username: org, noCheckAppFiles: !noCheck, directory: options.directory });
if (res?.code === 200) {
console.log('File uploaded successfully!');
res.data?.upload?.map?.((d) => {
console.log(chalk.green('uploaded file', d?.name, d?.path));
});
const res2 = await queryAppVersion({
key: key,
version: version,
});
if (res2.code !== 200) {
console.error(chalk.red('查询应用版本失败'), res2.message, key);
return;
}
// const { id, data, ...rest } = res.data?.app || {};
const { id, data, ...rest } = res2.data || {};
if (id && !update) {
console.log(chalk.green('id: '), id);
if (!org) {
console.log(chalk.green(`更新为最新版本: envision deploy-load ${id}`));
} else {
console.log(chalk.green(`更新为最新版本: envision deploy-load ${id} -o ${org}`));
}
} else if (id && update) {
deployLoadFn(id);
} else {
// console.log('rest', JSON.stringify(res.data, null, 2));
}
logger.debug('deploy success', res2.data);
if (id && showBackend) {
console.log('\n');
// 获取当前应用的key
const pkKey = pkgInfo?.app?.key || pkgInfo?.appKey;
console.log(chalk.blue('服务端应用部署: '), 'envision pack-deploy', id, '-k <key>');
if (pkKey) {
console.log('\n');
console.log(chalk.blue('命令推荐: '), 'envision pack-deploy', id, `-k ${pkKey} -f`);
}
console.log('\n');
}
} else {
console.error('File upload failed', res?.message);
}
return res;
} catch (error) {
console.error('error', error);
}
});
type UploadFileOptions = {
key: string;
version: string;
username?: string;
noCheckAppFiles?: boolean;
directory?: string;
};
const uploadFiles = async (files: string[], directory: string, opts: UploadFileOptions): Promise<any> => {
const { key, version, username } = opts || {};
const form = new FormData();
const data: Record<string, any> = { files: [] };
for (const file of files) {
const filePath = path.join(directory, file);
const hash = getHash(filePath);
if(!hash){
console.error('文件', filePath, '不存在');
console.error('请检查文件是否存在');
}
data.files.push({ path: file, hash: hash });
}
data.appKey = key;
data.version = version;
form.append('appKey', key);
form.append('version', version);
if (username) {
form.append('username', username);
data.username = username;
}
if (opts?.directory) {
form.append('directory', opts.directory);
data.directory = opts.directory;
}
const token = await storage.getItem('token');
const checkUrl = new URL('/api/s1/resources/upload/check', getBaseURL());
const res = await query.adapter({ url: checkUrl.toString(), method: 'POST', body: data, headers: { Authorization: 'Bearer ' + token } }).then((res) => {
try {
if (typeof res === 'string') {
return JSON.parse(res);
} else {
return res;
}
} catch (error) {
return typeof res === 'string' ? {} : res;
}
});
const checkData: { path: string; isUpload: boolean }[] = res.data;
if (res.code !== 200) {
console.error('check failed', res);
return res;
}
let needUpload = false;
for (const file of files) {
const filePath = path.join(directory, file);
const check = checkData.find((d) => d.path === file);
if (check?.isUpload) {
console.log('文件已经上传过了', file);
continue;
}
const filename = path.basename(filePath);
console.log('upload file', file, filename);
form.append('file', fs.createReadStream(filePath), {
filename: filename,
filepath: file,
});
needUpload = true;
}
if (!needUpload) {
console.log('所有文件都上传过了,不需要上传文件');
return {
code: 200,
};
}
const _baseURL = getBaseURL();
const url = new URL('/api/s1/resources/upload', _baseURL);
if (opts.noCheckAppFiles) {
url.searchParams.append('noCheckAppFiles', 'true');
}
return upload({ url: url, form: form, token: token });
};
app.addCommand(command);
const deployLoadFn = async (id: string, org?: string) => {
if (!id) {
console.error(chalk.red('id is required'));
return;
}
const res = await query.post({
path: 'app',
key: 'publish',
data: {
id: id,
username: org,
},
});
if (res.code === 200) {
console.log(chalk.green('deploy-load success. current version:', res.data?.version));
// /:username/:appName
try {
const { user, key } = res.data;
const baseURL = getBaseURL();
const deployURL = new URL(`/${user}/${key}/`, baseURL);
console.log(chalk.blue('deployURL', deployURL.href));
} catch (error) {}
} else {
console.error('deploy-load failed', res.message);
}
};
const deployLoad = new Command('deploy-load')
.description('部署加载')
.argument('<id>', 'id')
.option('-o, --org <org>', 'org')
.action(async (id, opts) => {
deployLoadFn(id, opts?.org);
});
app.addCommand(deployLoad);