'mongodb', 'host' => '127.0.0.1', 'port' => 27017, 'database' => 'openim_v3', 'username' => 'openIM', 'password' => 'n1e5a6s6m7', 'useDocker' => true, 'dockerContainerName' => 'mongo' // Docker 容器名称 ], [ 'type' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'imadmin', 'username' => 'root', 'password' => 'n1e5a6s6m7', 'useDocker' => true, 'dockerContainerName' => 'my_mysql' // Docker 容器名称 ], [ 'type' => 'redis', 'host' => '127.0.0.1', 'port' => 16379, 'database' => 0, 'username' => '', 'password' => 'n1e5a6s6m7', 'useDocker' => true, 'dockerContainerName' => 'redis' // Docker 容器名称 ], [ 'name' => 'tettt_mongodb', 'type' => 'mongodb', 'host' => '127.0.0.1', 'port' => 27017, 'database' => 'tettt', 'username' => 'commie', 'password' => 'n1e5a6s6m7', 'authSource' => 'admin', 'useDocker' => true, 'dockerContainerName' => 'mongo' ], ]; protected function configure() { $this->addOption('backup', 'b', InputOption::VALUE_NONE, '备份数据库'); $this->addOption('restore', 'r', InputOption::VALUE_NONE, '还原数据库'); $this->addOption('clear', 'c', InputOption::VALUE_NONE, '清空 Redis'); $this->addOption('source', 's', InputOption::VALUE_OPTIONAL, '数据源名称 (mongodb, mysql, redis)'); $this->addOption('output', 'o', InputOption::VALUE_OPTIONAL, '备份输出目录', '/backup'); } protected function execute(InputInterface $input, OutputInterface $output): int { $backup = $input->getOption('backup'); $restore = $input->getOption('restore'); $clear = $input->getOption('clear'); $source = $input->getOption('source'); $outputDir = $input->getOption('output'); // 确保备份目录存在 $mongoDir = base_path($outputDir) . '/mongo'; $mysqlDir = base_path($outputDir) . '/mysql'; if (!is_dir($mongoDir)) { mkdir($mongoDir, 0755, true); } if (!is_dir($mysqlDir)) { mkdir($mysqlDir, 0755, true); } // 显示备份目录 $output->writeln("\n备份目录:"); $output->writeln("- MongoDB: {$mongoDir}"); $output->writeln("- MySQL: {$mysqlDir}"); // 显示环境配置 $output->writeln("\n环境配置:"); foreach ($this->dataSources as $_source) { $name = $_source['name'] ?? $_source['dockerContainerName'] ?? ucfirst($_source['type']); if($name == $source){ $source = $_source; } $output->writeln("- {$name}: " . ($_source['useDocker'] ? "Docker 容器" : "本地环境")); } // 处理命令行选项 if ($backup) { if(is_array($source)){ if ($source['type'] === 'mongodb') { $this->backupMongoDB($mongoDir, $output, $source); } if ($source['type'] === 'mysql') { $this->backupMySQL($mysqlDir, $output, $source); } } else { foreach ($this->dataSources as $_source) { if ($_source['type'] === 'mongodb') { $this->backupMongoDB($mongoDir, $output, $_source); } elseif ($_source['type'] === 'mysql') { $this->backupMySQL($mysqlDir, $output, $_source); } } } } elseif ($restore) { if(is_array($source)){ if ($source['type'] === 'mongodb') { $this->restoreMongoDB($mongoDir, $output, $source); } if ($source['type'] === 'mysql') { $this->restoreMySQL($mysqlDir, $output, $source); } } else { $this->restoreMenu($output, $outputDir); } } elseif ($clear) { $this->clearRedis($output); } else { $this->mainMenu($output, $outputDir); } $output->writeln("\n✅ 操作完成!"); return self::SUCCESS; } /** * 主菜单 */ private function mainMenu($output, $outputDir): void { while (true) { $output->writeln("\n================================"); $output->writeln(" 备份工具"); $output->writeln("================================"); $output->writeln("1. 备份数据库"); $output->writeln("2. 还原数据库"); $output->writeln("3. 清空 Redis"); $output->writeln("0. 退出"); $output->write("\n请选择操作 (0-3): "); $handle = fopen("php://stdin", "r"); $choice = fgets($handle); fclose($handle); $choice = trim($choice); switch ($choice) { case '1': $this->backupMenu($output, $outputDir); break; case '2': $this->restoreMenu($output, $outputDir); break; case '3': $this->clearRedis($output); break; case '0': return; default: $output->writeln("\n无效选择,请重新输入"); } } } /** * 备份菜单 */ private function backupMenu($output, $outputDir): void { $output->writeln("\n================================"); $output->writeln(" 备份数据库"); $output->writeln("================================"); foreach ($this->dataSources as $index => $source) { if ($source['type'] !== 'redis') { $name = $source['name'] ?? $source['dockerContainerName'] ?? ucfirst($source['type']); $output->writeln(($index + 1) . ". " . $name . " (" . ($source['useDocker'] ? "Docker 容器" : "本地环境") . ")"); } } $output->writeln("0. 返回上一级"); $output->write("\n请选择要备份的数据源 (0-" . count($this->dataSources) . "): "); $handle = fopen("php://stdin", "r"); $choice = fgets($handle); fclose($handle); $choice = trim($choice); if ($choice === '0') { return; } if (is_numeric($choice) && $choice > 0 && $choice <= count($this->dataSources)) { $source = $this->dataSources[$choice - 1]; if ($source['type'] === 'mongodb') { $mongoDir = base_path($outputDir) . '/mongo'; if (!is_dir($mongoDir)) { mkdir($mongoDir, 0755, true); } $this->backupMongoDB($mongoDir, $output, $source); } elseif ($source['type'] === 'mysql') { $mysqlDir = base_path($outputDir) . '/mysql'; if (!is_dir($mysqlDir)) { mkdir($mysqlDir, 0755, true); } $this->backupMySQL($mysqlDir, $output, $source); } } else { $output->writeln("\n无效选择,请重新输入"); } } /** * 还原菜单 */ private function restoreMenu($output, $outputDir): void { $output->writeln("\n================================"); $output->writeln(" 还原数据库"); $output->writeln("================================"); foreach ($this->dataSources as $index => $source) { if ($source['type'] !== 'redis') { $name = $source['name'] ?? $source['dockerContainerName'] ?? ucfirst($source['type']); $output->writeln(($index + 1) . ". " . $name . " (" . ($source['useDocker'] ? "Docker 容器" : "本地环境") . ")"); } } $output->writeln("0. 返回上一级"); $output->write("\n请选择要还原的数据源 (0-" . count($this->dataSources) . "): "); $handle = fopen("php://stdin", "r"); $choice = fgets($handle); fclose($handle); $choice = trim($choice); if ($choice === '0') { return; } if (is_numeric($choice) && $choice > 0 && $choice <= count($this->dataSources)) { $source = $this->dataSources[$choice - 1]; if ($source['type'] === 'mongodb') { $mongoDir = base_path($outputDir) . '/mongo'; if (!is_dir($mongoDir)) { mkdir($mongoDir, 0755, true); } $this->restoreMongoDB($mongoDir, $output, $source); } elseif ($source['type'] === 'mysql') { $mysqlDir = base_path($outputDir) . '/mysql'; if (!is_dir($mysqlDir)) { mkdir($mysqlDir, 0755, true); } $this->restoreMySQL($mysqlDir, $output, $source); } } else { $output->writeln("\n无效选择,请重新输入"); } } private function backupMongoDB($backupDir, $output, $dataSource = null): void { $name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']); $output->writeln("\n开始备份 {$name}..."); try { $mongoSource = $dataSource; if (!$mongoSource) { foreach ($this->dataSources as $source) { if ($source['type'] === 'mongodb') { $mongoSource = $source; break; } } } if (!$mongoSource) { $output->writeln("❌ 未找到 MongoDB 数据源配置"); return; } $host = $mongoSource['host']; $port = $mongoSource['port']; $database = $mongoSource['database']; $useDocker = $mongoSource['useDocker']; $dockerContainerName = $mongoSource['dockerContainerName']; $username = $mongoSource['username'] ?? ''; $password = $mongoSource['password'] ?? ''; $authSource = $mongoSource['authSource'] ?? $database; $username = $mongoSource['username'] ?? ''; $password = $mongoSource['password'] ?? ''; $authSource = $mongoSource['authSource'] ?? $database; $backupFileName = "{$database}_" . date("Y_m_d_H_i_s") . ".zip"; $backupFilePath = "{$backupDir}/{$backupFileName}"; $tempDir = "/tmp/mongo_backup_" . uniqid(); if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } $cmd = $this->getMongoDumpCommand($host, $port, $database, $tempDir, $useDocker, $dockerContainerName, $username, $password, $authSource); $output->writeln("执行命令: {$cmd}"); exec($cmd, $outputLines, $returnCode); if ($returnCode === 0) { // 保存当前工作目录 $currentDir = getcwd(); // 切换到临时目录并压缩 chdir($tempDir); $zipCmd = "zip -r {$backupFilePath} ."; $output->writeln("创建压缩文件: {$backupFilePath}"); exec($zipCmd, $zipOutput, $zipReturnCode); // 切换回原来的工作目录 chdir($currentDir); if ($zipReturnCode === 0) { $output->writeln("✅ MongoDB 备份成功: {$backupFilePath}"); } else { $output->writeln("❌ MongoDB 压缩失败"); $output->writeln(implode("\n", $zipOutput)); } // 清理临时目录 exec("rm -rf {$tempDir}"); } else { $output->writeln("❌ MongoDB 备份失败"); $output->writeln(implode("\n", $outputLines)); // 清理临时目录 exec("rm -rf {$tempDir}"); } } catch (\Exception $e) { $output->writeln("❌ MongoDB 备份失败: " . $e->getMessage()); } } private function restoreMongoDB($backupDir, $output, $dataSource = null): void { $name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']); $output->writeln("\n开始还原 {$name}..."); try { $mongoSource = $dataSource; if (!$mongoSource) { foreach ($this->dataSources as $source) { if ($source['type'] === 'mongodb') { $mongoSource = $source; break; } } } if (!$mongoSource) { $output->writeln("❌ 未找到 MongoDB 数据源配置"); return; } $host = $mongoSource['host']; $port = $mongoSource['port']; $database = $mongoSource['database']; $useDocker = $mongoSource['useDocker']; $dockerContainerName = $mongoSource['dockerContainerName']; $username = $mongoSource['username'] ?? ''; $password = $mongoSource['password'] ?? ''; $authSource = $mongoSource['authSource'] ?? $database; $backupFiles = glob("{$backupDir}/*.zip"); if (empty($backupFiles)) { $output->writeln("❌ 未找到备份文件"); return; } // 按修改时间排序 usort($backupFiles, function ($a, $b) { return filemtime($b) - filemtime($a); }); // 显示备份文件列表 $output->writeln("\n可用的备份文件:"); foreach ($backupFiles as $index => $file) { $fileName = basename($file); $fileSize = filesize($file) / 1024 / 1024; $modTime = date("Y-m-d H:i:s", filemtime($file)); $output->writeln(($index + 1) . ". {$fileName} (" . round($fileSize, 2) . " MB, {$modTime})"); } // 选择备份文件 $output->write("\n请选择要还原的备份文件 (1-" . count($backupFiles) . "): "); $handle = fopen("php://stdin", "r"); $choice = fgets($handle); fclose($handle); $choice = trim($choice); if (!is_numeric($choice) || $choice < 1 || $choice > count($backupFiles)) { $output->writeln("\n无效选择"); return; } $selectedFile = $backupFiles[$choice - 1]; $output->writeln("\n选择的备份文件: " . basename($selectedFile)); // 生成临时还原目录 $tempDir = "/tmp/mongo_restore_" . uniqid(); if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } // 解压备份文件 $unzipCmd = "unzip {$selectedFile} -d {$tempDir}"; $output->writeln("解压备份文件..."); exec($unzipCmd, $unzipOutput, $unzipReturnCode); if ($unzipReturnCode === 0) { $dbRestoreDir = $tempDir; $backupDbName = null; $subDirs = glob("{$tempDir}/*", GLOB_ONLYDIR); $output->writeln("解压后的目录: " . implode(", ", array_map('basename', $subDirs))); if (!empty($subDirs)) { foreach ($subDirs as $subDir) { $bsonFiles = glob("{$subDir}/*.bson"); if (!empty($bsonFiles)) { $dbRestoreDir = $subDir; $backupDbName = basename($subDir); $output->writeln("找到备份目录: {$backupDbName}"); break; } } } $bsonFiles = glob("{$dbRestoreDir}/*.bson"); if (empty($bsonFiles)) { $output->writeln("❌ 备份文件中没有找到 BSON 数据文件"); $output->writeln("目录内容: " . implode(", ", scandir($dbRestoreDir))); exec("rm -rf {$tempDir}"); return; } $output->writeln("找到 " . count($bsonFiles) . " 个 BSON 文件"); $output->writeln("还原目录: {$dbRestoreDir}"); $output->writeln("备份数据库: {$backupDbName} -> 目标数据库: {$database}"); $cmd = $this->getMongoRestoreCommand($host, $port, $database, $dbRestoreDir, $useDocker, $dockerContainerName, $username, $password, $authSource); $output->writeln("执行命令: {$cmd}"); exec($cmd, $outputLines, $returnCode); if ($returnCode === 0) { $output->writeln("✅ MongoDB 还原成功"); if (!empty($outputLines)) { $output->writeln(implode("\n", $outputLines)); } } else { $output->writeln("❌ MongoDB 还原失败"); $output->writeln(implode("\n", $outputLines)); } exec("rm -rf {$tempDir}"); } else { $output->writeln("❌ 解压备份文件失败"); $output->writeln(implode("\n", $unzipOutput)); exec("rm -rf {$tempDir}"); } } catch (\Exception $e) { $output->writeln("❌ MongoDB 还原失败: " . $e->getMessage()); } } private function backupMySQL($backupDir, $output, $dataSource = null): void { $name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']); $output->writeln("\n开始备份 {$name}..."); try { $mysqlSource = $dataSource; if (!$mysqlSource) { foreach ($this->dataSources as $source) { if ($source['type'] === 'mysql') { $mysqlSource = $source; break; } } } if (!$mysqlSource) { $output->writeln("❌ 未找到 MySQL 数据源配置"); return; } $host = $mysqlSource['host']; $port = $mysqlSource['port']; $database = $mysqlSource['database']; $username = $mysqlSource['username']; $password = $mysqlSource['password']; $useDocker = $mysqlSource['useDocker']; $dockerContainerName = $mysqlSource['dockerContainerName']; // 生成备份文件名 $backupFileName = "{$database}_" . date("Y_m_d_H_i_s") . ".sql"; $backupFilePath = "{$backupDir}/{$backupFileName}"; // 构建备份命令 $cmd = $this->getMySqlDumpCommand($host, $port, $database, $username, $password, $backupFilePath, $useDocker, $dockerContainerName); $output->writeln("执行命令: {$cmd}"); // 执行备份命令 exec($cmd, $outputLines, $returnCode); if ($returnCode === 0) { $output->writeln("✅ MySQL 备份成功: {$backupFilePath}"); } else { $output->writeln("❌ MySQL 备份失败"); $output->writeln(implode("\n", $outputLines)); } } catch (\Exception $e) { $output->writeln("❌ MySQL 备份失败: " . $e->getMessage()); } } private function restoreMySQL($backupDir, $output, $dataSource = null): void { $name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']); $output->writeln("\n开始还原 {$name}..."); try { $mysqlSource = $dataSource; if (!$mysqlSource) { foreach ($this->dataSources as $source) { if ($source['type'] === 'mysql') { $mysqlSource = $source; break; } } } if (!$mysqlSource) { $output->writeln("❌ 未找到 MySQL 数据源配置"); return; } $host = $mysqlSource['host']; $port = $mysqlSource['port']; $database = $mysqlSource['database']; $username = $mysqlSource['username']; $password = $mysqlSource['password']; $useDocker = $mysqlSource['useDocker']; $dockerContainerName = $mysqlSource['dockerContainerName']; // 列出备份文件(支持 SQL 文件和 zip 文件) $backupFiles = array_merge( glob("{$backupDir}/*.sql"), glob("{$backupDir}/*.zip") ); if (empty($backupFiles)) { $output->writeln("❌ 未找到备份文件"); return; } // 按修改时间排序 usort($backupFiles, function ($a, $b) { return filemtime($b) - filemtime($a); }); // 显示备份文件列表 $output->writeln("\n可用的备份文件:"); foreach ($backupFiles as $index => $file) { $fileName = basename($file); $fileSize = filesize($file) / 1024 / 1024; $modTime = date("Y-m-d H:i:s", filemtime($file)); $output->writeln(($index + 1) . ". {$fileName} (" . round($fileSize, 2) . " MB, {$modTime})"); } // 选择备份文件 $output->write("\n请选择要还原的备份文件 (1-" . count($backupFiles) . "): "); $handle = fopen("php://stdin", "r"); $choice = fgets($handle); fclose($handle); $choice = trim($choice); if (!is_numeric($choice) || $choice < 1 || $choice > count($backupFiles)) { $output->writeln("\n无效选择"); return; } $selectedFile = $backupFiles[$choice - 1]; $output->writeln("\n选择的备份文件: " . basename($selectedFile)); $sqlFile = $selectedFile; // 如果是 zip 文件,需要解压 if (pathinfo($selectedFile, PATHINFO_EXTENSION) === 'zip') { // 生成临时还原目录 $tempDir = "/tmp/mysql_restore_" . uniqid(); if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } // 解压备份文件 $unzipCmd = "unzip {$selectedFile} -d {$tempDir}"; $output->writeln("解压备份文件..."); exec($unzipCmd, $unzipOutput, $unzipReturnCode); if ($unzipReturnCode !== 0) { $output->writeln("❌ 解压备份文件失败"); $output->writeln(implode("\n", $unzipOutput)); // 清理临时目录 exec("rm -rf {$tempDir}"); return; } // 找到解压后的 SQL 文件 $sqlFiles = glob("{$tempDir}/*.sql"); if (empty($sqlFiles)) { $output->writeln("❌ 未找到 SQL 文件"); // 清理临时目录 exec("rm -rf {$tempDir}"); return; } $sqlFile = $sqlFiles[0]; } // 构建还原命令 $cmd = $this->getMySqlRestoreCommand($host, $port, $database, $username, $password, $sqlFile, $useDocker, $dockerContainerName); $output->writeln("执行命令: {$cmd}"); // 执行还原命令 exec($cmd, $outputLines, $returnCode); if ($returnCode === 0) { $output->writeln("✅ MySQL 还原成功"); } else { $output->writeln("❌ MySQL 还原失败"); $output->writeln(implode("\n", $outputLines)); } // 清理临时目录(如果使用了临时目录) if (pathinfo($selectedFile, PATHINFO_EXTENSION) === 'zip') { exec("rm -rf {$tempDir}"); } } catch (\Exception $e) { $output->writeln("❌ MySQL 还原失败: " . $e->getMessage()); } } private function clearRedis($output): void { $output->writeln("\n开始清空 Redis..."); try { // 从数据源配置中获取 Redis 配置 $redisSource = null; foreach ($this->dataSources as $source) { if ($source['type'] === 'redis') { $redisSource = $source; break; } } if (!$redisSource) { $output->writeln("❌ 未找到 Redis 数据源配置"); return; } if ($redisSource['useDocker']) { $redisContainer = $redisSource['dockerContainerName']; if (!$redisContainer) { $output->writeln("❌ 未指定 Redis Docker 容器名称"); return; } $output->writeln("使用 Docker 容器清空 Redis"); $cmd = "docker exec -it {$redisContainer} redis-cli flushall"; $output->writeln("执行命令: {$cmd}"); exec($cmd, $outputLines, $returnCode); if ($returnCode === 0) { $output->writeln("✅ Redis 清空成功"); } else { $output->writeln("❌ Redis 清空失败"); $output->writeln(implode("\n", $outputLines)); } } else { $redis = new \Redis(); $host = $redisSource['host'] ?? '127.0.0.1'; $port = $redisSource['port'] ?? 6379; $password = $redisSource['password'] ?? ''; $output->writeln("连接 Redis: {$host}:{$port}"); if ($redis->connect($host, $port)) { if (!empty($password)) { $redis->auth($password); } $result = $redis->flushAll(); if ($result) { $output->writeln("✅ Redis 清空成功"); } else { $output->writeln("❌ Redis 清空失败"); } } else { $output->writeln("❌ 无法连接到 Redis"); } } } catch (\Exception $e) { $output->writeln("❌ Redis 操作失败: " . $e->getMessage()); } } private function getMongoDumpCommand($host, $port, $database, $outputDir, $useDocker = false, $dockerContainerName = null, $username = '', $password = '', $authSource = null): string { if ($authSource === null) { $authSource = $database; } $authParams = ''; if (!empty($username) && !empty($password)) { $authParams = "--username {$username} --password {$password} --authenticationDatabase {$authSource}"; } if ($useDocker) { if (!$dockerContainerName) { return "echo '错误:未指定 Docker 容器名称' && exit 1"; } $port = 27017; return "docker exec -it {$dockerContainerName} mongodump --host {$host}:{$port} {$authParams} --db {$database} --out /tmp/mongo_backup && docker cp {$dockerContainerName}:/tmp/mongo_backup/{$database} {$outputDir}/"; } else { return "mongodump --host {$host}:{$port} {$authParams} --db {$database} --out {$outputDir}"; } } private function getMongoRestoreCommand($host, $port, $database, $restoreDir, $useDocker = false, $dockerContainerName = null, $username = '', $password = '', $authSource = null): string { if ($authSource === null) { $authSource = $database; } $authParams = ''; if (!empty($username) && !empty($password)) { $authParams = "--username {$username} --password {$password} --authenticationDatabase {$authSource}"; } if ($useDocker) { if (!$dockerContainerName) { return "echo '错误:未指定 Docker 容器名称' && exit 1"; } $dirName = basename($restoreDir); return "docker cp {$restoreDir} {$dockerContainerName}:/tmp/ && docker exec -it {$dockerContainerName} mongorestore --host {$host}:{$port} {$authParams} --db {$database} /tmp/{$dirName} && docker exec -it {$dockerContainerName} rm -rf /tmp/{$dirName}"; } else { return "mongorestore --host {$host}:{$port} {$authParams} --db {$database} {$restoreDir}"; } } private function getMySqlDumpCommand($host, $port, $database, $username, $password, $outputFile, $useDocker = false, $dockerContainerName = null): string { if ($useDocker) { // 使用 Docker 容器 if (!$dockerContainerName) { return "echo '错误:未指定 Docker 容器名称' && exit 1"; } return "docker exec -it {$dockerContainerName} mysqldump -h {$host} -P {$port} -u {$username} --password={$password} {$database} > {$outputFile}"; } else { // 不使用 Docker,直接使用本地命令 return "mysqldump -h {$host} -P {$port} -u {$username} --password={$password} {$database} > {$outputFile}"; } } private function getMySqlRestoreCommand($host, $port, $database, $username, $password, $sqlFile, $useDocker = false, $dockerContainerName = null): string { if ($useDocker) { // 使用 Docker 容器 if (!$dockerContainerName) { return "echo '错误:未指定 Docker 容器名称' && exit 1"; } return "docker cp {$sqlFile} {$dockerContainerName}:/tmp/mysql_restore.sql && docker exec -it {$dockerContainerName} bash -c 'mysql -h {$host} -P {$port} -u {$username} --password={$password} {$database} < /tmp/mysql_restore.sql'"; } else { // 不使用 Docker,直接使用本地命令 return "mysql -h {$host} -P {$port} -u {$username} --password={$password} {$database} < {$sqlFile}"; } } }