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('', 'Path to the file to be uploaded, filepath or directory') // 定义文件路径参数 .option('-v, --version ', 'verbose') .option('-k, --key ', 'key') .option('-y, --yes ', 'yes') .option('-o, --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 ', '上传的默认路径') .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 '); 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 => { const { key, version, username } = opts || {}; const form = new FormData(); const data: Record = { 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') .option('-o, --org ', 'org') .action(async (id, opts) => { deployLoadFn(id, opts?.org); }); app.addCommand(deployLoad);