mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-10 16:51:11 +08:00
【新增】私有证书
This commit is contained in:
403
frontend/packages/gulp-build-tools/src/modules/compress.ts
Normal file
403
frontend/packages/gulp-build-tools/src/modules/compress.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import { src, dest, TaskFunction } from 'gulp';
|
||||
import gulpZip from 'gulp-zip';
|
||||
import { CompressConfig, TaskResult } from '../types.js';
|
||||
import chalk from 'chalk';
|
||||
import through2 from 'through2';
|
||||
import archiver from 'archiver';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 创建 ZIP 压缩任务
|
||||
* @param config 压缩配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createZipTask(config: CompressConfig): TaskFunction {
|
||||
return function zipFiles(cb) {
|
||||
let fileCount = 0;
|
||||
|
||||
console.log(chalk.blue('📦 开始创建 ZIP 压缩包...'));
|
||||
console.log(chalk.gray(`源路径: ${Array.isArray(config.src) ? config.src.join(', ') : config.src}`));
|
||||
console.log(chalk.gray(`压缩包: ${config.filename}`));
|
||||
console.log(chalk.gray(`输出目录: ${config.dest}`));
|
||||
|
||||
const stream = src(config.src, { allowEmpty: true })
|
||||
.pipe(through2.obj(function(file, enc, callback) {
|
||||
fileCount++;
|
||||
console.log(chalk.yellow(`添加文件: ${file.relative}`));
|
||||
this.push(file);
|
||||
callback();
|
||||
}))
|
||||
.pipe(gulpZip(config.filename))
|
||||
.pipe(dest(config.dest));
|
||||
|
||||
stream.on('end', () => {
|
||||
const zipPath = path.join(config.dest, config.filename);
|
||||
const stats = fs.statSync(zipPath);
|
||||
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
console.log(chalk.green(`✅ ZIP 压缩完成`));
|
||||
console.log(chalk.gray(` - 文件数量: ${fileCount}`));
|
||||
console.log(chalk.gray(` - 压缩包大小: ${fileSizeMB} MB`));
|
||||
console.log(chalk.gray(` - 保存路径: ${zipPath}`));
|
||||
cb();
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error(chalk.red('❌ ZIP 压缩失败:'), error);
|
||||
cb(error);
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义压缩任务(支持 tar, gzip 等)
|
||||
* @param config 压缩配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createCustomCompressTask(config: CompressConfig): TaskFunction {
|
||||
return function compressFiles(cb) {
|
||||
let fileCount = 0;
|
||||
|
||||
console.log(chalk.blue(`📦 开始创建 ${config.type?.toUpperCase()} 压缩包...`));
|
||||
console.log(chalk.gray(`源路径: ${Array.isArray(config.src) ? config.src.join(', ') : config.src}`));
|
||||
console.log(chalk.gray(`压缩包: ${config.filename}`));
|
||||
console.log(chalk.gray(`输出目录: ${config.dest}`));
|
||||
|
||||
const outputPath = path.join(config.dest, config.filename);
|
||||
const output = fs.createWriteStream(outputPath);
|
||||
|
||||
let archive: archiver.Archiver;
|
||||
|
||||
switch (config.type) {
|
||||
case 'tar':
|
||||
archive = archiver('tar', {
|
||||
gzip: false
|
||||
});
|
||||
break;
|
||||
case 'gzip':
|
||||
archive = archiver('tar', {
|
||||
gzip: true,
|
||||
gzipOptions: {
|
||||
level: config.level || 6
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
archive = archiver('zip', {
|
||||
zlib: { level: config.level || 6 }
|
||||
});
|
||||
}
|
||||
|
||||
// 监听错误事件
|
||||
archive.on('error', (err) => {
|
||||
console.error(chalk.red('❌ 压缩过程中出错:'), err);
|
||||
cb(err);
|
||||
});
|
||||
|
||||
// 监听警告事件
|
||||
archive.on('warning', (err) => {
|
||||
console.warn(chalk.yellow('⚠️ 压缩警告:'), err);
|
||||
});
|
||||
|
||||
// 监听完成事件
|
||||
output.on('close', () => {
|
||||
const stats = fs.statSync(outputPath);
|
||||
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
console.log(chalk.green(`✅ ${config.type?.toUpperCase()} 压缩完成`));
|
||||
console.log(chalk.gray(` - 文件数量: ${fileCount}`));
|
||||
console.log(chalk.gray(` - 压缩包大小: ${fileSizeMB} MB`));
|
||||
console.log(chalk.gray(` - 保存路径: ${outputPath}`));
|
||||
cb();
|
||||
});
|
||||
|
||||
// 将压缩包管道到输出流
|
||||
archive.pipe(output);
|
||||
|
||||
// 创建 Gulp 流处理文件
|
||||
const stream = src(config.src, { allowEmpty: true })
|
||||
.pipe(through2.obj(function(file, enc, callback) {
|
||||
if (!file.isBuffer()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fileCount++;
|
||||
console.log(chalk.yellow(`添加文件: ${file.relative}`));
|
||||
|
||||
// 将文件添加到压缩包
|
||||
archive.append(file.contents, { name: file.relative });
|
||||
|
||||
callback();
|
||||
}));
|
||||
|
||||
stream.on('end', () => {
|
||||
// 完成压缩
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error(chalk.red('❌ 文件处理失败:'), error);
|
||||
cb(error);
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建压缩任务(根据配置自动选择压缩方式)
|
||||
* @param config 压缩配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createCompressTask(config: CompressConfig): TaskFunction {
|
||||
if (!config.type || config.type === 'zip') {
|
||||
return createZipTask(config);
|
||||
} else {
|
||||
return createCustomCompressTask(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量压缩文件
|
||||
* @param configs 压缩配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function batchCompress(configs: CompressConfig[]): Promise<TaskResult[]> {
|
||||
const results: TaskResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createCompressTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
message: `文件压缩成功: ${path.join(config.dest, config.filename)}`,
|
||||
fileCount: 1
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `文件压缩失败: ${path.join(config.dest, config.filename)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行压缩文件
|
||||
* @param configs 压缩配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function parallelCompress(configs: CompressConfig[]): Promise<TaskResult[]> {
|
||||
console.log(chalk.blue(`📦 开始并行压缩 ${configs.length} 个文件包...`));
|
||||
|
||||
const promises = configs.map(async (config, index) => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createCompressTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `压缩成功 [${index + 1}]: ${path.join(config.dest, config.filename)}`,
|
||||
fileCount: 1
|
||||
} as TaskResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `压缩失败 [${index + 1}]: ${path.join(config.dest, config.filename)}`
|
||||
} as TaskResult;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(chalk.green(`✅ 并行压缩完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多压缩任务
|
||||
* @param configs 压缩配置数组
|
||||
* @param parallel 是否并行执行
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createMultiCompressTask(configs: CompressConfig[], parallel: boolean = false): TaskFunction {
|
||||
return async function multiCompress(cb) {
|
||||
try {
|
||||
console.log(chalk.blue(`📦 开始${parallel ? '并行' : '串行'}压缩 ${configs.length} 个文件包...`));
|
||||
|
||||
// 验证所有配置
|
||||
for (const config of configs) {
|
||||
const errors = compressHelpers.validateConfig(config);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`压缩配置错误 (${config.filename}): ${errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
let results: TaskResult[];
|
||||
|
||||
if (parallel) {
|
||||
results = await parallelCompress(configs);
|
||||
} else {
|
||||
results = await batchCompress(configs);
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
// 计算总文件大小
|
||||
let totalSize = 0;
|
||||
for (const config of configs) {
|
||||
try {
|
||||
const filePath = path.join(config.dest, config.filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stats = fs.statSync(filePath);
|
||||
totalSize += stats.size;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略获取文件大小失败的错误
|
||||
}
|
||||
}
|
||||
|
||||
const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2);
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.log(chalk.yellow(`⚠️ 部分压缩失败:`));
|
||||
results.filter(r => !r.success).forEach(result => {
|
||||
console.log(chalk.red(` - ${result.message}`));
|
||||
});
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ 多压缩任务完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
console.log(chalk.gray(` - 总压缩包大小: ${totalSizeMB} MB`));
|
||||
|
||||
if (failureCount > 0 && successCount === 0) {
|
||||
cb(new Error('所有压缩任务都失败了'));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 多压缩任务执行失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩工具函数
|
||||
*/
|
||||
export const compressHelpers = {
|
||||
/**
|
||||
* 创建压缩配置
|
||||
* @param src 源文件路径
|
||||
* @param filename 压缩包文件名
|
||||
* @param dest 输出目录
|
||||
* @param options 其他选项
|
||||
*/
|
||||
createConfig: (
|
||||
src: string | string[],
|
||||
filename: string,
|
||||
dest: string,
|
||||
options: Partial<CompressConfig> = {}
|
||||
): CompressConfig => {
|
||||
return {
|
||||
src,
|
||||
filename,
|
||||
dest,
|
||||
type: 'zip',
|
||||
level: 6,
|
||||
...options
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据文件扩展名推断压缩类型
|
||||
* @param filename 文件名
|
||||
*/
|
||||
inferType: (filename: string): CompressConfig['type'] => {
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.zip':
|
||||
return 'zip';
|
||||
case '.tar':
|
||||
return 'tar';
|
||||
case '.gz':
|
||||
case '.tgz':
|
||||
return 'gzip';
|
||||
default:
|
||||
return 'zip';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成带时间戳的文件名
|
||||
* @param basename 基础文件名
|
||||
* @param extension 扩展名
|
||||
*/
|
||||
timestampedFilename: (basename: string, extension: string = 'zip'): string => {
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
|
||||
return `${basename}-${timestamp}.${extension}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推荐的压缩级别
|
||||
* @param priority 优先级 ('speed' | 'size' | 'balanced')
|
||||
*/
|
||||
getCompressionLevel: (priority: 'speed' | 'size' | 'balanced' = 'balanced'): number => {
|
||||
switch (priority) {
|
||||
case 'speed':
|
||||
return 1;
|
||||
case 'size':
|
||||
return 9;
|
||||
case 'balanced':
|
||||
default:
|
||||
return 6;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证压缩配置
|
||||
* @param config 压缩配置
|
||||
*/
|
||||
validateConfig: (config: CompressConfig): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.src) errors.push('缺少源文件路径');
|
||||
if (!config.filename) errors.push('缺少压缩包文件名');
|
||||
if (!config.dest) errors.push('缺少输出目录');
|
||||
if (config.level && (config.level < 0 || config.level > 9)) {
|
||||
errors.push('压缩级别必须在 0-9 之间');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
433
frontend/packages/gulp-build-tools/src/modules/git.ts
Normal file
433
frontend/packages/gulp-build-tools/src/modules/git.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import { TaskFunction } from 'gulp';
|
||||
import { GitConfig, TaskResult } from '../types.js';
|
||||
import chalk from 'chalk';
|
||||
import { simpleGit, SimpleGit } from 'simple-git';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 创建 Git 操作任务
|
||||
* @param config Git 配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createGitTask(config: GitConfig): TaskFunction {
|
||||
return async function gitOperation(cb) {
|
||||
try {
|
||||
const git: SimpleGit = simpleGit({
|
||||
baseDir: config.repoPath || process.cwd(),
|
||||
binary: 'git',
|
||||
maxConcurrentProcesses: 6,
|
||||
});
|
||||
|
||||
console.log(chalk.blue(`🔧 执行 Git 操作: ${config.action}`));
|
||||
console.log(chalk.gray(`仓库路径: ${config.repoPath || process.cwd()}`));
|
||||
|
||||
switch (config.action) {
|
||||
case 'commit':
|
||||
await handleCommit(git, config);
|
||||
break;
|
||||
case 'pull':
|
||||
await handlePull(git, config);
|
||||
break;
|
||||
case 'push':
|
||||
await handlePush(git, config);
|
||||
break;
|
||||
case 'checkout':
|
||||
await handleCheckout(git, config);
|
||||
break;
|
||||
case 'branch':
|
||||
await handleBranch(git, config);
|
||||
break;
|
||||
case 'merge':
|
||||
await handleMerge(git, config);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的 Git 操作: ${config.action}`);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ Git 操作完成: ${config.action}`));
|
||||
cb();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`❌ Git 操作失败: ${config.action}`), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提交操作
|
||||
*/
|
||||
async function handleCommit(git: SimpleGit, config: GitConfig) {
|
||||
console.log(chalk.yellow('📝 准备提交代码...'));
|
||||
|
||||
// 检查是否有未提交的更改
|
||||
const status = await git.status();
|
||||
if (status.files.length === 0) {
|
||||
console.log(chalk.yellow('⚠️ 没有文件需要提交'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加文件
|
||||
if (config.files) {
|
||||
if (Array.isArray(config.files)) {
|
||||
for (const file of config.files) {
|
||||
await git.add(file);
|
||||
console.log(chalk.cyan(`添加文件: ${file}`));
|
||||
}
|
||||
} else {
|
||||
await git.add(config.files);
|
||||
console.log(chalk.cyan(`添加文件: ${config.files}`));
|
||||
}
|
||||
} else {
|
||||
await git.add('.');
|
||||
console.log(chalk.cyan('添加所有更改的文件'));
|
||||
}
|
||||
|
||||
// 提交
|
||||
const message = config.message || `自动提交 - ${new Date().toLocaleString()}`;
|
||||
await git.commit(message);
|
||||
console.log(chalk.green(`✅ 提交完成: ${message}`));
|
||||
|
||||
// 显示提交信息
|
||||
const log = await git.log({ maxCount: 1 });
|
||||
if (log.latest) {
|
||||
console.log(chalk.gray(`提交哈希: ${log.latest.hash}`));
|
||||
console.log(chalk.gray(`提交时间: ${log.latest.date}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拉取操作
|
||||
*/
|
||||
async function handlePull(git: SimpleGit, config: GitConfig) {
|
||||
console.log(chalk.yellow('⬇️ 拉取远程代码...'));
|
||||
|
||||
const remote = config.remote || 'origin';
|
||||
const branch = config.branch;
|
||||
|
||||
if (branch) {
|
||||
await git.pull(remote, branch);
|
||||
console.log(chalk.green(`✅ 拉取完成: ${remote}/${branch}`));
|
||||
} else {
|
||||
await git.pull();
|
||||
console.log(chalk.green(`✅ 拉取完成`));
|
||||
}
|
||||
|
||||
// 显示最新提交信息
|
||||
const log = await git.log({ maxCount: 1 });
|
||||
if (log.latest) {
|
||||
console.log(chalk.gray(`最新提交: ${log.latest.message}`));
|
||||
console.log(chalk.gray(`提交作者: ${log.latest.author_name}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理推送操作
|
||||
*/
|
||||
async function handlePush(git: SimpleGit, config: GitConfig) {
|
||||
console.log(chalk.yellow('⬆️ 推送代码到远程...'));
|
||||
|
||||
const remote = config.remote || 'origin';
|
||||
const branch = config.branch;
|
||||
|
||||
if (branch) {
|
||||
await git.push(remote, branch);
|
||||
console.log(chalk.green(`✅ 推送完成: ${remote}/${branch}`));
|
||||
} else {
|
||||
await git.push();
|
||||
console.log(chalk.green(`✅ 推送完成`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分支切换操作
|
||||
*/
|
||||
async function handleCheckout(git: SimpleGit, config: GitConfig) {
|
||||
if (!config.branch) {
|
||||
throw new Error('切换分支需要指定分支名称');
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`🔄 切换到分支: ${config.branch}`));
|
||||
|
||||
// 检查分支是否存在
|
||||
const branches = await git.branch();
|
||||
const branchExists = branches.all.includes(config.branch);
|
||||
|
||||
if (branchExists) {
|
||||
await git.checkout(config.branch);
|
||||
console.log(chalk.green(`✅ 切换到分支: ${config.branch}`));
|
||||
} else {
|
||||
// 创建并切换到新分支
|
||||
await git.checkoutLocalBranch(config.branch);
|
||||
console.log(chalk.green(`✅ 创建并切换到新分支: ${config.branch}`));
|
||||
}
|
||||
|
||||
// 显示当前分支信息
|
||||
const currentBranch = await git.branch();
|
||||
console.log(chalk.gray(`当前分支: ${currentBranch.current}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分支操作
|
||||
*/
|
||||
async function handleBranch(git: SimpleGit, config: GitConfig) {
|
||||
if (!config.branch) {
|
||||
// 列出所有分支
|
||||
console.log(chalk.yellow('📋 列出所有分支...'));
|
||||
const branches = await git.branch();
|
||||
|
||||
console.log(chalk.green('本地分支:'));
|
||||
Object.entries(branches.branches).forEach(([name, branch]) => {
|
||||
const marker = name === branches.current ? ' * ' : ' ';
|
||||
console.log(chalk.gray(`${marker}${name}`));
|
||||
});
|
||||
|
||||
if (Object.keys(branches.branches).some(name => name.startsWith('remotes/'))) {
|
||||
console.log(chalk.green('\n远程分支:'));
|
||||
Object.keys(branches.branches)
|
||||
.filter(name => name.startsWith('remotes/'))
|
||||
.forEach(name => {
|
||||
console.log(chalk.gray(` ${name}`));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 创建新分支
|
||||
console.log(chalk.yellow(`🌿 创建新分支: ${config.branch}`));
|
||||
await git.checkoutLocalBranch(config.branch);
|
||||
console.log(chalk.green(`✅ 分支创建完成: ${config.branch}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理合并操作
|
||||
*/
|
||||
async function handleMerge(git: SimpleGit, config: GitConfig) {
|
||||
if (!config.branch) {
|
||||
throw new Error('合并操作需要指定要合并的分支');
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`🔀 合并分支: ${config.branch}`));
|
||||
|
||||
try {
|
||||
await git.merge([config.branch]);
|
||||
console.log(chalk.green(`✅ 分支合并完成: ${config.branch}`));
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('CONFLICTS')) {
|
||||
console.log(chalk.red('❌ 合并冲突,需要手动解决'));
|
||||
|
||||
// 显示冲突文件
|
||||
const status = await git.status();
|
||||
if (status.conflicted.length > 0) {
|
||||
console.log(chalk.yellow('冲突文件:'));
|
||||
status.conflicted.forEach(file => {
|
||||
console.log(chalk.red(` - ${file}`));
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行 Git 操作
|
||||
* @param configs Git 配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function batchGitOperation(configs: GitConfig[]): Promise<TaskResult[]> {
|
||||
const results: TaskResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createGitTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
message: `Git 操作成功: ${config.action}`,
|
||||
fileCount: 1
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `Git 操作失败: ${config.action}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行执行 Git 操作
|
||||
* @param configs Git 配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function parallelGitOperation(configs: GitConfig[]): Promise<TaskResult[]> {
|
||||
console.log(chalk.blue(`🔧 开始并行执行 ${configs.length} 个 Git 操作...`));
|
||||
|
||||
const promises = configs.map(async (config, index) => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createGitTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Git 操作成功 [${index + 1}]: ${config.action}`,
|
||||
fileCount: 1
|
||||
} as TaskResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `Git 操作失败 [${index + 1}]: ${config.action}`
|
||||
} as TaskResult;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(chalk.green(`✅ 并行 Git 操作完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多 Git 操作任务
|
||||
* @param configs Git 配置数组
|
||||
* @param parallel 是否并行执行
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createMultiGitTask(configs: GitConfig[], parallel: boolean = false): TaskFunction {
|
||||
return async function multiGitOperation(cb) {
|
||||
try {
|
||||
console.log(chalk.blue(`🔧 开始${parallel ? '并行' : '串行'}执行 ${configs.length} 个 Git 操作...`));
|
||||
|
||||
let results: TaskResult[];
|
||||
|
||||
if (parallel) {
|
||||
results = await parallelGitOperation(configs);
|
||||
} else {
|
||||
results = await batchGitOperation(configs);
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.log(chalk.yellow(`⚠️ 部分操作失败:`));
|
||||
results.filter(r => !r.success).forEach(result => {
|
||||
console.log(chalk.red(` - ${result.message}`));
|
||||
});
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ 多 Git 操作完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
|
||||
if (failureCount > 0 && successCount === 0) {
|
||||
cb(new Error('所有 Git 操作都失败了'));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 多 Git 操作执行失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Git 工具函数
|
||||
*/
|
||||
export const gitHelpers = {
|
||||
/**
|
||||
* 检查 Git 仓库状态
|
||||
* @param repoPath 仓库路径
|
||||
*/
|
||||
checkStatus: async (repoPath?: string) => {
|
||||
const git = simpleGit(repoPath || process.cwd());
|
||||
const status = await git.status();
|
||||
|
||||
return {
|
||||
clean: status.files.length === 0,
|
||||
ahead: status.ahead,
|
||||
behind: status.behind,
|
||||
modified: status.modified,
|
||||
staged: status.staged,
|
||||
deleted: status.deleted,
|
||||
created: status.created,
|
||||
conflicted: status.conflicted
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前分支名
|
||||
* @param repoPath 仓库路径
|
||||
*/
|
||||
getCurrentBranch: async (repoPath?: string): Promise<string> => {
|
||||
const git = simpleGit(repoPath || process.cwd());
|
||||
const branch = await git.branch();
|
||||
return branch.current;
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否有未提交的更改
|
||||
* @param repoPath 仓库路径
|
||||
*/
|
||||
hasUncommittedChanges: async (repoPath?: string): Promise<boolean> => {
|
||||
const git = simpleGit(repoPath || process.cwd());
|
||||
const status = await git.status();
|
||||
return status.files.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取最新提交信息
|
||||
* @param repoPath 仓库路径
|
||||
*/
|
||||
getLatestCommit: async (repoPath?: string) => {
|
||||
const git = simpleGit(repoPath || process.cwd());
|
||||
const log = await git.log({ maxCount: 1 });
|
||||
return log.latest;
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证 Git 配置
|
||||
* @param config Git 配置
|
||||
*/
|
||||
validateConfig: (config: GitConfig): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.action) {
|
||||
errors.push('缺少 Git 操作类型');
|
||||
}
|
||||
|
||||
if (config.action === 'commit' && !config.message && !config.files) {
|
||||
errors.push('提交操作需要指定提交信息或文件');
|
||||
}
|
||||
|
||||
if (['checkout', 'merge'].includes(config.action) && !config.branch) {
|
||||
errors.push(`${config.action} 操作需要指定分支名称`);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
144
frontend/packages/gulp-build-tools/src/modules/rename.ts
Normal file
144
frontend/packages/gulp-build-tools/src/modules/rename.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { src, dest, TaskFunction } from 'gulp';
|
||||
import gulpRename from 'gulp-rename';
|
||||
import { RenameConfig, TaskResult } from '../types.js';
|
||||
import chalk from 'chalk';
|
||||
import through2 from 'through2';
|
||||
|
||||
/**
|
||||
* 创建文件重命名任务
|
||||
* @param config 重命名配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createRenameTask(config: RenameConfig): TaskFunction {
|
||||
return function renameFiles(cb) {
|
||||
let fileCount = 0;
|
||||
|
||||
console.log(chalk.blue('🔄 开始重命名文件...'));
|
||||
console.log(chalk.gray(`源路径: ${Array.isArray(config.src) ? config.src.join(', ') : config.src}`));
|
||||
console.log(chalk.gray(`目标路径: ${config.dest}`));
|
||||
|
||||
const stream = src(config.src, { allowEmpty: true })
|
||||
.pipe(through2.obj(function(file, enc, callback) {
|
||||
fileCount++;
|
||||
console.log(chalk.yellow(`处理文件: ${file.relative}`));
|
||||
this.push(file);
|
||||
callback();
|
||||
}))
|
||||
.pipe(gulpRename(config.rename))
|
||||
.pipe(through2.obj(function(file, enc, callback) {
|
||||
console.log(chalk.green(`重命名为: ${file.relative}`));
|
||||
this.push(file);
|
||||
callback();
|
||||
}))
|
||||
.pipe(dest(config.dest));
|
||||
|
||||
stream.on('end', () => {
|
||||
console.log(chalk.green(`✅ 文件重命名完成,共处理 ${fileCount} 个文件`));
|
||||
cb();
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error(chalk.red('❌ 文件重命名失败:'), error);
|
||||
cb(error);
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重命名文件
|
||||
* @param configs 重命名配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function batchRename(configs: RenameConfig[]): Promise<TaskResult[]> {
|
||||
const results: TaskResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createRenameTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
message: `文件重命名成功: ${config.dest}`
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `文件重命名失败: ${config.dest}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建常用的重命名函数
|
||||
*/
|
||||
export const renameHelpers = {
|
||||
/**
|
||||
* 添加前缀
|
||||
* @param prefix 前缀
|
||||
*/
|
||||
addPrefix: (prefix: string) => (path: any) => {
|
||||
path.basename = prefix + path.basename;
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加后缀
|
||||
* @param suffix 后缀
|
||||
*/
|
||||
addSuffix: (suffix: string) => (path: any) => {
|
||||
path.basename = path.basename + suffix;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更改扩展名
|
||||
* @param ext 新扩展名(包含点)
|
||||
*/
|
||||
changeExtension: (ext: string) => (path: any) => {
|
||||
path.extname = ext;
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加时间戳
|
||||
*/
|
||||
addTimestamp: () => (path: any) => {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
path.basename = `${path.basename}-${timestamp}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 转换为小写
|
||||
*/
|
||||
toLowerCase: () => (path: any) => {
|
||||
path.basename = path.basename.toLowerCase();
|
||||
},
|
||||
|
||||
/**
|
||||
* 转换为大写
|
||||
*/
|
||||
toUpperCase: () => (path: any) => {
|
||||
path.basename = path.basename.toUpperCase();
|
||||
},
|
||||
|
||||
/**
|
||||
* 替换文件名中的字符
|
||||
* @param search 要替换的字符串或正则
|
||||
* @param replace 替换为的字符串
|
||||
*/
|
||||
replaceInName: (search: string | RegExp, replace: string) => (path: any) => {
|
||||
path.basename = path.basename.replace(search, replace);
|
||||
}
|
||||
};
|
||||
185
frontend/packages/gulp-build-tools/src/modules/replace.ts
Normal file
185
frontend/packages/gulp-build-tools/src/modules/replace.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { src, dest, TaskFunction } from 'gulp';
|
||||
import gulpReplace from 'gulp-replace';
|
||||
import { ReplaceConfig, TaskResult } from '../types.js';
|
||||
import chalk from 'chalk';
|
||||
import through2 from 'through2';
|
||||
import { Transform } from 'stream';
|
||||
|
||||
/**
|
||||
* 创建文件内容替换任务
|
||||
* @param config 替换配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createReplaceTask(config: ReplaceConfig): TaskFunction {
|
||||
return function replaceContent(cb) {
|
||||
let fileCount = 0;
|
||||
let replaceCount = 0;
|
||||
|
||||
console.log(chalk.blue('🔄 开始替换文件内容...'));
|
||||
console.log(chalk.gray(`源路径: ${Array.isArray(config.src) ? config.src.join(', ') : config.src}`));
|
||||
console.log(chalk.gray(`目标路径: ${config.dest}`));
|
||||
console.log(chalk.gray(`替换规则数量: ${config.replacements.length}`));
|
||||
|
||||
let stream: any = src(config.src, { allowEmpty: true })
|
||||
.pipe(through2.obj(function(file, enc, callback) {
|
||||
fileCount++;
|
||||
console.log(chalk.yellow(`处理文件: ${file.relative}`));
|
||||
this.push(file);
|
||||
callback();
|
||||
}));
|
||||
|
||||
// 应用所有替换规则
|
||||
for (const replacement of config.replacements) {
|
||||
const { search, replace } = replacement;
|
||||
|
||||
stream = (stream as any).pipe(gulpReplace(search, (match, ...args) => {
|
||||
replaceCount++;
|
||||
console.log(chalk.cyan(`替换内容: ${match.substring(0, 50)}${match.length > 50 ? '...' : ''}`));
|
||||
|
||||
if (typeof replace === 'function') {
|
||||
return replace(match, ...args);
|
||||
}
|
||||
return replace;
|
||||
}));
|
||||
}
|
||||
|
||||
stream = (stream as any).pipe(dest(config.dest));
|
||||
|
||||
stream.on('end', () => {
|
||||
console.log(chalk.green(`✅ 内容替换完成,共处理 ${fileCount} 个文件,执行 ${replaceCount} 次替换`));
|
||||
cb();
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error(chalk.red('❌ 内容替换失败:'), error);
|
||||
cb(error);
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量替换文件内容
|
||||
* @param configs 替换配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function batchReplace(configs: ReplaceConfig[]): Promise<TaskResult[]> {
|
||||
const results: TaskResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createReplaceTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
message: `内容替换成功: ${config.dest}`
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `内容替换失败: ${config.dest}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 常用替换模式
|
||||
*/
|
||||
export const replacePatterns = {
|
||||
/**
|
||||
* 替换版本号
|
||||
* @param newVersion 新版本号
|
||||
*/
|
||||
version: (newVersion: string) => ({
|
||||
search: /"version"\s*:\s*"[^"]+"/g,
|
||||
replace: `"version": "${newVersion}"`
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换 API 基础 URL
|
||||
* @param newBaseUrl 新的基础 URL
|
||||
*/
|
||||
apiBaseUrl: (newBaseUrl: string) => ({
|
||||
search: /const\s+API_BASE_URL\s*=\s*['"][^'"]+['"]/g,
|
||||
replace: `const API_BASE_URL = '${newBaseUrl}'`
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换环境变量
|
||||
* @param envVar 环境变量名
|
||||
* @param value 新值
|
||||
*/
|
||||
envVariable: (envVar: string, value: string) => ({
|
||||
search: new RegExp(`${envVar}\\s*=\\s*[^\\n\\r]+`, 'g'),
|
||||
replace: `${envVar}=${value}`
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换 HTML 中的标题
|
||||
* @param newTitle 新标题
|
||||
*/
|
||||
htmlTitle: (newTitle: string) => ({
|
||||
search: /<title>[^<]*<\/title>/gi,
|
||||
replace: `<title>${newTitle}</title>`
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换注释中的版权信息
|
||||
* @param newCopyright 新版权信息
|
||||
*/
|
||||
copyright: (newCopyright: string) => ({
|
||||
search: /\/\*\*[\s\S]*?Copyright[\s\S]*?\*\//g,
|
||||
replace: `/**\n * ${newCopyright}\n */`
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换时间戳
|
||||
*/
|
||||
timestamp: () => ({
|
||||
search: /\{\{TIMESTAMP\}\}/g,
|
||||
replace: new Date().toISOString()
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换构建号
|
||||
* @param buildNumber 构建号
|
||||
*/
|
||||
buildNumber: (buildNumber: string) => ({
|
||||
search: /BUILD_NUMBER\s*=\s*['"][^'"]*['"]/g,
|
||||
replace: `BUILD_NUMBER = '${buildNumber}'`
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换 CSS 中的颜色值
|
||||
* @param oldColor 旧颜色值
|
||||
* @param newColor 新颜色值
|
||||
*/
|
||||
cssColor: (oldColor: string, newColor: string) => ({
|
||||
search: new RegExp(oldColor.replace('#', '\\#'), 'gi'),
|
||||
replace: newColor
|
||||
}),
|
||||
|
||||
/**
|
||||
* 替换 JavaScript 中的配置对象
|
||||
* @param configKey 配置键名
|
||||
* @param newValue 新值
|
||||
*/
|
||||
jsConfig: (configKey: string, newValue: any) => ({
|
||||
search: new RegExp(`${configKey}\\s*:\\s*[^,}]+`, 'g'),
|
||||
replace: `${configKey}: ${JSON.stringify(newValue)}`
|
||||
})
|
||||
};
|
||||
448
frontend/packages/gulp-build-tools/src/modules/ssh.ts
Normal file
448
frontend/packages/gulp-build-tools/src/modules/ssh.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
import { TaskFunction } from 'gulp';
|
||||
import { SSHConfig, TaskResult } from '../types.js';
|
||||
import chalk from 'chalk';
|
||||
import { Client } from 'ssh2';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
* 创建 SSH 命令执行任务
|
||||
* @param config SSH 配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createSSHTask(config: SSHConfig): TaskFunction {
|
||||
return function sshExecution(cb) {
|
||||
const conn = new Client();
|
||||
let commandCount = 0;
|
||||
const commands = Array.isArray(config.commands) ? config.commands : [config.commands];
|
||||
|
||||
console.log(chalk.blue('🔗 开始 SSH 连接...'));
|
||||
console.log(chalk.gray(`服务器: ${config.host}:${config.port || 22}`));
|
||||
console.log(chalk.gray(`用户: ${config.username}`));
|
||||
console.log(chalk.gray(`命令数量: ${commands.length}`));
|
||||
|
||||
conn.on('ready', async () => {
|
||||
console.log(chalk.green('✅ SSH 连接成功'));
|
||||
|
||||
try {
|
||||
for (const command of commands) {
|
||||
await executeCommand(conn, command, config.verbose || false);
|
||||
commandCount++;
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ SSH 命令执行完成,共执行 ${commandCount} 个命令`));
|
||||
conn.end();
|
||||
cb();
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ SSH 命令执行失败:'), error);
|
||||
conn.end();
|
||||
cb(error);
|
||||
}
|
||||
});
|
||||
|
||||
conn.on('error', (error) => {
|
||||
console.error(chalk.red('❌ SSH 连接失败:'), error);
|
||||
cb(error);
|
||||
});
|
||||
|
||||
conn.on('end', () => {
|
||||
console.log(chalk.blue('🔌 SSH 连接已断开'));
|
||||
});
|
||||
|
||||
// 准备连接配置
|
||||
const connectConfig: any = {
|
||||
host: config.host,
|
||||
port: config.port || 22,
|
||||
username: config.username,
|
||||
};
|
||||
|
||||
if (config.password) {
|
||||
connectConfig.password = config.password;
|
||||
} else if (config.privateKey) {
|
||||
try {
|
||||
connectConfig.privateKey = fs.readFileSync(config.privateKey);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 读取私钥文件失败:'), error);
|
||||
cb(error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red('❌ 缺少密码或私钥'));
|
||||
cb(new Error('缺少密码或私钥'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
conn.connect(connectConfig);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个命令
|
||||
* @param conn SSH 连接
|
||||
* @param command 要执行的命令
|
||||
* @param verbose 是否显示详细输出
|
||||
*/
|
||||
function executeCommand(conn: Client, command: string, verbose: boolean): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(chalk.yellow(`📝 执行命令: ${command}`));
|
||||
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let code = 0;
|
||||
|
||||
stream.on('close', (exitCode: number, signal: string) => {
|
||||
code = exitCode;
|
||||
|
||||
if (exitCode === 0) {
|
||||
console.log(chalk.green(`✅ 命令执行成功 (退出码: ${exitCode})`));
|
||||
} else {
|
||||
console.log(chalk.red(`❌ 命令执行失败 (退出码: ${exitCode})`));
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
console.log(chalk.yellow(`收到信号: ${signal}`));
|
||||
}
|
||||
|
||||
resolve({ stdout, stderr, code });
|
||||
});
|
||||
|
||||
stream.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
stdout += output;
|
||||
|
||||
if (verbose) {
|
||||
console.log(chalk.gray('[输出]'), output.trim());
|
||||
}
|
||||
});
|
||||
|
||||
stream.stderr.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
stderr += output;
|
||||
|
||||
if (verbose) {
|
||||
console.log(chalk.red('[错误]'), output.trim());
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行 SSH 命令
|
||||
* @param configs SSH 配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function batchSSHExecution(configs: SSHConfig[]): Promise<TaskResult[]> {
|
||||
const results: TaskResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createSSHTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
message: `SSH 命令执行成功: ${config.host}`,
|
||||
fileCount: Array.isArray(config.commands) ? config.commands.length : 1
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `SSH 命令执行失败: ${config.host}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行执行 SSH 命令
|
||||
* @param configs SSH 配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function parallelSSHExecution(configs: SSHConfig[]): Promise<TaskResult[]> {
|
||||
console.log(chalk.blue(`🔗 开始并行连接 ${configs.length} 个服务器...`));
|
||||
|
||||
const promises = configs.map(async (config, index) => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createSSHTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `SSH 执行成功 [${index + 1}]: ${config.host}`,
|
||||
fileCount: Array.isArray(config.commands) ? config.commands.length : 1
|
||||
} as TaskResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `SSH 执行失败 [${index + 1}]: ${config.host}`
|
||||
} as TaskResult;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(chalk.green(`✅ 并行 SSH 执行完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多 SSH 任务
|
||||
* @param configs SSH 配置数组
|
||||
* @param parallel 是否并行执行
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createMultiSSHTask(configs: SSHConfig[], parallel: boolean = false): TaskFunction {
|
||||
return async function multiSSHExecution(cb) {
|
||||
try {
|
||||
console.log(chalk.blue(`🔗 开始${parallel ? '并行' : '串行'}执行 ${configs.length} 个 SSH 任务...`));
|
||||
|
||||
// 验证所有配置
|
||||
for (const config of configs) {
|
||||
const errors = sshHelpers.validateConfig(config);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`SSH 配置错误 (${config.host}): ${errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
let results: TaskResult[];
|
||||
|
||||
if (parallel) {
|
||||
results = await parallelSSHExecution(configs);
|
||||
} else {
|
||||
results = await batchSSHExecution(configs);
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
const totalCommands = results.reduce((sum, r) => sum + (r.fileCount || 0), 0);
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.log(chalk.yellow(`⚠️ 部分 SSH 执行失败:`));
|
||||
results.filter(r => !r.success).forEach(result => {
|
||||
console.log(chalk.red(` - ${result.message}`));
|
||||
});
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ 多 SSH 任务完成: ${successCount} 服务器成功, ${failureCount} 失败, 共执行 ${totalCommands} 个命令`));
|
||||
|
||||
if (failureCount > 0 && successCount === 0) {
|
||||
cb(new Error('所有 SSH 任务都失败了'));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 多 SSH 任务执行失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SSH 工具函数
|
||||
*/
|
||||
export const sshHelpers = {
|
||||
/**
|
||||
* 测试 SSH 连接
|
||||
* @param config SSH 配置(不包含命令)
|
||||
*/
|
||||
testConnection: (config: Omit<SSHConfig, 'commands'>): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
|
||||
conn.on('ready', () => {
|
||||
console.log(chalk.green('✅ SSH 连接测试成功'));
|
||||
conn.end();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
conn.on('error', (error) => {
|
||||
console.error(chalk.red('❌ SSH 连接测试失败:'), error);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
const connectConfig: any = {
|
||||
host: config.host,
|
||||
port: config.port || 22,
|
||||
username: config.username,
|
||||
};
|
||||
|
||||
if (config.password) {
|
||||
connectConfig.password = config.password;
|
||||
} else if (config.privateKey) {
|
||||
try {
|
||||
connectConfig.privateKey = fs.readFileSync(config.privateKey);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 读取私钥文件失败:'), error);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
conn.connect(connectConfig);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行单个命令并返回结果
|
||||
* @param config SSH 配置
|
||||
* @param command 单个命令
|
||||
*/
|
||||
executeCommand: async (config: Omit<SSHConfig, 'commands'>, command: string): Promise<{ stdout: string; stderr: string; code: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = new Client();
|
||||
|
||||
conn.on('ready', () => {
|
||||
executeCommand(conn, command, config.verbose || false)
|
||||
.then(result => {
|
||||
conn.end();
|
||||
resolve(result);
|
||||
})
|
||||
.catch(error => {
|
||||
conn.end();
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
conn.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
const connectConfig: any = {
|
||||
host: config.host,
|
||||
port: config.port || 22,
|
||||
username: config.username,
|
||||
};
|
||||
|
||||
if (config.password) {
|
||||
connectConfig.password = config.password;
|
||||
} else if (config.privateKey) {
|
||||
try {
|
||||
connectConfig.privateKey = fs.readFileSync(config.privateKey);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
conn.connect(connectConfig);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建常用的命令模板
|
||||
*/
|
||||
commands: {
|
||||
/**
|
||||
* 重启服务
|
||||
* @param serviceName 服务名称
|
||||
*/
|
||||
restartService: (serviceName: string) => `sudo systemctl restart ${serviceName}`,
|
||||
|
||||
/**
|
||||
* 检查服务状态
|
||||
* @param serviceName 服务名称
|
||||
*/
|
||||
checkService: (serviceName: string) => `sudo systemctl status ${serviceName}`,
|
||||
|
||||
/**
|
||||
* 部署应用
|
||||
* @param appPath 应用路径
|
||||
*/
|
||||
deployApp: (appPath: string) => [
|
||||
`cd ${appPath}`,
|
||||
'git pull origin main',
|
||||
'npm install',
|
||||
'npm run build',
|
||||
'pm2 restart all'
|
||||
],
|
||||
|
||||
/**
|
||||
* 清理日志
|
||||
* @param logPath 日志路径
|
||||
* @param days 保留天数
|
||||
*/
|
||||
cleanLogs: (logPath: string, days: number = 7) =>
|
||||
`find ${logPath} -name "*.log" -mtime +${days} -delete`,
|
||||
|
||||
/**
|
||||
* 备份数据库
|
||||
* @param dbName 数据库名
|
||||
* @param backupPath 备份路径
|
||||
*/
|
||||
backupDatabase: (dbName: string, backupPath: string) =>
|
||||
`mysqldump -u root -p ${dbName} > ${backupPath}/${dbName}_$(date +%Y%m%d_%H%M%S).sql`,
|
||||
|
||||
/**
|
||||
* 检查磁盘空间
|
||||
*/
|
||||
checkDiskSpace: () => 'df -h',
|
||||
|
||||
/**
|
||||
* 检查内存使用
|
||||
*/
|
||||
checkMemory: () => 'free -h',
|
||||
|
||||
/**
|
||||
* 检查 CPU 使用
|
||||
*/
|
||||
checkCPU: () => 'top -bn1 | grep "Cpu(s)"',
|
||||
|
||||
/**
|
||||
* 检查进程
|
||||
* @param processName 进程名称
|
||||
*/
|
||||
checkProcess: (processName: string) => `ps aux | grep ${processName}`
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证 SSH 配置
|
||||
* @param config SSH 配置
|
||||
*/
|
||||
validateConfig: (config: SSHConfig): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.host) errors.push('缺少服务器主机地址');
|
||||
if (!config.username) errors.push('缺少用户名');
|
||||
if (!config.password && !config.privateKey) errors.push('缺少密码或私钥');
|
||||
if (!config.commands || (Array.isArray(config.commands) && config.commands.length === 0)) {
|
||||
errors.push('缺少要执行的命令');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
418
frontend/packages/gulp-build-tools/src/modules/upload.ts
Normal file
418
frontend/packages/gulp-build-tools/src/modules/upload.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { src, TaskFunction } from 'gulp';
|
||||
import { UploadConfig, TaskResult } from '../types.js';
|
||||
import chalk from 'chalk';
|
||||
import through2 from 'through2';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// 动态导入以避免 CommonJS 模块问题
|
||||
let SftpClient: any;
|
||||
let ftp: any;
|
||||
|
||||
async function loadDependencies() {
|
||||
if (!SftpClient) {
|
||||
SftpClient = (await import('ssh2-sftp-client')).default;
|
||||
}
|
||||
if (!ftp) {
|
||||
ftp = await import('basic-ftp');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SFTP 上传任务
|
||||
* @param config 上传配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createSftpUploadTask(config: UploadConfig): TaskFunction {
|
||||
return async function sftpUpload(cb) {
|
||||
try {
|
||||
await loadDependencies();
|
||||
|
||||
const sftp = new SftpClient();
|
||||
let fileCount = 0;
|
||||
|
||||
console.log(chalk.blue('🚀 开始 SFTP 上传...'));
|
||||
console.log(chalk.gray(`服务器: ${config.host}:${config.port || 22}`));
|
||||
console.log(chalk.gray(`用户: ${config.username}`));
|
||||
console.log(chalk.gray(`远程路径: ${config.remotePath}`));
|
||||
|
||||
// 连接 SFTP
|
||||
const connectConfig: any = {
|
||||
host: config.host,
|
||||
port: config.port || 22,
|
||||
username: config.username
|
||||
};
|
||||
|
||||
if (config.password) {
|
||||
connectConfig.password = config.password;
|
||||
} else if (config.privateKey) {
|
||||
connectConfig.privateKey = fs.readFileSync(config.privateKey);
|
||||
}
|
||||
|
||||
await sftp.connect(connectConfig);
|
||||
console.log(chalk.green('✅ SFTP 连接成功'));
|
||||
|
||||
// 确保远程目录存在
|
||||
await sftp.mkdir(config.remotePath, true);
|
||||
|
||||
if (config.clean) {
|
||||
console.log(chalk.yellow('🧹 清空远程目录...'));
|
||||
await sftp.rmdir(config.remotePath, true);
|
||||
await sftp.mkdir(config.remotePath, true);
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
const uploadPromises: Promise<void>[] = [];
|
||||
|
||||
const stream = src(config.src, { allowEmpty: true })
|
||||
.pipe(through2.obj(function(file, enc, callback) {
|
||||
if (!file.isBuffer()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fileCount++;
|
||||
const relativePath = file.relative;
|
||||
const remotePath = path.posix.join(config.remotePath, relativePath).replace(/\\/g, '/');
|
||||
|
||||
console.log(chalk.yellow(`上传文件: ${relativePath} -> ${remotePath}`));
|
||||
|
||||
const uploadPromise = (async () => {
|
||||
try {
|
||||
// 确保远程目录存在
|
||||
const remoteDir = path.posix.dirname(remotePath);
|
||||
await sftp.mkdir(remoteDir, true);
|
||||
|
||||
// 上传文件
|
||||
await sftp.put(file.contents, remotePath);
|
||||
console.log(chalk.green(`✅ 上传成功: ${relativePath}`));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`❌ 上传失败: ${relativePath}`), error);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
if (config.parallel) {
|
||||
uploadPromises.push(uploadPromise);
|
||||
} else {
|
||||
uploadPromise.then(() => callback()).catch(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
}));
|
||||
|
||||
stream.on('end', async () => {
|
||||
try {
|
||||
if (config.parallel && uploadPromises.length > 0) {
|
||||
await Promise.all(uploadPromises);
|
||||
}
|
||||
|
||||
await sftp.end();
|
||||
console.log(chalk.green(`✅ SFTP 上传完成,共上传 ${fileCount} 个文件`));
|
||||
cb();
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ SFTP 上传失败:'), error);
|
||||
await sftp.end();
|
||||
cb(error);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', async (error) => {
|
||||
console.error(chalk.red('❌ SFTP 上传流错误:'), error);
|
||||
await sftp.end();
|
||||
cb(error);
|
||||
});
|
||||
|
||||
return stream;
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ SFTP 连接失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 FTP 上传任务
|
||||
* @param config 上传配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createFtpUploadTask(config: UploadConfig): TaskFunction {
|
||||
return async function ftpUpload(cb) {
|
||||
try {
|
||||
await loadDependencies();
|
||||
|
||||
let fileCount = 0;
|
||||
|
||||
console.log(chalk.blue('🚀 开始 FTP 上传...'));
|
||||
console.log(chalk.gray(`服务器: ${config.host}:${config.port || 21}`));
|
||||
console.log(chalk.gray(`用户: ${config.username}`));
|
||||
console.log(chalk.gray(`远程路径: ${config.remotePath}`));
|
||||
|
||||
const client = new ftp.Client();
|
||||
|
||||
try {
|
||||
await client.access({
|
||||
host: config.host,
|
||||
port: config.port || 21,
|
||||
user: config.username,
|
||||
password: config.password
|
||||
});
|
||||
|
||||
console.log(chalk.green('✅ FTP 连接成功'));
|
||||
|
||||
if (config.clean) {
|
||||
console.log(chalk.yellow('🧹 清空远程目录...'));
|
||||
try {
|
||||
await client.removeDir(config.remotePath);
|
||||
} catch (error) {
|
||||
// 忽略删除失败的错误
|
||||
}
|
||||
}
|
||||
|
||||
// 确保远程目录存在
|
||||
await client.ensureDir(config.remotePath);
|
||||
|
||||
const stream = src(config.src, { allowEmpty: true })
|
||||
.pipe(through2.obj(async function(file, enc, callback) {
|
||||
if (!file.isBuffer()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fileCount++;
|
||||
const relativePath = file.relative;
|
||||
const remotePath = path.posix.join(config.remotePath, relativePath).replace(/\\/g, '/');
|
||||
|
||||
console.log(chalk.yellow(`上传文件: ${relativePath} -> ${remotePath}`));
|
||||
|
||||
try {
|
||||
// 确保远程目录存在
|
||||
const remoteDir = path.posix.dirname(remotePath);
|
||||
await client.ensureDir(remoteDir);
|
||||
|
||||
// 上传文件
|
||||
await client.uploadFrom(file.contents, remotePath);
|
||||
console.log(chalk.green(`✅ 上传成功: ${relativePath}`));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`❌ 上传失败: ${relativePath}`), error);
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
}));
|
||||
|
||||
stream.on('end', async () => {
|
||||
try {
|
||||
client.close();
|
||||
console.log(chalk.green(`✅ FTP 上传完成,共上传 ${fileCount} 个文件`));
|
||||
cb();
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ FTP 上传失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error(chalk.red('❌ FTP 上传流错误:'), error);
|
||||
client.close();
|
||||
cb(error);
|
||||
});
|
||||
|
||||
return stream;
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ FTP 连接失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ FTP 初始化失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建上传任务(根据配置自动选择 FTP 或 SFTP)
|
||||
* @param config 上传配置
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createUploadTask(config: UploadConfig): TaskFunction {
|
||||
if (config.type === 'sftp') {
|
||||
return createSftpUploadTask(config);
|
||||
} else {
|
||||
return createFtpUploadTask(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件
|
||||
* @param configs 上传配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function batchUpload(configs: UploadConfig[]): Promise<TaskResult[]> {
|
||||
const results: TaskResult[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createUploadTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
message: `文件上传成功: ${config.host}:${config.remotePath}`,
|
||||
fileCount: 1
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `文件上传失败: ${config.host}:${config.remotePath}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行上传文件
|
||||
* @param configs 上传配置数组
|
||||
* @returns Promise<TaskResult[]>
|
||||
*/
|
||||
export async function parallelUpload(configs: UploadConfig[]): Promise<TaskResult[]> {
|
||||
console.log(chalk.blue(`🚀 开始并行上传到 ${configs.length} 个服务器...`));
|
||||
|
||||
const promises = configs.map(async (config, index) => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const task = createUploadTask(config);
|
||||
task((error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `上传成功 [${index + 1}]: ${config.host}:${config.remotePath}`,
|
||||
fileCount: 1
|
||||
} as TaskResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error as Error,
|
||||
message: `上传失败 [${index + 1}]: ${config.host}:${config.remotePath}`
|
||||
} as TaskResult;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(chalk.green(`✅ 并行上传完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多上传任务
|
||||
* @param configs 上传配置数组
|
||||
* @param parallel 是否并行执行
|
||||
* @returns Gulp 任务函数
|
||||
*/
|
||||
export function createMultiUploadTask(configs: UploadConfig[], parallel: boolean = false): TaskFunction {
|
||||
return async function multiUpload(cb) {
|
||||
try {
|
||||
console.log(chalk.blue(`🚀 开始${parallel ? '并行' : '串行'}上传到 ${configs.length} 个目标...`));
|
||||
|
||||
// 验证所有配置
|
||||
for (const config of configs) {
|
||||
const errors = uploadHelpers.validateConfig(config);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`上传配置错误 (${config.host}): ${errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
let results: TaskResult[];
|
||||
|
||||
if (parallel) {
|
||||
results = await parallelUpload(configs);
|
||||
} else {
|
||||
results = await batchUpload(configs);
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failureCount = results.filter(r => !r.success).length;
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.log(chalk.yellow(`⚠️ 部分上传失败:`));
|
||||
results.filter(r => !r.success).forEach(result => {
|
||||
console.log(chalk.red(` - ${result.message}`));
|
||||
});
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ 多目标上传完成: ${successCount} 成功, ${failureCount} 失败`));
|
||||
|
||||
if (failureCount > 0 && successCount === 0) {
|
||||
cb(new Error('所有上传任务都失败了'));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ 多目标上传失败:'), error);
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传工具函数
|
||||
*/
|
||||
export const uploadHelpers = {
|
||||
/**
|
||||
* 创建上传配置
|
||||
* @param baseConfig 基础配置
|
||||
* @param overrides 覆盖配置
|
||||
*/
|
||||
createConfig: (baseConfig: Partial<UploadConfig>, overrides: Partial<UploadConfig> = {}): UploadConfig => {
|
||||
return {
|
||||
type: 'sftp',
|
||||
port: baseConfig.type === 'ftp' ? 21 : 22,
|
||||
parallel: true,
|
||||
clean: false,
|
||||
...baseConfig,
|
||||
...overrides
|
||||
} as UploadConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证上传配置
|
||||
* @param config 上传配置
|
||||
*/
|
||||
validateConfig: (config: UploadConfig): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.host) errors.push('缺少服务器主机地址');
|
||||
if (!config.username) errors.push('缺少用户名');
|
||||
if (!config.password && !config.privateKey) errors.push('缺少密码或私钥');
|
||||
if (!config.remotePath) errors.push('缺少远程路径');
|
||||
if (!config.src) errors.push('缺少源文件路径');
|
||||
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user