__invoke('backup', $target); $destPath = $backupPath.DIRECTORY_SEPARATOR.$target; if (File::isDirectory($sourcePath)) { FilePermissionHelper::copyDirectory($sourcePath, $destPath, $onProgress, $excludes); } else { FilePermissionHelper::copyFile($sourcePath, $destPath); } } Log::info('코어 백업 생성 완료', ['path' => $backupPath]); return $backupPath; } /** * 백업에서 코어 파일을 선택적으로 복원합니다. * * 복원 시에는 FilePermissionHelper를 사용하여 퍼미션을 보존합니다. * 백업 파일을 복원하되 현재 파일의 퍼미션은 유지합니다. * 개별 target 복원 실패 시에도 나머지 target 복원을 계속 진행합니다. * * @param string $backupPath 백업 디렉토리 경로 * @param array $targets 복원 대상 경로 목록 * @param \Closure|null $onProgress 진행 콜백 * @param array $excludes 제외할 디렉토리/파일 이름 목록 (예: ['node_modules', '.git']) * @return array 복원 실패한 target 목록 (빈 배열이면 전체 성공) */ public static function restoreFromBackup(string $backupPath, array $targets, ?\Closure $onProgress = null, array $excludes = []): array { $failedTargets = []; foreach ($targets as $target) { $src = $backupPath.DIRECTORY_SEPARATOR.$target; $dest = base_path($target); if (! File::exists($src) && ! File::isDirectory($src)) { continue; } try { $onProgress?->__invoke('restore', $target); if (File::isDirectory($src)) { FilePermissionHelper::copyDirectory($src, $dest, $onProgress, $excludes); } else { FilePermissionHelper::copyFile($src, $dest); } } catch (\Throwable $e) { Log::error("코어 백업 복원 실패 (계속 진행): {$target}", [ 'error' => $e->getMessage(), 'backup_path' => $backupPath, ]); $failedTargets[] = $target; } } if (empty($failedTargets)) { Log::info('코어 백업 복원 완료', ['backup_path' => $backupPath]); } else { Log::warning('코어 백업 부분 복원 완료', [ 'backup_path' => $backupPath, 'failed_targets' => $failedTargets, ]); } return $failedTargets; } /** * 백업을 삭제합니다. * * @param string $backupPath 백업 디렉토리 경로 */ public static function deleteBackup(string $backupPath): void { if (! File::isDirectory($backupPath)) { return; } File::deleteDirectory($backupPath); // Windows: 파일 핸들 잠금으로 빈 디렉토리가 남을 수 있음 — 재시도 if (File::isDirectory($backupPath)) { usleep(500_000); // 0.5초 대기 후 재시도 File::deleteDirectory($backupPath); } if (File::isDirectory($backupPath)) { Log::warning('코어 백업 삭제 불완전 (잔여 디렉토리 존재)', ['path' => $backupPath]); } else { Log::info('코어 백업 삭제 완료', ['path' => $backupPath]); } } /** * 모든 코어 백업 목록을 반환합니다. * * @return array 백업 목록 [{path, created_at, size}] */ public static function listBackups(): array { $backupsDir = storage_path('app/core_backups'); if (! File::isDirectory($backupsDir)) { return []; } $backups = []; foreach (File::directories($backupsDir) as $dir) { $backups[] = [ 'path' => $dir, 'name' => basename($dir), 'created_at' => date('Y-m-d H:i:s', filectime($dir)), ]; } return $backups; } }