Commit 891e088b authored by platyhouse's avatar platyhouse

# CLI 스크립트 가이드라인 개선 및 ptyMysqlBackup/Restore tgz 압축 지원 추가

## CLAUDE.md 문서 개선

### 옵션 처리 우선순위 가이드라인 추가
- CLAUDE.md: --help 옵션 최우선 처리 규칙 상세 문서화
  - 올바른 패턴과 잘못된 패턴 예시 코드 추가
  - 옵션 처리 순서 명시: --help → --edit → 필수 인자 검증 → 비즈니스 로직
- CLAUDE.md: 스크립트 기본 구조 템플릿에서 옵션 처리 순서 재정리
  - 각 섹션에 번호 주석 추가 (1. --help, 2. --edit, 3. 필수 인자 검증)

### ptyMysqlBackup/Restore 문서 업데이트
- CLAUDE.md: ptyMysqlBackup --tgz 옵션 사용법 및 옵션 테이블에 추가
- CLAUDE.md: ptyMysqlRestore에 .tgz 파일 처리 지원 문서화
  - 사용 예시에 tgz 관련 명령어 추가
  - .tgz 파일 처리 동작 설명 추가

## ptyMysqlBackup 기능 추가

### --tgz 압축 옵션 구현
- ptyMysqlBackup: --tgz 옵션 플래그 및 도움말 추가
- ptyMysqlBackup: backupTable() 함수에 $useTgz 파라미터 추가
- ptyMysqlBackup: 백업 완료 후 tar 압축 및 원본 .sql 파일 삭제 로직 구현
- ptyMysqlBackup: 압축률 표시 (tgzSize / originalSize %)
- ptyMysqlBackup: --dry-run 모드에서 출력 확장자(.tgz/.sql) 동적 표시

## ptyMysqlRestore 기능 추가

