Files
cli/src/command/deploy.ts
abearxiong 5774391bbe feat: refactor deploy command to enhance file upload process and user handling
- Updated the deploy command to include a new username retrieval mechanism, falling back to the organization if not specified.
- Introduced uploadFilesV2 function to streamline file upload logic, including hash checking to prevent redundant uploads.
- Modified queryAppVersion to accept a create parameter for better version management.
- Added a new test file to validate the uploadFilesV2 functionality.
2026-02-02 17:57:50 +08:00

264 lines
9.1 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 { input, confirm } from '@inquirer/prompts';
import chalk from 'chalk';
import { upload } from '@/module/download/upload.ts';
import { getBufferHash, getHash } from '@/uitls/hash.ts';
import { queryAppVersion } from '@/query/app-manager/query-app.ts';
import { logger } from '@/module/logger.ts';
import { getUsername } from './login.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;
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: opts?.appKey || 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('-d, --dot', '是否上传隐藏文件')
.option('--dir, --directory <directory>', '上传的默认路径')
.action(async (filePath, options) => {
try {
let { version, key, yes, update, org, showBackend } = options;
const dot = !!options.dot;
const pkgInfo = getPackageJson({ version, appKey: key });
if (!version && pkgInfo?.version) {
version = pkgInfo?.version || '';
}
if (!key && pkgInfo?.appKey) {
key = pkgInfo?.appKey || '';
}
logger.debug('start deploy');
if (!version) {
version = await input({
message: 'Enter your version:',
});
}
if (!key) {
key = await input({
message: 'Enter your 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];
}
logger.debug('upload Files', _relativeFiles);
logger.debug('upload Files Key', key, version);
if (!yes) {
// 确认是否上传
const confirmed = await confirm({
message: 'Do you want to upload these files?',
});
if (!confirmed) {
return;
}
}
let username = '';
if (pkgInfo?.user) {
username = pkgInfo.user;
} else if (org) {
username = org;
} else {
const me = await getUsername();
if (me) {
username = me;
} else {
logger.error('无法获取用户名,请使用先登录');
return;
}
}
const uploadDirectory = isDirectory ? directory : path.dirname(directory);
const res = await uploadFilesV2(_relativeFiles, uploadDirectory, { key, version, username: username, directory: options.directory });
logger.debug('upload res', res);
if (res?.code === 200) {
const res2 = await queryAppVersion({
key: key,
version: version,
create: true
});
logger.debug('queryAppVersion res', res2, key, 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) {
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(chalk.blue('下一个步骤服务端应用部署:\n'), 'envision pack-deploy', id);
}
} else {
console.error('File upload failed', res?.message);
}
} catch (error) {
console.error('error', error);
}
});
type UploadFileOptions = {
key: string;
version: string;
username: string;
directory?: string;
};
export const uploadFilesV2 = async (files: string[], directory: string, opts: UploadFileOptions): Promise<any> => {
const { key, version, username } = opts || {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
const filePath = path.join(directory, file);
logger.info('[上传进度]', `${i + 1}/${files.length}`, file);
const form = new FormData();
const filename = path.basename(filePath);
// 解决 busbox 文件名乱码: 将 UTF-8 编码的文件名转换为 binary 字符串
const encodedFilename = Buffer.from(filename, 'utf-8').toString('binary');
form.append('file', fs.createReadStream(filePath), {
filename: encodedFilename,
filepath: file,
});
const _baseURL = getBaseURL();
const url = new URL(`/${username}/resources/${key}/${version}/${file}`, _baseURL);
// console.log('upload file', file, filePath);
const token = await storage.getItem('token');
const check = () => {
const checkUrl = new URL(url.toString());
checkUrl.searchParams.set('stat', '1');
const res = query
.adapter({ url: checkUrl.toString(), method: 'GET', headers: { Authorization: 'Bearer ' + token } })
return res;
}
const checkRes = await check();
let needUpload = false;
let hash = '';
if (checkRes?.code === 404) {
needUpload = true;
hash = getHash(filePath);
} else if (checkRes?.code === 200) {
const etag = checkRes?.data?.etag;
hash = getHash(filePath);
if (etag !== hash) {
needUpload = true;
}
}
if (needUpload) {
url.searchParams.append('hash', hash);
const res = await upload({ url: url, form: form, token: token });
logger.debug('upload file', file, res);
if (res.code !== 200) {
logger.error('文件上传失败', file, res);
return { code: 500, message: '文件上传失败', file, fileRes: res };
}
} else {
console.log(chalk.green('\t 文件已经上传过了', url.toString()));
}
continue;
}
return { code: 200 }
}
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) {
logger.info(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);
logger.info(chalk.blue('deployURL', deployURL.href));
} catch (error) { }
} else {
logger.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);