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.
This commit is contained in:
2026-02-02 17:57:50 +08:00
parent 310d727321
commit 5774391bbe
6 changed files with 373 additions and 166 deletions

View File

@@ -7,9 +7,10 @@ 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 { getHash } from '@/uitls/hash.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
@@ -44,13 +45,11 @@ const command = new Command('deploy')
.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;
const pkgInfo = getPackageJson({ version, appKey: key });
if (!version && pkgInfo?.version) {
@@ -109,18 +108,30 @@ const command = new Command('deploy')
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 uploadFiles(_relativeFiles, uploadDirectory, { key, version, username: org, noCheckAppFiles: !noCheck, directory: options.directory });
const res = await uploadFilesV2(_relativeFiles, uploadDirectory, { key, version, username: username, directory: options.directory });
logger.debug('upload res', res);
if (res?.code === 200) {
res.data?.upload?.map?.((d) => {
console.log(chalk.green('uploaded file', d?.name, d?.path));
});
const res2 = await queryAppVersion({
key: key,
version: version,
create: true
});
logger.debug('queryAppVersion res', res2);
logger.debug('queryAppVersion res', res2, key, version);
if (res2.code !== 200) {
console.error(chalk.red('查询应用版本失败'), res2.message, key);
return;
@@ -145,7 +156,6 @@ const command = new Command('deploy')
} else {
console.error('File upload failed', res?.message);
}
return res;
} catch (error) {
console.error('error', error);
}
@@ -154,88 +164,65 @@ const command = new Command('deploy')
type UploadFileOptions = {
key: string;
version: string;
username?: string;
noCheckAppFiles?: boolean;
username: string;
directory?: string;
};
const uploadFiles = async (files: string[], directory: string, opts: UploadFileOptions): Promise<any> => {
export const uploadFilesV2 = async (files: string[], directory: string, opts: UploadFileOptions): Promise<any> => {
const { key, version, username } = opts || {};
const form = new FormData();
const data: Record<string, any> = { files: [] };
let description = '';
for (const file of files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const filePath = path.join(directory, file);
const hash = getHash(filePath);
if (!hash) {
logger.error('文件', filePath, '不存在');
logger.error('请检查文件是否存在');
}
data.files.push({ path: file, hash: hash });
if (filePath.includes('readme.md')) {
description = fs.readFileSync(filePath, 'utf-8');
}
}
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) {
logger.debug('文件已经上传过了', file);
continue;
}
logger.info('[上传进度]', `${i + 1}/${files.length}`, file);
const form = new FormData();
const filename = path.basename(filePath);
logger.debug('upload file', file, filename);
// 解决 busbox 文件名乱码: 将 UTF-8 编码的文件名转换为 binary 字符串
const encodedFilename = Buffer.from(filename, 'utf-8').toString('binary');
form.append('file', fs.createReadStream(filePath), {
filename: encodedFilename,
filepath: file,
});
needUpload = true;
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;
}
if (!needUpload) {
logger.debug('所有文件都上传过了,不需要上传文件');
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 });
};
return { code: 200 }
}
app.addCommand(command);
const deployLoadFn = async (id: string, org?: string) => {

View File

@@ -82,6 +82,22 @@ const loginCommand = new Command('login')
program.addCommand(loginCommand);
export const getUsername = async () => {
const token = getEnvToken();
const localToken = storage.getItem('token');
if (!token && !localToken) {
console.log('请先登录');
return null;
}
let me = await queryLogin.getMe(localToken || token);
if (me?.code === 401) {
me = await queryLogin.getMe();
}
if (me?.code === 200) {
return me.data?.username;
}
return null;
}
const showMe = async (show = true) => {
const token = getEnvToken();
const localToken = storage.getItem('token');

View File

@@ -26,7 +26,7 @@ export const queryApp = async (params: QueryAppParams, opts?: any) => {
);
};
export const queryAppVersion = async (params: { key?: string; version?: string; id?: string }, opts?: DataOpts) => {
export const queryAppVersion = async (params: { key?: string; version?: string; id?: string, create?: boolean }, opts?: DataOpts) => {
logger.debug('queryAppVersion params', params, query.url);
return await query.post(
{