diff --git a/app/command/Backup.php b/app/command/Backup.php new file mode 100644 index 0000000..dc8ecd8 --- /dev/null +++ b/app/command/Backup.php @@ -0,0 +1,752 @@ + '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' => false, + 'dockerContainerName' => 'redis' // Docker 容器名称 + ] + ]; + + 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['dockerContainerName'] ?? ucfirst($source['type']); + $output->writeln("- {$name}: " . ($source['useDocker'] ? "Docker 容器" : "本地环境")); + } + + // 处理命令行选项 + if ($backup) { + if ($source === 'mongodb') { + $this->backupMongoDB($mongoDir, $output); + } elseif ($source === 'mysql') { + $this->backupMySQL($mysqlDir, $output); + } else { + // 备份所有数据库 + $this->backupMongoDB($mongoDir, $output); + $this->backupMySQL($mysqlDir, $output); + } + } elseif ($restore) { + if ($source === 'mongodb') { + $this->restoreMongoDB($mongoDir, $output); + } elseif ($source === 'mysql') { + $this->restoreMySQL($mysqlDir, $output); + } 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['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); + } elseif ($source['type'] === 'mysql') { + $mysqlDir = base_path($outputDir) . '/mysql'; + if (!is_dir($mysqlDir)) { + mkdir($mysqlDir, 0755, true); + } + $this->backupMySQL($mysqlDir, $output); + } + } 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['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); + } elseif ($source['type'] === 'mysql') { + $mysqlDir = base_path($outputDir) . '/mysql'; + if (!is_dir($mysqlDir)) { + mkdir($mysqlDir, 0755, true); + } + $this->restoreMySQL($mysqlDir, $output); + } + } else { + $output->writeln("\n无效选择,请重新输入"); + } + } + + private function backupMongoDB($backupDir, $output): void + { + $output->writeln("\n开始备份 MongoDB..."); + + try { + // 从数据源配置中获取 MongoDB 配置 + $mongoSource = null; + 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']; + + // 生成备份文件名 + $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); + $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): void + { + $output->writeln("\n开始还原 MongoDB..."); + + try { + // 从数据源配置中获取 MongoDB 配置 + $mongoSource = null; + 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']; + + // 列出备份文件 + $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) { + // 构建还原命令 + $restoreDir = $tempDir; + $cmd = $this->getMongoRestoreCommand($host, $port, $database, $restoreDir, $useDocker, $dockerContainerName); + $output->writeln("执行命令: {$cmd}"); + + // 执行还原命令 + exec($cmd, $outputLines, $returnCode); + + if ($returnCode === 0) { + $output->writeln("✅ MongoDB 还原成功"); + } 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): void + { + $output->writeln("\n开始备份 MySQL..."); + + try { + // 从数据源配置中获取 MySQL 配置 + $mysqlSource = null; + 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): void + { + $output->writeln("\n开始还原 MySQL..."); + + try { + // 从数据源配置中获取 MySQL 配置 + $mysqlSource = null; + 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 && $redisSource['useDocker']) { + // 使用 Docker 容器 + $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 连接 + $redis = new \Redis(); + $config = config('cache.stores.redis'); + $host = $config['host'] ?? '127.0.0.1'; + $port = $config['port'] ?? 6379; + $password = $config['password'] ?? ''; + + $output->writeln("连接 Redis: {$host}:{$port}"); + + if ($redis->connect($host, $port)) { + if (!empty($password)) { + $redis->auth($password); + } + + // 清空 Redis + $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): string + { + // 获取 MongoDB 认证信息 + $config = config('thinkorm.connections.immongodb'); + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + $authSource = $config['authSource'] ?? $database; + + // 构建认证参数 + $authParams = ''; + if (!empty($username) && !empty($password)) { + $authParams = "--username {$username} --password {$password} --authenticationDatabase {$authSource}"; + } + + if ($useDocker) { + // 使用 Docker 容器 + 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 { + // 不使用 Docker,直接使用本地命令 + return "mongodump --host {$host}:{$port} {$authParams} --db {$database} --out {$outputDir}"; + } + } + + private function getMongoRestoreCommand($host, $port, $database, $restoreDir, $useDocker = false, $dockerContainerName = null): string + { + // 获取 MongoDB 认证信息 + $config = config('thinkorm.connections.immongodb'); + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + $authSource = $config['authSource'] ?? $database; + + // 构建认证参数 + $authParams = ''; + if (!empty($username) && !empty($password)) { + $authParams = "--username {$username} --password {$password} --authenticationDatabase {$authSource}"; + } + + if ($useDocker) { + // 使用 Docker 容器 + if (!$dockerContainerName) { + return "echo '错误:未指定 Docker 容器名称' && exit 1"; + } + return "docker cp {$restoreDir} {$dockerContainerName}:/tmp/mongo_restore && docker exec -it {$dockerContainerName} mongorestore --host {$host}:{$port} {$authParams} --db {$database} /tmp/mongo_restore"; + } else { + // 不使用 Docker,直接使用本地命令 + 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}"; + } + } +} \ No newline at end of file