Commit c305821b authored by platyhouse's avatar platyhouse

# MySQL 도구 추가 및 ptyMysqlBackup 리팩토링

## 신규 파일 추가

### MySQL 설정 로더 (ptyLibrary_PHP/mysql/ptyMysqlConfig.php)
- MySQL 공통 설정 로더 클래스 추가
- ~/.ptyMysqlConfig.ini 파일 기반 설정 관리
- getSections(), load(), connect() 메서드 제공
- 설정 파일 예시를 반환하는 getConfigExample() 메서드 포함

### MySQL 정보 조회 도구 (ptyMysqlInfo.php)
- MySQL 서버 정보, 상태, 데이터베이스 목록, 사용자 목록 등 조회
- ANSI 색상 코드를 활용한 가독성 높은 출력
- formatBytes(), formatUptime(), padLabel() 유틸리티 함수 포함
- 시스템 DB와 사용자 DB 구분 표시

### MySQL 복원 도구 (ptyMysqlRestore.php)
- SQL 백업 파일을 MySQL로 복원하는 CLI 도구
- --dry-run 옵션으로 실제 실행 전 미리보기 지원
- --force 옵션으로 확인 없이 바로 실행 가능
- 대화형 실행 확인 기능 포함

## 기존 파일 수정

### ptyMysqlBackup.php 리팩토링
- platyFramework 네임스페이스 적용
- ptyCliOptionParser, ptyMysqlConfig 라이브러리 사용으로 전환
- 기존 하드코딩된 설정을 INI 파일 기반으로 변경
- 데이터베이스/테이블 선택적 백업 지원 (* 와일드카드)
- --output 옵션으로 출력 디렉토리 지정 가능
- 백업 파일에 실행 정보 주석 추가
- mysqldump 에러와 stdout 분리 처리
parent acc2c1ef
<?php
/**
* ptyMysqlConfig
*
* MySQL 공통 설정 로더
* 설정 파일: ~/.ptyMysqlConfig.ini
*/
namespace platyFramework;
/**
* MySQL 설정 로더 클래스
*/
class ptyMysqlConfig
{
private static $configPath = null;
/**
* 설정 파일 경로 반환
*/
public static function getConfigPath()
{
if (self::$configPath === null) {
self::$configPath = getenv('HOME') . '/.ptyMysqlConfig.ini';
}
return self::$configPath;
}
/**
* 설정 파일에서 섹션 목록 조회
*/
public static function getSections()
{
$configPath = self::getConfigPath();
if (!file_exists($configPath)) {
return [];
}
$config = parse_ini_file($configPath, true);
return $config ? array_keys($config) : [];
}
/**
* MySQL 설정 로드
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return array 설정 배열 [host, username, password, database, charset]
* @throws \Exception 설정 파일이나 섹션이 없을 경우
*/
public static function load($section = 'default')
{
$configPath = self::getConfigPath();
if (!file_exists($configPath)) {
throw new \Exception("MySQL 설정 파일을 찾을 수 없습니다: {$configPath}\n\n" . self::getConfigExample());
}
$config = parse_ini_file($configPath, true);
if ($config === false) {
throw new \Exception("MySQL 설정 파일을 파싱할 수 없습니다: {$configPath}");
}
if (!isset($config[$section])) {
$availableSections = implode(', ', array_keys($config));
throw new \Exception("MySQL 설정에서 [{$section}] 섹션을 찾을 수 없습니다.\n사용 가능한 섹션: {$availableSections}");
}
$sectionConfig = $config[$section];
// 필수 필드 검증
if (!isset($sectionConfig['host']) || empty($sectionConfig['host'])) {
throw new \Exception("MySQL 설정 [{$section}] 섹션에 필수 필드 'host'가 없습니다.");
}
return [
'host' => $sectionConfig['host'],
'username' => $sectionConfig['username'] ?? 'root',
'password' => isset($sectionConfig['password']) ? trim($sectionConfig['password'], '"\'') : null,
'database' => $sectionConfig['database'] ?? null,
'charset' => $sectionConfig['charset'] ?? 'utf8mb4',
];
}
/**
* mysqli 연결 생성
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return array [connection => mysqli, config => array]
* @throws \Exception 연결 실패 시
*/
public static function connect($section = 'default')
{
$config = self::load($section);
$connection = mysqli_connect(
$config['host'],
$config['username'],
$config['password'],
$config['database']
);
if (!$connection) {
throw new \Exception("MySQL 연결 실패: " . mysqli_connect_error());
}
if (!empty($config['charset'])) {
mysqli_set_charset($connection, $config['charset']);
}
return [
'connection' => $connection,
'config' => $config,
];
}
/**
* 설정 파일 예시 반환
*/
public static function getConfigExample()
{
return <<<EOT
설정 파일 예시 (~/.ptyMysqlConfig.ini):
[default]
host=localhost
username=root
password="your_password"
database=your_db
charset=utf8mb4
[production]
host=prod-mysql.example.com
username=admin
password="production_password"
database=prod_db
charset=utf8mb4
EOT;
}
}
#!/usr/bin/php
#!/usr/bin/env php
<?php
/**
* ptyMysqlBackup
*
* MySQL 데이터베이스/테이블 백업 도구
* 설정 파일: ~/.ptyMysqlConfig.ini
*
* Usage: ./ptyMysqlBackup <database> <table> [options]
* ./ptyMysqlBackup * * (전체 백업)
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/mysql/ptyMysqlConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$mysqlSection = $options['mysql'] ?? 'default';
$verbose = isset($options['verbose']);
$outputDir = $options['output'] ?? '.';
// 도움말 또는 필수 인자 확인
if (count($positionalArgs) < 2 || isset($options['help'])) {
echo "사용법: {$argv[0]} <database> <table> [옵션]\n";
echo "\n";
echo "인자:\n";
echo " <database> 데이터베이스명 (* = 전체)\n";
echo " <table> 테이블명 (* = 전체)\n";
echo "\n";
echo "옵션:\n";
echo " --mysql=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --output=경로 출력 디렉토리 (기본값: 현재 디렉토리)\n";
echo " --verbose 상세 로그 출력\n";
echo " --help 도움말 출력\n";
echo "\n";
echo "예시:\n";
echo " {$argv[0]} mydb users # mydb.users.sql 생성\n";
echo " {$argv[0]} mydb * # mydb의 모든 테이블 백업\n";
echo " {$argv[0]} * * # 전체 백업\n";
echo " {$argv[0]} mydb users --output=/backup # /backup/mydb.users.sql 생성\n";
echo " {$argv[0]} mydb users --mysql=production # production 섹션 사용\n";
echo "\n";
echo "기본 mysqldump 옵션:\n";
echo " --default-character-set=utf8mb4\n";
echo " --routines --events\n";
echo " --add-drop-table --add-drop-database\n";
echo " --complete-insert --extended-insert=TRUE\n";
echo " --single-transaction --ssl-mode=DISABLED\n";
echo " --max-allowed-packet=1G\n";
echo "\n";
echo "설정 파일: ~/.ptyMysqlConfig.ini\n";
echo ptyMysqlConfig::getConfigExample() . "\n";
exit(isset($options['help']) ? 0 : 1);
}
$dbPattern = $positionalArgs[0];
$tablePattern = $positionalArgs[1];
// 원본 명령어 저장 (백업 파일 주석용)
$originalCommand = implode(' ', $argv);
// 타임스탬프를 포함한 출력 함수
function logMessage($message) {
// 타임스탬프 로그 함수
function logMessage($message, $verbose = false, $isVerbose = false) {
if ($isVerbose && !$verbose) {
return;
}
$timestamp = date('Y-m-d H:i:s');
echo "[$timestamp] $message";
echo "[$timestamp] $message\n";
}
// 인자 파싱 함수 (--key=value 형태만 허용)
function parseArguments($argv) {
$args = [];
for ($i = 1; $i < count($argv); $i++) {
if ($argv[$i] === '--help') {
$args['help'] = true;
} elseif (preg_match('/^--([a-zA-Z]+)=(.*)$/', $argv[$i], $matches)) {
$args[$matches[1]] = $matches[2];
} else {
logMessage("[WARNING] Invalid argument format: {$argv[$i]}\n");
logMessage(" Use --key=value format (e.g., --host=localhost)\n\n");
// mysqldump 명령 생성
function getMysqldumpCmd($host, $user, $password, $dbName, $tableName = null) {
$cmd = "mysqldump";
$cmd .= " -h " . escapeshellarg($host);
$cmd .= " -u " . escapeshellarg($user);
$cmd .= " -p" . escapeshellarg($password);
$cmd .= " --default-character-set=utf8mb4";
$cmd .= " --routines --events";
$cmd .= " --add-drop-table --add-drop-database";
$cmd .= " --complete-insert --extended-insert=TRUE";
$cmd .= " --single-transaction";
$cmd .= " --ssl-mode=DISABLED";
$cmd .= " --max-allowed-packet=1G";
$cmd .= " " . escapeshellarg($dbName);
if ($tableName) {
$cmd .= " " . escapeshellarg($tableName);
}
}
return $args;
}
$args = parseArguments($argv);
// --help 옵션 체크
if (isset($args['help'])) {
echo "pty_mysql_backup_all_each_db_each_table version 2.0\n";
echo "Copyright (C) 2019 by cpueblo, PlatyHouse Co.,LTD.\n";
echo "Web site: https://www.platyhouse.com/\n\n";
echo "Usage: php ./ptyMysqlBackup.php [OPTIONS]\n\n";
echo "Options:\n";
echo " --host=<hostname> MySQL host (default: localhost)\n";
echo " --username=<user> MySQL username (default: root)\n";
echo " --password=<password> MySQL password (optional if ~/.mysql_password exists)\n";
echo " --backupPath=<path> Backup directory path (default: /root/pty_server_backup/mysql)\n";
echo " --help Show this help message\n\n";
echo "Examples:\n";
echo " php ./ptyMysqlBackup.php --host=localhost --username=root --password=mypass\n";
echo " php ./ptyMysqlBackup.php --host=192.168.1.100 --username=admin\n";
echo " php ./ptyMysqlBackup.php --host=remotehost --backupPath=/backup/mysql\n\n";
echo "Note:\n";
echo " - You can create ~/.mysql_password file with password (local or remote host)\n";
echo " - If password is not provided, script will try to read from ~/.mysql_password\n";
echo " - Each database backup will be compressed to .tgz format\n";
exit(0);
return $cmd;
}
$host = $args['host'] ?? null;
$user = $args['username'] ?? null;
$password = $args['password'] ?? null;
$backupPath = $args['backupPath'] ?? null;
if (!$host) $host = "localhost";
if (!$user) $user = "root";
if (!$backupPath) $backupPath = "/root/pty_server_backup/mysql";
// 패스워드가 없으면 ~/.mysql_password 파일에서 읽기 시도
if (!$password)
{
// 먼저 로컬 ~/.mysql_password 체크
logMessage("[INFO] Checking local ~/.mysql_password file...\n");
$passwordFile = getenv("HOME") . "/.mysql_password";
if (file_exists($passwordFile)) {
$password = trim(file_get_contents($passwordFile));
logMessage("[INFO] Password loaded from local ~/.mysql_password\n");
} else {
logMessage("[INFO] Local ~/.mysql_password not found\n");
// 백업 실행 함수
function backupTable($host, $user, $password, $dbName, $tableName, $outputDir, $verbose, $originalCommand) {
$outputFile = rtrim($outputDir, '/') . "/{$dbName}.{$tableName}.sql";
$errorFile = sys_get_temp_dir() . "/mysqldump_err_" . getmypid() . ".txt";
// 백업 정보 주석 생성
$backupTime = date('Y-m-d H:i:s');
$header = "-- ============================================\n";
$header .= "-- 백업 시간: $backupTime\n";
$header .= "-- 실행 명령: $originalCommand\n";
$header .= "-- 데이터베이스: $dbName\n";
$header .= "-- 테이블: $tableName\n";
$header .= "-- ============================================\n\n";
// 주석을 먼저 파일에 작성
file_put_contents($outputFile, $header);
$cmd = getMysqldumpCmd($host, $user, $password, $dbName, $tableName);
// stdout은 SQL 파일에 append, stderr는 임시 파일로 분리
$cmd .= " >> " . escapeshellarg($outputFile) . " 2>" . escapeshellarg($errorFile);
if ($verbose) {
logMessage("명령: $cmd", $verbose, true);
}
// 로컬에 없고 호스트가 localhost가 아니면 SSH로 원격 호스트에서 가져오기
if (!$password && $host != "localhost" && $host != "127.0.0.1") {
logMessage("[INFO] Trying to get password from remote host ($host)...\n");
$sshCmd = "ssh root@$host 'cat ~/.mysql_password' 2>/dev/null";
$remotePassword = trim(shell_exec($sshCmd));
if ($remotePassword) {
$password = $remotePassword;
logMessage("[INFO] Password loaded from remote host = $password\n");
} else {
logMessage("[INFO] Remote ~/.mysql_password not found\n");
exec($cmd, $output, $returnCode);
// 에러 확인
$errorOutput = file_exists($errorFile) ? trim(file_get_contents($errorFile)) : '';
@unlink($errorFile);
if ($returnCode !== 0) {
logMessage("ERROR: {$dbName}.{$tableName} 백업 실패", true);
if ($errorOutput) {
logMessage($errorOutput, true);
}
return false;
}
}
if (!$password)
{
echo "[ERROR] Password is required!\n\n";
echo "pty_mysql_backup_all_each_db_each_table version 2.0\n";
echo "Copyright (C) 2019 by cpueblo, PlatyHouse Co.,LTD.\n";
echo "Web site: https://www.platyhouse.com/\n\n";
echo "Usage: php ./ptyMysqlBackup.php [OPTIONS]\n\n";
echo "Options:\n";
echo " --host=<hostname> MySQL host (default: localhost)\n";
echo " --username=<user> MySQL username (default: root)\n";
echo " --password=<password> MySQL password (optional if ~/.mysql_password exists)\n";
echo " --backupPath=<path> Backup directory path (default: /root/pty_server_backup/mysql)\n";
echo " --help Show this help message\n\n";
echo "Examples:\n";
echo " php ./ptyMysqlBackup.php --host=localhost --username=root --password=mypass\n";
echo " php ./ptyMysqlBackup.php --host=192.168.1.100 --username=admin\n";
echo " php ./ptyMysqlBackup.php --host=remotehost --backupPath=/backup/mysql\n\n";
echo "Note:\n";
echo " - You can create ~/.mysql_password file with password (local or remote host)\n";
echo " - If password is not provided, script will try to read from ~/.mysql_password\n";
echo " - Each database backup will be compressed to .tgz format\n";
exit(1);
}
// 파일 크기 확인
if (file_exists($outputFile)) {
$size = filesize($outputFile);
$sizeStr = formatBytes($size);
logMessage("OK: {$dbName}.{$tableName}.sql ($sizeStr)", true);
}
return true;
}
// 백업 디렉토리 생성
if (!is_dir($backupPath)) {
logMessage("[INFO] Creating backup directory: $backupPath\n");
if (!mkdir($backupPath, 0755, true)) {
logMessage("[ERROR] Failed to create backup directory: $backupPath\n");
exit(1);
// 바이트를 읽기 쉬운 형식으로 변환
function formatBytes($bytes) {
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' bytes';
}
}
logMessage("\n[INFO] Backup path: $backupPath\n");
logMessage("[INFO] Connecting to MySQL server: $host as $user, password = $password\n");
try {
// MySQL 연결
$conn = ptyMysqlConfig::connect($mysqlSection);
$connection = $conn['connection'];
$config = $conn['config'];
logMessage("MySQL 백업 시작", true);
logMessage("Host: {$config['host']}", true);
logMessage("User: {$config['username']}", true);
logMessage("Output: $outputDir", true);
echo str_repeat("=", 60) . "\n";
// 출력 디렉토리 생성
if (!is_dir($outputDir)) {
if (!mkdir($outputDir, 0755, true)) {
throw new \Exception("출력 디렉토리 생성 실패: $outputDir");
}
}
// 시스템 DB 제외 목록
$systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
$connection = mysqli_connect($host, $user, $password);
if (!$connection) {
logMessage('[ERROR] Could not connect database.' . mysqli_error()."\n");
exit(1);
}
logMessage("[INFO] Connected successfully!\n\n");
logMessage("[INFO] Retrieving database list...\n");
$databases = mysqli_query($connection, "SHOW databases");
$dbCount = 0;
$totalTables = 0;
while ($databaseInfo = mysqli_fetch_array($databases)) { // go through each row that was returned in $result
$dbName = $databaseInfo[0];
# performance_schema, information_schema 는 백업 무시
if ($dbName == "performance_schema" || $dbName == "information_schema") {
logMessage("[SKIP] Skipping system database: $dbName\n");
continue;
$totalBackups = 0;
$successBackups = 0;
// 데이터베이스 목록 조회
if ($dbPattern === '*') {
$dbResult = mysqli_query($connection, "SHOW DATABASES");
$databases = [];
while ($row = mysqli_fetch_row($dbResult)) {
if (!in_array($row[0], $systemDbs)) {
$databases[] = $row[0];
}
}
} else {
$databases = [$dbPattern];
}
$dbCount++;
logMessage("\n====================================\n");
logMessage("[DB $dbCount] Processing database: $dbName\n");
logMessage("====================================\n");
foreach ($databases as $dbName) {
// x_ 접두사 DB 제외
if (strpos($dbName, 'x_') === 0) {
logMessage("SKIP: $dbName (x_ prefix)", true);
continue;
}
$sql = "SHOW TABLES FROM `$dbName`";
$result = mysqli_query($connection, $sql);
logMessage("", true);
logMessage("Database: $dbName", true);
echo str_repeat("-", 40) . "\n";
if (!$result) {
logMessage("[ERROR] DB Error, could not list tables\n");
logMessage('[ERROR] MySQL Error: ' . mysqli_error($connection) . "\n");
exit;
// 테이블 목록 조회
if ($tablePattern === '*') {
$tableResult = mysqli_query($connection, "SHOW TABLES FROM `$dbName`");
if (!$tableResult) {
logMessage("ERROR: $dbName 테이블 조회 실패", true);
continue;
}
$tableCount = 0;
$tables = [];
while ($row = mysqli_fetch_row($result)) {
while ($row = mysqli_fetch_row($tableResult)) {
$tables[] = $row[0];
}
$totalTablesInDb = count($tables);
logMessage("[INFO] Found $totalTablesInDb tables in $dbName\n");
foreach ($tables as $tableName) {
$tableCount++;
logMessage("[TABLE $tableCount/$totalTablesInDb] Backing up: $dbName.$tableName ... ");
mysql_backup($host, $user, $password, $dbName, $tableName, $backupPath);
logMessage("Done\n");
$totalTables++;
}
// 데이터베이스 백업 완료 후 압축
logMessage("[INFO] Compressing database backup...\n");
$dbDir = $backupPath . "/" . $dbName;
$tarFile = $backupPath . "/" . $dbName . ".tgz";
// tar 명령 실행 (백업 경로로 이동해서 실행)
$tarCmd = "cd \"$backupPath\" && tar czf \"$dbName.tgz\" \"$dbName\" 2>&1";
exec($tarCmd, $tarOutput, $tarReturnCode);
if ($tarReturnCode === 0) {
logMessage("[INFO] Compressed to: $tarFile\n");
// 압축 성공 시 원본 디렉토리 삭제
logMessage("[INFO] Removing original directory...\n");
$rmCmd = "rm -rf \"$dbDir\"";
exec($rmCmd);
logMessage("[INFO] Original directory removed\n");
} else {
logMessage("[ERROR] Failed to compress $dbName\n");
logMessage(implode("\n", $tarOutput) . "\n");
$tables = [$tablePattern];
}
}
logMessage("\n====================================\n");
logMessage("[COMPLETE] Backup finished!\n");
logMessage("[STATS] Backup path: $backupPath\n");
logMessage("[STATS] Total databases: $dbCount\n");
logMessage("[STATS] Total tables: $totalTables\n");
logMessage("====================================\n");
function mysql_backup($host, $user, $password, $dbName, $tableName, $backupPath)
{
if (strpos($dbName, "x_") === 0) {
echo "(skipped: x_ prefix) ";
return;
foreach ($tables as $tableName) {
$totalBackups++;
if (backupTable($config['host'], $config['username'], $config['password'], $dbName, $tableName, $outputDir, $verbose, $originalCommand)) {
$successBackups++;
}
}
// 데이터베이스별 디렉토리 생성
$dbDir = $backupPath . "/" . $dbName;
if (!is_dir($dbDir)) {
@mkdir($dbDir, 0755, true);
}
$outputFile = $dbDir . "/" . $tableName . ".sql";
$cmd = "mysqldump -u $user -p$password -h $host $dbName $tableName --default-character-set=utf8mb4 --routines --events --add-drop-table --add-drop-database --complete-insert --extended-insert=TRUE --single-transaction --ssl-mode=DISABLED --max-allowed-packet=1G > \"$outputFile\" 2>&1";
mysqli_close($connection);
// 명령 실행 (출력 억제)
exec($cmd, $output, $returnCode);
echo "\n" . str_repeat("=", 60) . "\n";
logMessage("백업 완료: $successBackups / $totalBackups", true);
if ($returnCode !== 0) {
logMessage("\n[ERROR] Failed to backup $dbName.$tableName\n");
logMessage(implode("\n", $output) . "\n");
if ($successBackups < $totalBackups) {
exit(1);
}
}
?>
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
#!/usr/bin/env php
<?php
/**
* ptyMysqlInfo
*
* MySQL 서버 정보 조회 도구
* 설정 파일: ~/.ptyMysqlConfig.ini
*
* Usage: ./ptyMysqlInfo [options]
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/mysql/ptyMysqlConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$mysqlSection = $options['mysql'] ?? 'default';
// 도움말
if (isset($options['help'])) {
echo "사용법: {$argv[0]} [옵션]\n";
echo "\n";
echo "옵션:\n";
echo " --mysql=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --help 도움말 출력\n";
echo "\n";
echo "예시:\n";
echo " {$argv[0]} # default 섹션 사용\n";
echo " {$argv[0]} --mysql=production # production 섹션 사용\n";
echo "\n";
echo "설정 파일: ~/.ptyMysqlConfig.ini\n";
echo ptyMysqlConfig::getConfigExample() . "\n";
exit(0);
}
// ANSI 색상 코드
$RED = "\033[1;31m";
$GREEN = "\033[1;32m";
$YELLOW = "\033[1;33m";
$CYAN = "\033[1;36m";
$MAGENTA = "\033[1;35m";
$RESET = "\033[0m";
// 바이트를 읽기 쉬운 형식으로 변환
function formatBytes($bytes) {
$bytes = (int)$bytes;
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' bytes';
}
}
// 한글 포함 문자열 패딩 (터미널 출력용)
function padLabel($str, $width) {
$strWidth = mb_strwidth($str);
$padding = $width - $strWidth;
return $str . str_repeat(' ', max(0, $padding));
}
// 초를 읽기 쉬운 형식으로 변환
function formatUptime($seconds) {
$days = floor($seconds / 86400);
$hours = floor(($seconds % 86400) / 3600);
$minutes = floor(($seconds % 3600) / 60);
$secs = $seconds % 60;
$parts = [];
if ($days > 0) $parts[] = "{$days}일";
if ($hours > 0) $parts[] = "{$hours}시간";
if ($minutes > 0) $parts[] = "{$minutes}분";
if ($secs > 0 || empty($parts)) $parts[] = "{$secs}초";
return implode(' ', $parts);
}
try {
// MySQL 연결
$conn = ptyMysqlConfig::connect($mysqlSection);
$connection = $conn['connection'];
$config = $conn['config'];
echo "\n";
echo "{$CYAN}╔══════════════════════════════════════════════════════════════════╗{$RESET}\n";
echo "{$CYAN}║ MySQL 서버 정보 ║{$RESET}\n";
echo "{$CYAN}╚══════════════════════════════════════════════════════════════════╝{$RESET}\n";
echo "\n";
// ============================================
// 1. 연결 정보
// ============================================
echo "{$YELLOW}[ 연결 정보 ]{$RESET}\n";
echo str_repeat("-", 60) . "\n";
echo " Host : {$config['host']}\n";
echo " User : {$config['username']}\n";
echo " Section : $mysqlSection\n";
echo " Charset : {$config['charset']}\n";
echo "\n";
// ============================================
// 2. 서버 정보
// ============================================
echo "{$YELLOW}[ 서버 정보 ]{$RESET}\n";
echo str_repeat("-", 60) . "\n";
// 버전
$result = mysqli_query($connection, "SELECT VERSION() as version");
$row = mysqli_fetch_assoc($result);
echo " " . padLabel("Version", 18) . ": {$row['version']}\n";
// 주요 변수들
$variables = [
'version_comment' => '버전 설명',
'hostname' => '호스트명',
'port' => '포트',
'socket' => '소켓',
'datadir' => '데이터 디렉토리',
'basedir' => '설치 디렉토리',
'character_set_server' => '서버 문자셋',
'collation_server' => '서버 Collation',
'max_connections' => '최대 연결 수',
'max_allowed_packet' => '최대 패킷 크기',
'innodb_buffer_pool_size' => 'InnoDB 버퍼 풀',
];
$result = mysqli_query($connection, "SHOW VARIABLES WHERE Variable_name IN ('" . implode("','", array_keys($variables)) . "')");
$varValues = [];
while ($row = mysqli_fetch_assoc($result)) {
$varValues[$row['Variable_name']] = $row['Value'];
}
foreach ($variables as $varName => $label) {
if (isset($varValues[$varName])) {
$value = $varValues[$varName];
// 바이트 값 포맷팅
if (in_array($varName, ['max_allowed_packet', 'innodb_buffer_pool_size'])) {
$value = formatBytes($value);
}
echo " " . padLabel($label, 18) . ": $value\n";
}
}
echo "\n";
// ============================================
// 3. 서버 상태
// ============================================
echo "{$YELLOW}[ 서버 상태 ]{$RESET}\n";
echo str_repeat("-", 60) . "\n";
$statusVars = [
'Uptime' => 'Uptime',
'Threads_connected' => '현재 연결 수',
'Threads_running' => '실행 중 쓰레드',
'Questions' => '총 쿼리 수',
'Slow_queries' => '슬로우 쿼리',
'Opens' => '열린 테이블',
'Connections' => '총 연결 시도',
'Aborted_connects' => '실패한 연결',
'Bytes_received' => '수신 바이트',
'Bytes_sent' => '송신 바이트',
];
$result = mysqli_query($connection, "SHOW GLOBAL STATUS WHERE Variable_name IN ('" . implode("','", array_keys($statusVars)) . "')");
$statusValues = [];
while ($row = mysqli_fetch_assoc($result)) {
$statusValues[$row['Variable_name']] = $row['Value'];
}
foreach ($statusVars as $varName => $label) {
if (isset($statusValues[$varName])) {
$value = $statusValues[$varName];
if ($varName === 'Uptime') {
$value = formatUptime($value);
} elseif (in_array($varName, ['Bytes_received', 'Bytes_sent'])) {
$value = formatBytes($value);
} else {
$value = number_format($value);
}
echo " " . padLabel($label, 18) . ": $value\n";
}
}
echo "\n";
// ============================================
// 4. 데이터베이스 목록
// ============================================
echo "{$YELLOW}[ 데이터베이스 목록 ]{$RESET}\n";
echo str_repeat("-", 60) . "\n";
$systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
$result = mysqli_query($connection, "SHOW DATABASES");
$databases = [];
while ($row = mysqli_fetch_row($result)) {
$databases[] = $row[0];
}
$userDbCount = 0;
$systemDbCount = 0;
printf(" {$CYAN}%-30s %10s %10s{$RESET}\n", "Database", "Tables", "Size");
echo " " . str_repeat("-", 52) . "\n";
foreach ($databases as $dbName) {
$isSystem = in_array($dbName, $systemDbs);
if ($isSystem) {
$systemDbCount++;
} else {
$userDbCount++;
}
// 테이블 수 조회
$tableResult = mysqli_query($connection, "SELECT COUNT(*) as cnt FROM information_schema.tables WHERE table_schema = '$dbName'");
$tableRow = mysqli_fetch_assoc($tableResult);
$tableCount = $tableRow['cnt'];
// DB 크기 조회
$sizeResult = mysqli_query($connection, "SELECT SUM(data_length + index_length) as size FROM information_schema.tables WHERE table_schema = '$dbName'");
$sizeRow = mysqli_fetch_assoc($sizeResult);
$dbSize = $sizeRow['size'] ? formatBytes($sizeRow['size']) : '0 bytes';
$color = $isSystem ? $MAGENTA : $GREEN;
$marker = $isSystem ? '[SYS]' : '';
printf(" {$color}%-30s %10s %10s{$RESET} %s\n", $dbName, $tableCount, $dbSize, $marker);
}
echo "\n";
echo " 총 데이터베이스: " . count($databases) . "개 (사용자: {$userDbCount}, 시스템: {$systemDbCount})\n";
echo "\n";
// ============================================
// 5. 사용자 목록
// ============================================
echo "{$YELLOW}[ 사용자 목록 ]{$RESET}\n";
echo str_repeat("-", 100) . "\n";
$result = mysqli_query($connection, "SELECT User, Host, account_locked, password_expired,
IF(authentication_string = '' OR authentication_string IS NULL, 'NO', 'YES') as has_password
FROM mysql.user ORDER BY User, Host");
if ($result) {
printf(" {$CYAN}%-20s %-25s %-12s %-12s %-10s{$RESET}\n",
"User", "Host", "Locked", "PW Expired", "Has PW");
echo " " . str_repeat("-", 82) . "\n";
$userCount = 0;
while ($row = mysqli_fetch_assoc($result)) {
$userCount++;
$locked = $row['account_locked'] ?? 'N';
$expired = $row['password_expired'] ?? 'N';
$hasPw = $row['has_password'] ?? '-';
$lockedColor = ($locked === 'Y') ? $RED : $GREEN;
$expiredColor = ($expired === 'Y') ? $RED : $GREEN;
printf(" %-20s %-25s {$lockedColor}%-12s{$RESET} {$expiredColor}%-12s{$RESET} %-10s\n",
substr($row['User'], 0, 20),
substr($row['Host'], 0, 25),
$locked,
$expired,
$hasPw
);
}
echo "\n";
echo " 총 사용자: {$userCount}\n";
} else {
echo " (mysql.user 테이블 접근 권한 없음)\n";
}
echo "\n";
// ============================================
// 6. 현재 프로세스 목록
// ============================================
echo "{$YELLOW}[ 현재 프로세스 (FULL PROCESSLIST) ]{$RESET}\n";
echo str_repeat("-", 100) . "\n";
$result = mysqli_query($connection, "SHOW FULL PROCESSLIST");
printf(" {$CYAN}%-7s %-12s %-20s %-12s %-6s %-10s %-20s{$RESET}\n",
"ID", "User", "Host", "DB", "Time", "Command", "State");
echo " " . str_repeat("-", 96) . "\n";
$processCount = 0;
while ($row = mysqli_fetch_assoc($result)) {
$processCount++;
$host = substr($row['Host'], 0, 20);
$db = $row['db'] ?? '-';
$state = $row['State'] ?? '-';
$command = $row['Command'] ?? '-';
$info = $row['Info'] ?? null;
printf(" %-7s %-12s %-20s %-12s %-6s %-10s %-20s\n",
$row['Id'],
substr($row['User'], 0, 12),
$host,
substr($db, 0, 12),
$row['Time'] . 's',
substr($command, 0, 10),
substr($state, 0, 20)
);
// 실행 중인 쿼리가 있으면 표시
if ($info && $command !== 'Sleep') {
$infoTrimmed = trim(preg_replace('/\s+/', ' ', $info));
if (strlen($infoTrimmed) > 80) {
$infoTrimmed = substr($infoTrimmed, 0, 77) . '...';
}
echo " {$MAGENTA}└─ Query: {$infoTrimmed}{$RESET}\n";
}
}
echo "\n";
echo " 총 프로세스: {$processCount}\n";
echo "\n";
// ============================================
// 7. InnoDB 상태
// ============================================
echo "{$YELLOW}[ InnoDB 상태 ]{$RESET}\n";
echo str_repeat("-", 60) . "\n";
$innodbVars = [
'Innodb_buffer_pool_pages_total' => '버퍼 풀 페이지 (전체)',
'Innodb_buffer_pool_pages_free' => '버퍼 풀 페이지 (여유)',
'Innodb_buffer_pool_pages_dirty' => '버퍼 풀 페이지 (더티)',
'Innodb_buffer_pool_read_requests' => '버퍼 풀 읽기 요청',
'Innodb_buffer_pool_reads' => '디스크 읽기',
'Innodb_rows_read' => '읽은 행 수',
'Innodb_rows_inserted' => '삽입된 행 수',
'Innodb_rows_updated' => '업데이트된 행 수',
'Innodb_rows_deleted' => '삭제된 행 수',
];
$result = mysqli_query($connection, "SHOW GLOBAL STATUS WHERE Variable_name LIKE 'Innodb%'");
$innodbValues = [];
while ($row = mysqli_fetch_assoc($result)) {
$innodbValues[$row['Variable_name']] = $row['Value'];
}
foreach ($innodbVars as $varName => $label) {
if (isset($innodbValues[$varName])) {
echo " " . padLabel($label, 26) . ": " . number_format($innodbValues[$varName]) . "\n";
}
}
// 버퍼 풀 히트율 계산
if (isset($innodbValues['Innodb_buffer_pool_read_requests']) && isset($innodbValues['Innodb_buffer_pool_reads'])) {
$requests = (int)$innodbValues['Innodb_buffer_pool_read_requests'];
$reads = (int)$innodbValues['Innodb_buffer_pool_reads'];
if ($requests > 0) {
$hitRate = (($requests - $reads) / $requests) * 100;
echo " " . padLabel("버퍼 풀 히트율", 26) . ": " . sprintf("%.2f%%", $hitRate) . "\n";
}
}
echo "\n";
// ============================================
// 8. 주요 설정 변수
// ============================================
echo "{$YELLOW}[ 주요 설정 변수 ]{$RESET}\n";
echo str_repeat("-", 60) . "\n";
$moreVars = [
'query_cache_size' => '쿼리 캐시 크기',
'query_cache_type' => '쿼리 캐시 타입',
'tmp_table_size' => '임시 테이블 크기',
'max_heap_table_size' => '힙 테이블 크기',
'sort_buffer_size' => '정렬 버퍼',
'read_buffer_size' => '읽기 버퍼',
'join_buffer_size' => '조인 버퍼',
'thread_cache_size' => '쓰레드 캐시',
'table_open_cache' => '테이블 캐시',
'slow_query_log' => '슬로우 쿼리 로그',
'long_query_time' => '슬로우 쿼리 기준(초)',
'log_bin' => '바이너리 로그',
];
$bytesVars = ['query_cache_size', 'tmp_table_size', 'max_heap_table_size', 'sort_buffer_size', 'read_buffer_size', 'join_buffer_size'];
$result = mysqli_query($connection, "SHOW VARIABLES WHERE Variable_name IN ('" . implode("','", array_keys($moreVars)) . "')");
while ($row = mysqli_fetch_assoc($result)) {
$varName = $row['Variable_name'];
$label = $moreVars[$varName] ?? $varName;
$value = $row['Value'];
// 바이트 값 포맷팅
if (in_array($varName, $bytesVars)) {
$value = formatBytes($value);
}
$displayLabel = "$label ($varName)";
echo " " . padLabel($displayLabel, 42) . ": $value\n";
}
echo "\n";
mysqli_close($connection);
echo "{$CYAN}══════════════════════════════════════════════════════════════════{$RESET}\n";
echo "\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
#!/usr/bin/env php
<?php
/**
* ptyMysqlRestore
*
* MySQL 데이터베이스 복원 도구
* 설정 파일: ~/.ptyMysqlConfig.ini
*
* Usage: ./ptyMysqlRestore <pattern> [options]
* ./ptyMysqlRestore "*" (현재 경로의 모든 sql 파일)
* ./ptyMysqlRestore "mydb.*.sql" (mydb의 모든 테이블)
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/mysql/ptyMysqlConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$mysqlSection = $options['mysql'] ?? 'default';
$verbose = isset($options['verbose']);
$force = isset($options['force']);
$inputDir = $options['input'] ?? '.';
$targetDatabase = $options['database'] ?? null;
// 도움말 또는 필수 인자 확인
if (empty($positionalArgs) || isset($options['help'])) {
echo "사용법: {$argv[0]} <pattern> [옵션]\n";
echo "\n";
echo "인자:\n";
echo " <pattern> 파일 패턴 (* = 전체, mydb.*.sql = 특정 DB)\n";
echo "\n";
echo "옵션:\n";
echo " --mysql=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --database=DB명 복원할 대상 데이터베이스 (미지정시 파일명에서 추출)\n";
echo " --input=경로 입력 디렉토리 (기본값: 현재 디렉토리)\n";
echo " --force 확인 없이 바로 복원 실행\n";
echo " --verbose 상세 로그 출력\n";
echo " --help 도움말 출력\n";
echo "\n";
echo "예시:\n";
echo " {$argv[0]} \"*\" # 모든 sql 파일 복원\n";
echo " {$argv[0]} \"*.sql\" # 모든 sql 파일 복원\n";
echo " {$argv[0]} \"mydb.*.sql\" # mydb의 모든 테이블 복원\n";
echo " {$argv[0]} \"mydb.users.sql\" # 특정 파일 복원\n";
echo " {$argv[0]} \"*\" --input=/backup # /backup 디렉토리에서 복원\n";
echo " {$argv[0]} \"*\" --mysql=production # production 섹션 사용\n";
echo " {$argv[0]} \"*\" --force # 확인 없이 복원\n";
echo " {$argv[0]} \"*\" --database=mydb_dev # 모든 파일을 mydb_dev DB에 복원\n";
echo "\n";
echo "파일명 형식: <database>.<table>.sql\n";
echo "\n";
echo "설정 파일: ~/.ptyMysqlConfig.ini\n";
echo ptyMysqlConfig::getConfigExample() . "\n";
exit(isset($options['help']) ? 0 : 1);
}
$pattern = $positionalArgs[0];
// 패턴 정규화
if ($pattern === '*') {
$pattern = '*.sql';
}
if (!preg_match('/\.sql$/i', $pattern)) {
$pattern .= '.sql';
}
// ANSI 색상 코드
$RED = "\033[1;31m";
$GREEN = "\033[1;32m";
$YELLOW = "\033[1;33m";
$CYAN = "\033[1;36m";
$MAGENTA = "\033[1;35m";
$RESET = "\033[0m";
// 타임스탬프 로그 함수
function logMessage($message, $verbose = false, $isVerbose = false) {
if ($isVerbose && !$verbose) {
return;
}
$timestamp = date('Y-m-d H:i:s');
echo "[$timestamp] $message\n";
}
// 바이트를 읽기 쉬운 형식으로 변환
function formatBytes($bytes) {
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' bytes';
}
}
// 파일명에서 DB명, 테이블명 추출 (mydb.tablename.sql -> [mydb, tablename])
function extractDbAndTable($filename) {
$basename = basename($filename);
$parts = explode('.', $basename);
if (count($parts) >= 3) {
$dbName = $parts[0];
// 마지막 .sql 제거하고 나머지를 테이블명으로
array_pop($parts); // .sql 제거
array_shift($parts); // DB명 제거
$tableName = implode('.', $parts);
return [$dbName, $tableName];
}
return [null, null];
}
// 복원 실행 함수
function restoreFile($host, $user, $password, $dbName, $sqlFile, $verbose) {
$basename = basename($sqlFile);
$size = filesize($sqlFile);
$sizeStr = formatBytes($size);
// mysql 명령 생성
$cmd = "mysql";
$cmd .= " -h " . escapeshellarg($host);
$cmd .= " -u " . escapeshellarg($user);
$cmd .= " -p" . escapeshellarg($password);
$cmd .= " --default-character-set=utf8mb4";
$cmd .= " " . escapeshellarg($dbName);
$cmd .= " < " . escapeshellarg($sqlFile);
$cmd .= " 2>&1";
if ($verbose) {
logMessage("명령: $cmd", $verbose, true);
}
exec($cmd, $output, $returnCode);
if ($returnCode !== 0) {
logMessage("ERROR: $basename 복원 실패", true);
if (!empty($output)) {
logMessage(implode("\n", $output), true);
}
return false;
}
logMessage("OK: $basename ($sizeStr)", true);
return true;
}
try {
// MySQL 연결 (DB 목록 조회용)
$conn = ptyMysqlConfig::connect($mysqlSection);
$connection = $conn['connection'];
$config = $conn['config'];
// 입력 디렉토리 확인
if (!is_dir($inputDir)) {
throw new \Exception("입력 디렉토리가 없습니다: $inputDir");
}
// 패턴에 맞는 파일 찾기
$searchPattern = rtrim($inputDir, '/') . '/' . $pattern;
$files = glob($searchPattern);
if (empty($files)) {
logMessage("패턴에 맞는 파일이 없습니다: $pattern", true);
exit(0);
}
sort($files); // 파일명 순서로 정렬
// ============================================
// 복원 정보 표시
// ============================================
echo "\n";
echo "{$CYAN}╔══════════════════════════════════════════════════════════════════╗{$RESET}\n";
echo "{$CYAN}║ MySQL 복원 정보 확인 ║{$RESET}\n";
echo "{$CYAN}╚══════════════════════════════════════════════════════════════════╝{$RESET}\n";
echo "\n";
// 서버 정보
echo "{$YELLOW}[ 서버 정보 ]{$RESET}\n";
echo str_repeat("-", 50) . "\n";
echo " Host : {$config['host']}\n";
echo " User : {$config['username']}\n";
echo " Section : $mysqlSection\n";
// MySQL 버전 조회
$versionResult = mysqli_query($connection, "SELECT VERSION() as version");
if ($versionResult && $row = mysqli_fetch_assoc($versionResult)) {
echo " Version : {$row['version']}\n";
}
// 대상 DB 지정시 표시
if ($targetDatabase) {
echo " {$CYAN}Target DB: $targetDatabase{$RESET}\n";
}
echo "\n";
// 복원 대상 파일 분석
$restoreInfo = [];
$totalSize = 0;
$dbList = [];
foreach ($files as $sqlFile) {
list($origDbName, $tableName) = extractDbAndTable($sqlFile);
if (!$origDbName) {
continue;
}
// --database 옵션이 있으면 해당 DB로, 없으면 파일명에서 추출한 DB 사용
$dbName = $targetDatabase ?? $origDbName;
$size = filesize($sqlFile);
$totalSize += $size;
if (!isset($dbList[$dbName])) {
$dbList[$dbName] = [];
}
$dbList[$dbName][] = [
'table' => $tableName,
'file' => $sqlFile,
'size' => $size,
'origDb' => $origDbName,
];
}
// DB별 복원 대상 표시
echo "{$YELLOW}[ 복원 대상 ]{$RESET}\n";
echo str_repeat("-", 50) . "\n";
echo " Input : " . realpath($inputDir) . "\n";
echo " Pattern : $pattern\n";
echo " Files : " . count($files) . "개\n";
echo " Total : " . formatBytes($totalSize) . "\n";
echo "\n";
foreach ($dbList as $dbName => $tables) {
// DB 존재 여부 확인
$checkDb = mysqli_query($connection, "SHOW DATABASES LIKE '$dbName'");
$dbExists = mysqli_num_rows($checkDb) > 0;
$dbStatus = $dbExists ? "{$GREEN}[EXISTS]{$RESET}" : "{$YELLOW}[NEW]{$RESET}";
echo "{$CYAN} Database: $dbName{$RESET} $dbStatus\n";
foreach ($tables as $info) {
$tableName = $info['table'];
$sizeStr = formatBytes($info['size']);
// 원본 DB가 다르면 표시
$origInfo = "";
if ($targetDatabase && $info['origDb'] !== $dbName) {
$origInfo = " {$MAGENTA}{$info['origDb']}{$RESET}";
}
// 테이블 존재 여부 확인
$tableStatus = "";
if ($dbExists) {
$checkTable = mysqli_query($connection, "SHOW TABLES FROM `$dbName` LIKE '$tableName'");
if ($checkTable && mysqli_num_rows($checkTable) > 0) {
$tableStatus = "{$RED}[OVERWRITE]{$RESET}";
} else {
$tableStatus = "{$GREEN}[NEW]{$RESET}";
}
} else {
$tableStatus = "{$GREEN}[NEW]{$RESET}";
}
echo " - $tableName ($sizeStr) $tableStatus$origInfo\n";
}
echo "\n";
}
// 경고 메시지
echo "{$RED}╔══════════════════════════════════════════════════════════════════╗{$RESET}\n";
echo "{$RED}║ 주의: [OVERWRITE] 표시된 테이블은 기존 데이터가 삭제됩니다! ║{$RESET}\n";
echo "{$RED}╚══════════════════════════════════════════════════════════════════╝{$RESET}\n";
echo "\n";
// 확인 절차 (--force가 없을 때만)
if (!$force) {
echo "복원을 진행하시겠습니까? (y/N): ";
$handle = fopen("php://stdin", "r");
$line = fgets($handle);
fclose($handle);
$answer = strtolower(trim($line));
if ($answer !== 'y' && $answer !== 'yes') {
echo "\n복원이 취소되었습니다.\n";
mysqli_close($connection);
exit(0);
}
echo "\n";
}
// ============================================
// 복원 실행
// ============================================
echo str_repeat("=", 60) . "\n";
logMessage("MySQL 복원 시작", true);
echo str_repeat("=", 60) . "\n";
$totalRestores = 0;
$successRestores = 0;
foreach ($dbList as $dbName => $tables) {
// DB 존재 확인 및 생성
$checkDb = mysqli_query($connection, "SHOW DATABASES LIKE '$dbName'");
if (mysqli_num_rows($checkDb) === 0) {
logMessage("DB 생성: $dbName", true);
mysqli_query($connection, "CREATE DATABASE IF NOT EXISTS `$dbName` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
}
foreach ($tables as $info) {
$totalRestores++;
if (restoreFile($config['host'], $config['username'], $config['password'], $dbName, $info['file'], $verbose)) {
$successRestores++;
}
}
}
mysqli_close($connection);
echo "\n" . str_repeat("=", 60) . "\n";
logMessage("복원 완료: $successRestores / $totalRestores", true);
if ($successRestores < $totalRestores) {
exit(1);
}
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment