286 lines
9.7 KiB
TypeScript
286 lines
9.7 KiB
TypeScript
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;
|
||
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');
|
||
console.log(chalk.blue('服务端应用部署: '), 'envision pack-deploy', id);
|
||
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);
|
||
|