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('', '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('-d, --dot', '是否上传隐藏文件') .option('--dir, --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 => { 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') .option('-o, --org ', 'org') .action(async (id, opts) => { deployLoadFn(id, opts?.org); }); app.addCommand(deployLoad);