### .tgz 파일 복원 지원
- ptyMysqlRestore: 패턴 처리 로직 변경 - '*' 입력 시 .sql과 .tgz 모두 검색
- ptyMysqlRestore: extractTgz() 함수 추가 - 임시 디렉토리에 압축 해제
- ptyMysqlRestore: extractDbAndTable() 함수가 .tgz 확장자도 처리하도록 수정
- ptyMysqlRestore: 복원 루프에서 .tgz 파일 감지 및 압축 해제 로직 추가
- ptyMysqlRestore: 복원 완료 후 임시 파일/디렉토리 자동 정리
parent f4912f9c
......@@ -38,6 +38,72 @@ require_once __DIR__ . '/ptyLibrary_PHP/ai/ptyAIConfig.php'; // AI API
| `--edit` | 스크립트를 에디터로 열기 | O |
| `--dry-run` | 실제 실행 없이 대상/결과 미리보기 | O (데이터 변경 스크립트) |
### 옵션 처리 우선순위 (중요!)
**`--help`는 반드시 최우선으로 처리되어야 합니다.** 사용자가 `--help`를 입력하면 다른 모든 로직보다 먼저 도움말을 출력하고 종료해야 합니다.
```php
// ============================================================
// 옵션 처리 우선순위
// ============================================================
// 1. --help : 최우선! 인자 검증/연결 없이 즉시 도움말 출력
// 2. --edit : 스크립트 편집 (인자 없이도 동작)
// 3. 필수 인자 검증
// 4. 서비스 연결 및 비즈니스 로직
// ============================================================
// 1. --help: 최우선 처리 (인자 유무와 관계없이)
if (isset($options['help'])) {
echo "사용법: {$argv[0]} <필수인자> [옵션]\n";
// ... 도움말 출력 ...
exit(0);
}
// 2. --edit: 스크립트 편집
if (isset($options['edit'])) {
$editor = getenv('EDITOR') ?: 'vi';
passthru("$editor " . escapeshellarg(__FILE__));
exit(0);
}
// 3. 필수 인자 검증 (--help 이후에 처리)
if (count($positionalArgs) < 1) {
echo "Error: 필수 인자가 누락되었습니다.\n";
echo "도움말: {$argv[0]} --help\n";
exit(1);
}
// 4. 서비스 연결 및 비즈니스 로직
try {
$conn = ServiceConfig::connect($section);
// ...
}
```
**잘못된 패턴 (금지):**
```php
// ❌ 필수 인자 검증과 --help를 함께 처리하면 안됨
if (count($positionalArgs) < 1 || isset($options['help'])) {
// 이 패턴은 인자 없이 실행 시 exit(1)로 종료됨
exit(isset($options['help']) ? 0 : 1);
}
```
**올바른 패턴:**
```php
// ✅ --help를 먼저 독립적으로 처리
if (isset($options['help'])) {
// 도움말 출력
exit(0); // 항상 성공 코드
}
// 그 다음 인자 검증
if (count($positionalArgs) < 1) {
echo "Error: 필수 인자가 누락되었습니다.\n";
exit(1); // 에러 코드
}
```
### 서비스별 필수 옵션
| 서비스 | 옵션 | 설명 |
......@@ -75,17 +141,10 @@ $options = $parsed['options'];
$verbose = isset($options['verbose']);
$dryRun = isset($options['dry-run']);
// --edit 옵션: 스크립트 편집 (도움말보다 먼저 처리)
if (isset($options['edit'])) {
$editor = getenv('EDITOR') ?: 'vi';
passthru("$editor " . escapeshellarg(__FILE__));
exit(0);
}
// ============================================================
// 도움말
// 1. --help: 최우선 처리 (인자 유무와 관계없이)
// ============================================================
if (count($positionalArgs) < 1 || isset($options['help'])) {
if (isset($options['help'])) {
echo "사용법: {$argv[0]} <필수인자> [옵션]\n";
echo "\n";
echo "인자:\n";
......@@ -99,7 +158,25 @@ if (count($positionalArgs) < 1 || isset($options['help'])) {
echo "\n";
// 설정 파일 예시 출력 (해당 시)
// echo ServiceConfig::getConfigExample() . "\n";
exit(isset($options['help']) ? 0 : 1);
exit(0);
}
// ============================================================
// 2. --edit: 스크립트 편집
// ============================================================
if (isset($options['edit'])) {
$editor = getenv('EDITOR') ?: 'vi';
passthru("$editor " . escapeshellarg(__FILE__));
exit(0);
}
// ============================================================
// 3. 필수 인자 검증
// ============================================================
if (count($positionalArgs) < 1) {
echo "Error: 필수 인자가 누락되었습니다.\n";
echo "도움말: {$argv[0]} --help\n";
exit(1);
}
// ============================================================
......@@ -658,6 +735,7 @@ mysqldump를 사용하여 테이블을 백업합니다.
./ptyMysqlBackup mydb users --output=/backup # 지정 경로에 저장
./ptyMysqlBackup '*' '*' --dry-run # 백업 대상만 미리보기
./ptyMysqlBackup '*' '*' --ignore-x=false # x_ 접두사 포함 전체 백업
./ptyMysqlBackup mydb users --tgz # mydb.users.tgz 생성 (압축)
```
**옵션:**
......@@ -667,12 +745,13 @@ mysqldump를 사용하여 테이블을 백업합니다.
| `--mysql=섹션명` | INI 파일 섹션 | `default` |
| `--output=경로` | 출력 디렉토리 | `.` |
| `--ignore-x=true\|false` | x_ 접두사 DB/테이블 무시 | `true` |
| `--tgz` | 백업 후 .tgz로 압축 (원본 .sql 삭제) | - |
| `--dry-run` | 실제 백업 없이 대상 목록만 출력 | - |
| `--verbose` | 상세 로그 출력 | - |
| `--edit` | 스크립트를 에디터로 열기 | - |
**백업 파일:**
- 파일명 형식: `{database}.{table}.sql`
- 파일명 형식: `{database}.{table}.sql` 또는 `{database}.{table}.tgz` (--tgz 사용 시)
- 파일 헤더에 백업 시간, 실행 명령어 포함
- `x_` 접두사 DB/테이블은 기본 스킵 (`--ignore-x=false`로 포함 가능)
......@@ -688,13 +767,16 @@ mysqldump를 사용하여 테이블을 백업합니다.
### ptyMysqlRestore - 복원
SQL 파일을 MySQL에 복원합니다.
SQL 파일 또는 tgz 압축 파일을 MySQL에 복원합니다.
```bash
./ptyMysqlRestore <pattern> [옵션]
./ptyMysqlRestore "*" # 모든 sql 파일 복원
./ptyMysqlRestore "*" # 모든 sql/tgz 파일 복원
./ptyMysqlRestore "*.sql" # 모든 sql 파일만 복원
./ptyMysqlRestore "*.tgz" # 모든 tgz 파일 복원 (압축 해제 후)
./ptyMysqlRestore "mydb.*.sql" # mydb의 모든 테이블 복원
./ptyMysqlRestore "mydb.users.sql" # 특정 파일 복원
./ptyMysqlRestore "mydb.users.tgz" # 특정 tgz 파일 복원
./ptyMysqlRestore "*" --database=mydb_dev # 모든 파일을 mydb_dev DB에 복원
./ptyMysqlRestore "*" --force # 확인 없이 복원
```
......@@ -709,7 +791,12 @@ SQL 파일을 MySQL에 복원합니다.
| `--force` | 확인 없이 바로 복원 | - |
| `--verbose` | 상세 로그 출력 | - |
**파일명 형식:** `{database}.{table}.sql`
**파일명 형식:** `{database}.{table}.sql` 또는 `{database}.{table}.tgz`
**.tgz 파일 처리:**
- `*` 패턴 사용 시 `.sql``.tgz` 모두 검색
- `.tgz` 파일은 임시 디렉토리에 압축 해제 후 복원
- 복원 완료 후 임시 파일 자동 정리
**복원 전 확인:**
- 서버 정보 표시
......
......@@ -24,6 +24,7 @@ $verbose = isset($options['verbose']);
$outputDir = $options['output'] ?? '.';
$ignoreX = ($options['ignore-x'] ?? 'true') !== 'false'; // 기본값: true
$dryRun = isset($options['dry-run']);
$useTgz = isset($options['tgz']);
// --edit 옵션: 스크립트 편집
if (isset($options['edit'])) {
......@@ -45,6 +46,7 @@ if (count($positionalArgs) < 2 || isset($options['help'])) {
echo " --mysql=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --output=경로 출력 디렉토리 (기본값: 현재 디렉토리)\n";
echo " --ignore-x=true|false x_ 접두사 DB/테이블 무시 (기본값: true)\n";
echo " --tgz 백업 후 .tgz로 압축 (원본 .sql 삭제)\n";
echo " --dry-run 실제 백업 없이 대상 목록만 출력\n";
echo " --verbose 상세 로그 출력\n";
echo " --edit 이 스크립트를 에디터로 열기\n";
......@@ -57,6 +59,7 @@ if (count($positionalArgs) < 2 || isset($options['help'])) {
echo " {$argv[0]} mydb users --output=/backup # /backup/mydb.users.sql 생성\n";
echo " {$argv[0]} mydb users --mysql=production # production 섹션 사용\n";
echo " {$argv[0]} '*' '*' --ignore-x=false # x_ 접두사 포함 전체 백업\n";
echo " {$argv[0]} mydb users --tgz # mydb.users.tgz 생성 (압축)\n";
echo "\n";
echo "기본 mysqldump 옵션:\n";
echo " --default-character-set=utf8mb4\n";
......@@ -109,7 +112,7 @@ function getMysqldumpCmd($host, $user, $password, $dbName, $tableName = null) {
}
// 백업 실행 함수
function backupTable($host, $user, $password, $dbName, $tableName, $outputDir, $verbose, $originalCommand) {
function backupTable($host, $user, $password, $dbName, $tableName, $outputDir, $verbose, $originalCommand, $useTgz = false) {
$outputFile = rtrim($outputDir, '/') . "/{$dbName}.{$tableName}.sql";
$errorFile = sys_get_temp_dir() . "/mysqldump_err_" . getmypid() . ".txt";
......@@ -151,8 +154,38 @@ function backupTable($host, $user, $password, $dbName, $tableName, $outputDir, $
if (file_exists($outputFile)) {
$size = filesize($outputFile);
$sizeStr = formatBytes($size);
// --tgz 옵션: 압축 후 원본 삭제
if ($useTgz) {
$tgzFile = rtrim($outputDir, '/') . "/{$dbName}.{$tableName}.tgz";
$sqlBasename = "{$dbName}.{$tableName}.sql";
// tar 명령 실행 (outputDir에서 상대 경로로 압축)
$tarCmd = "tar -czf " . escapeshellarg($tgzFile) . " -C " . escapeshellarg(rtrim($outputDir, '/')) . " " . escapeshellarg($sqlBasename);
if ($verbose) {
logMessage("압축: $tarCmd", $verbose, true);
}
exec($tarCmd, $tarOutput, $tarReturnCode);
if ($tarReturnCode !== 0) {
logMessage("ERROR: {$dbName}.{$tableName} 압축 실패", true);
return false;
}
// 원본 SQL 파일 삭제
@unlink($outputFile);
// 압축 파일 크기
$tgzSize = filesize($tgzFile);
$tgzSizeStr = formatBytes($tgzSize);
$ratio = $size > 0 ? round(($tgzSize / $size) * 100, 1) : 0;
logMessage("OK: {$dbName}.{$tableName}.tgz ($tgzSizeStr, {$ratio}% of $sizeStr)", true);
} else {
logMessage("OK: {$dbName}.{$tableName}.sql ($sizeStr)", true);
}
}
return true;
}
......@@ -246,10 +279,11 @@ try {
}
$totalBackups++;
if ($dryRun) {
logMessage("WOULD BACKUP: $dbName.$tableName -> $outputDir/$dbName.$tableName.sql", true);
$ext = $useTgz ? 'tgz' : 'sql';
logMessage("WOULD BACKUP: $dbName.$tableName -> $outputDir/$dbName.$tableName.$ext", true);
$successBackups++;
} else {
if (backupTable($config['host'], $config['username'], $config['password'], $dbName, $tableName, $outputDir, $verbose, $originalCommand)) {
if (backupTable($config['host'], $config['username'], $config['password'], $dbName, $tableName, $outputDir, $verbose, $originalCommand, $useTgz)) {
$successBackups++;
}
}
......
......@@ -44,17 +44,19 @@ if (empty($positionalArgs) || isset($options['help'])) {
echo " --help 도움말 출력\n";
echo "\n";
echo "예시:\n";
echo " {$argv[0]} \"*\" # 모든 sql 파일 복원\n";
echo " {$argv[0]} \"*\" # 모든 sql/tgz 파일 복원\n";
echo " {$argv[0]} \"*.sql\" # 모든 sql 파일 복원\n";
echo " {$argv[0]} \"*.tgz\" # 모든 tgz 파일 복원 (압축 해제 후)\n";
echo " {$argv[0]} \"mydb.*.sql\" # mydb의 모든 테이블 복원\n";
echo " {$argv[0]} \"mydb.users.sql\" # 특정 파일 복원\n";
echo " {$argv[0]} \"mydb.users.tgz\" # 특정 tgz 파일 복원\n";
echo " {$argv[0]} \"*\" --input=/backup # /backup 디렉토리에서 복원\n";
echo " {$argv[0]} \"*\" --mysql=production # production 섹션 사용\n";
echo " {$argv[0]} \"*\" --force # 확인 없이 복원\n";
echo " {$argv[0]} \"*\" --db=mydb_dev # 모든 파일을 mydb_dev DB에 복원\n";
echo " {$argv[0]} \"mydb.users.sql\" --table=users_backup # users → users_backup 으로 복원\n";
echo "\n";
echo "파일명 형식: <database>.<table>.sql\n";
echo "파일명 형식: <database>.<table>.sql 또는 <database>.<table>.tgz\n";
echo "\n";
echo "설정 파일: ~/.ptyMysqlConfig.ini\n";
echo ptyMysqlConfig::getConfigExample() . "\n";
......@@ -62,13 +64,18 @@ if (empty($positionalArgs) || isset($options['help'])) {
}
$pattern = $positionalArgs[0];
$patterns = [];
// 패턴 정규화
if ($pattern === '*') {
$pattern = '*.sql';
}
if (!preg_match('/\.sql$/i', $pattern)) {
$pattern .= '.sql';
// * 이면 sql과 tgz 모두 검색
$patterns = ['*.sql', '*.tgz'];
} elseif (preg_match('/\.(sql|tgz)$/i', $pattern)) {
// 이미 확장자가 있으면 그대로 사용
$patterns = [$pattern];
} else {
// 확장자 없으면 .sql 추가
$patterns = [$pattern . '.sql'];
}
// ANSI 색상 코드
......@@ -101,14 +108,14 @@ function formatBytes($bytes) {
}
}
// 파일명에서 DB명, 테이블명 추출 (mydb.tablename.sql -> [mydb, tablename])
// 파일명에서 DB명, 테이블명 추출 (mydb.tablename.sql 또는 mydb.tablename.tgz -> [mydb, tablename])
function extractDbAndTable($filename) {
$basename = basename($filename);
$parts = explode('.', $basename);
if (count($parts) >= 3) {
$dbName = $parts[0];
// 마지막 .sql 제거하고 나머지를 테이블명으로
array_pop($parts); // .sql 제거
// 마지막 확장자(.sql 또는 .tgz) 제거하고 나머지를 테이블명으로
array_pop($parts); // 확장자 제거
array_shift($parts); // DB명 제거
$tableName = implode('.', $parts);
return [$dbName, $tableName];
......@@ -156,6 +163,42 @@ function transformTableName($srcFile, $srcTable, $dstTable, $verbose) {
return $dstFile;
}
// ============================================
// tgz 파일 압축 해제 함수
// ============================================
function extractTgz($tgzFile, $verbose) {
$tmpDir = sys_get_temp_dir() . '/ptyMysqlRestore_' . getmypid() . '_' . time();
if (!mkdir($tmpDir, 0755, true)) {
return null;
}
$tarCmd = "tar -xzf " . escapeshellarg($tgzFile) . " -C " . escapeshellarg($tmpDir);
if ($verbose) {
logMessage("압축 해제: $tarCmd", true);
}
exec($tarCmd, $output, $returnCode);
if ($returnCode !== 0) {
@rmdir($tmpDir);
return null;
}
// 압축 해제된 SQL 파일 찾기
$sqlFiles = glob($tmpDir . '/*.sql');
if (empty($sqlFiles)) {
@rmdir($tmpDir);
return null;
}
return [
'sqlFile' => $sqlFiles[0],
'tmpDir' => $tmpDir
];
}
// 복원 실행 함수
function restoreFile($host, $user, $password, $dbName, $tableName, $sqlFile, $verbose) {
$basename = basename($sqlFile);
......@@ -202,11 +245,18 @@ try {
}
// 패턴에 맞는 파일 찾기
$searchPattern = rtrim($inputDir, '/') . '/' . $pattern;
$files = glob($searchPattern);
$files = [];
foreach ($patterns as $p) {
$searchPattern = rtrim($inputDir, '/') . '/' . $p;
$found = glob($searchPattern);
if ($found) {
$files = array_merge($files, $found);
}
}
$files = array_unique($files);
if (empty($files)) {
logMessage("패턴에 맞는 파일이 없습니다: $pattern", true);
logMessage("패턴에 맞는 파일이 없습니다: " . implode(', ', $patterns), true);
exit(0);
}
......@@ -270,6 +320,7 @@ try {
'file' => $sqlFile,
'size' => $size,
'origDb' => $origDbName,
'isTgz' => preg_match('/\.tgz$/i', $sqlFile),
];
}
......@@ -277,7 +328,7 @@ try {
echo "{$YELLOW}[ 복원 대상 ]{$RESET}\n";
echo str_repeat("-", 50) . "\n";
echo " Input : " . realpath($inputDir) . "\n";
echo " Pattern : $pattern\n";
echo " Pattern : " . implode(', ', $patterns) . "\n";
echo " Files : " . count($files) . "개\n";
echo " Total : " . formatBytes($totalSize) . "\n";
echo "\n";
......@@ -372,12 +423,28 @@ try {
$origTableName = $info['origTable'];
$sqlFile = $info['file'];
$tempFile = null;
$extractedInfo = null;
// .tgz 파일인 경우 압축 해제
if ($info['isTgz']) {
$extractedInfo = extractTgz($sqlFile, $verbose);
if ($extractedInfo === null) {
logMessage("ERROR: {$tableName} 압축 해제 실패", true);
continue;
}
$sqlFile = $extractedInfo['sqlFile'];
}
// 테이블명이 다를 경우 sed로 변환
if ($tableName !== $origTableName) {
$tempFile = transformTableName($sqlFile, $origTableName, $tableName, $verbose);
if ($tempFile === null) {
logMessage("ERROR: 테이블명 변환 실패 - $origTableName$tableName", true);
// 압축 해제된 임시 파일 정리
if ($extractedInfo !== null) {
@unlink($extractedInfo['sqlFile']);
@rmdir($extractedInfo['tmpDir']);
}
continue;
}
$sqlFile = $tempFile;
......@@ -391,6 +458,12 @@ try {
if ($tempFile !== null && file_exists($tempFile)) {
@unlink($tempFile);
}
// 압축 해제된 임시 파일/디렉토리 정리
if ($extractedInfo !== null) {
@unlink($extractedInfo['sqlFile']);
@rmdir($extractedInfo['tmpDir']);
}
}
}
......
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