progress = $progress; $this->config = $progress->getChunkConfig() ?? new ChunkConfig(); $this->workDir = ALLDAMI_TEMP_DIR . $progress->getJobId() . '/'; if (!is_dir($this->workDir)) { wp_mkdir_p($this->workDir); } $this->dbExporter = new DbExporter($this->config, $progress, $this->workDir); $this->fileArchiver = new FileArchiver($this->config, $progress, $this->workDir); } /** * Export işlemini başlatır */ public function initialize(): array { $this->progress->setStep('init', 0, 1, __('Initializing...', 'alldami-site-migration')); $this->progress->addMessage(__('Export process started.', 'alldami-site-migration'), 'info'); // Dizinleri oluştur wp_mkdir_p($this->workDir . 'db/'); wp_mkdir_p($this->workDir . 'files/'); wp_mkdir_p($this->workDir . 'meta/'); // Disk alanı kontrolü $disk_free = disk_free_space(ABSPATH); /* translators: %s: Free disk space */ /* translators: %s: Free disk space */ $this->progress->addMessage(sprintf(__('Disk space: %s free.', 'alldami-site-migration'), \AllDataMigration::formatBytes($disk_free)), 'info'); // Eski temp dosyalarını temizle (1 saatten eski) $this->cleanupOldTempFiles(); $this->progress->completeStep('init'); return ['next_step' => 'analyze']; } /** * Ortam analizi yapar */ public function analyze(): array { $this->progress->setStep('analyze', 0, 4, __('Analyzing environment...', 'alldami-site-migration')); // Disk alanı kontrolü $this->progress->setStep('analyze', 1, 4, __('Checking disk space...', 'alldami-site-migration')); $upload_dir = wp_upload_dir(); $freeSpace = @disk_free_space($upload_dir['basedir']); $freeSpaceMB = $freeSpace ? round($freeSpace / 1024 / 1024, 2) : 0; $freeSpaceGB = round($freeSpaceMB / 1024, 2); // Minimum 500MB gerekli if ($freeSpaceMB < 500) { throw new \Exception( sprintf( /* translators: 1: Free space in MB, 2: Free space in GB */ __('Insufficient disk space! Only %1$.2f MB (%2$.2f GB) free. At least 500 MB is required for export. Please clean up unnecessary files or empty the wp-content/uploads/alldami-site-migration/temp folder.', 'alldami-site-migration'), $freeSpaceMB, $freeSpaceGB ) ); } /* translators: %s: Free space in GB */ /* translators: %s: Free space in GB */ $this->progress->addMessage(sprintf(__('Disk space: %.2f GB free.', 'alldami-site-migration'), $freeSpaceGB), 'info'); // Tabloları bul $this->progress->setStep('analyze', 2, 4, __('Analyzing database...', 'alldami-site-migration')); $tables = $this->dbExporter->prepareTables(); /* translators: %d: Number of tables */ $this->progress->addMessage(sprintf(__('%d tables found.', 'alldami-site-migration'), count($tables)), 'info'); // Dosyaları bul $this->progress->setStep('analyze', 3, 4, __('Analyzing files...', 'alldami-site-migration')); $files = $this->fileArchiver->prepareFileList(); /* translators: %d: Number of files */ $this->progress->addMessage(sprintf(__('%d files found.', 'alldami-site-migration'), count($files)), 'info'); // Manifest oluştur $this->progress->setStep('analyze', 4, 4, __('Creating manifest...', 'alldami-site-migration')); $this->createManifest($tables, $files); // Resume pointer ayarla - dosya listesini de kaydet! $this->progress->setResumePointer([ 'db_table_index' => 0, 'db_row_offset' => 0, 'file_index' => 0, 'tables' => $tables, 'files_count' => count($files), 'file_list' => $files // Dosya listesini kaydet ]); $this->progress->completeStep('analyze'); return ['next_step' => 'db_export']; } /** * Veritabanı export chunk */ public function exportDbChunk(): array { $pointer = $this->progress->getResumePointer(); $tableIndex = $pointer['db_table_index'] ?? 0; $offset = $pointer['db_row_offset'] ?? 0; $tables = $pointer['tables'] ?? []; // Tablo listesini resume pointer'dan geri yükle if (!empty($tables)) { $this->dbExporter->setTables($tables); } $totalTables = count($tables); $totalRows = array_sum(array_column($tables, 'rows')); // Progress hesapla $completedRows = 0; $tableNames = array_keys($tables); for ($i = 0; $i < $tableIndex && $i < count($tableNames); $i++) { $completedRows += $tables[$tableNames[$i]]['rows']; } $completedRows += $offset; $this->progress->setStep( 'db_export', $completedRows, $totalRows, /* translators: 1: Table name, 2: Current table index, 3: Total tables */ /* translators: 1: Table name, 2: Current index, 3: Total tables */ sprintf(__('Table: %1$s (%2$d/%3$d)', 'alldami-site-migration'), $tableNames[$tableIndex] ?? __('Unknown', 'alldami-site-migration'), $tableIndex + 1, $totalTables) ); // Chunk export $result = $this->dbExporter->exportChunk($tableIndex, $offset); if ($result === null) { // Tamamlandı $this->dbExporter->exportTableStructures(); $this->progress->completeStep('db_export'); $this->progress->addMessage(__('Database export completed.', 'alldami-site-migration'), 'info'); return ['next_step' => 'file_archive']; } // Sonraki chunk ayarla - mevcut verileri koru! $this->progress->setResumePointer(array_merge($pointer, [ 'db_table_index' => $result['table_index'], 'db_row_offset' => $result['offset'] ])); if (isset($result['completed_table'])) { /* translators: %s: Table name */ /* translators: %s: Table name */ $this->progress->addMessage(sprintf(__('Table completed: %s', 'alldami-site-migration'), $result['completed_table']), 'info'); } return ['next_step' => 'db_export']; } /** * Dosya arşivleme chunk */ public function archiveFilesChunk(): array { $pointer = $this->progress->getResumePointer(); $fileIndex = $pointer['file_index'] ?? 0; // Dosya listesini resume pointer'dan geri yükle $savedFileList = $pointer['file_list'] ?? []; if (!empty($savedFileList)) { $this->fileArchiver->setFileList($savedFileList); } else { // İlk kez çalışıyorsa veya liste kaybolmuşsa, dosya listesini yeniden tara $savedFileList = $this->fileArchiver->prepareFileList(); /* translators: %d: Number of files found */ /* translators: %d: Number of files found */ $this->progress->addMessage(sprintf(__('File scan completed: %d files found.', 'alldami-site-migration'), count($savedFileList)), 'info'); } $totalFiles = $this->fileArchiver->getFileCount(); // Eğer dosya yoksa, direkt tamamlan if ($totalFiles === 0) { $this->progress->completeStep('file_archive'); $this->progress->addMessage(__('No files found to archive.', 'alldami-site-migration'), 'warning'); return ['next_step' => 'package']; } /* translators: 1: current file index, 2: total files */ $this->progress->setStep('file_archive', $fileIndex, $totalFiles, sprintf(__('Archiving files... (%1$d/%2$d)', 'alldami-site-migration'), $fileIndex, $totalFiles)); $result = $this->fileArchiver->archiveChunk($fileIndex); // Log mesajı ekle $filesAdded = $result['files_added'] ?? 0; if ($filesAdded > 0) { /* translators: 1: files added, 2: current count, 3: total */ $this->progress->addMessage(sprintf(__('%1$d files archived (total: %2$d/%3$d)', 'alldami-site-migration'), $filesAdded, ($result['next_index'] ?? $fileIndex), $totalFiles), 'info'); } if ($result === null || ($result['completed'] ?? false)) { $this->progress->completeStep('file_archive'); /* translators: %d: total files */ $this->progress->addMessage(sprintf(__('File archiving completed. Total: %d files', 'alldami-site-migration'), $totalFiles), 'info'); return ['next_step' => 'package']; } // file_list'i de kaydet ki sonraki chunk'ta tekrar yüklenebilsin $this->progress->setResumePointer([ 'file_index' => $result['next_index'], 'file_list' => $savedFileList, 'files_count' => $totalFiles ]); return ['next_step' => 'file_archive']; } /** * Final paketi oluşturur */ public function createPackage(): array { $this->progress->setStep('package', 0, 2, __('Creating final package...', 'alldami-site-migration')); $packageName = sprintf('alldami-backup-%s.zip', gmdate('Y-m-d-His')); $packagePath = ALLDAMI_TEMP_DIR . $packageName; $zip = new \ZipArchive(); if ($zip->open($packagePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { throw new \Exception(__('Could not create package.', 'alldami-site-migration')); } $this->progress->setStep('package', 1, 2, __('Adding files...', 'alldami-site-migration')); // Debug: workDir yolunu ve içeriğini logla $this->progress->addMessage(sprintf('WorkDir: %s', $this->workDir), 'info'); // files/ alt dizinini kontrol et $filesDir = $this->workDir . 'files/'; if (is_dir($filesDir)) { $chunkZips = glob($filesDir . '*.zip'); /* translators: %d: number of zip files */ $this->progress->addMessage(sprintf(__('%d zip files found in files/ directory', 'alldami-site-migration'), count($chunkZips)), 'info'); } else { /* translators: %s: directory path */ $this->progress->addMessage(sprintf(__('ERROR: files/ directory not found: %s', 'alldami-site-migration'), $filesDir), 'error'); } // workDir'deki dosya sayısını logla $filesCount = $this->countFilesInDir($this->workDir); /* translators: %d: number of files */ $this->progress->addMessage(sprintf(__('Files to package: %d', 'alldami-site-migration'), $filesCount), 'info'); // Tüm dosyaları ekle $addedCount = $this->addDirectoryToZipWithCount($zip, $this->workDir, ''); /* translators: %d: number of files */ $this->progress->addMessage(sprintf(__('Files added to package: %d', 'alldami-site-migration'), $addedCount), 'info'); $zip->close(); $this->progress->setStep('package', 2, 2, __('Cleaning up...', 'alldami-site-migration')); // Geçici dizini temizle $this->recursiveDelete($this->workDir); $filesize = filesize($packagePath); // Dynamically resolve URL instead of stripping WP_CONTENT_DIR $upload_dir = wp_upload_dir(); $downloadUrl = trailingslashit($upload_dir['baseurl']) . 'alldami-site-migration/temp/' . $packageName; /* translators: %s: package filename */ $this->progress->addMessage(sprintf(__('Package created: %s', 'alldami-site-migration'), $packageName), 'info'); $this->progress->markComplete([ 'filename' => $packageName, 'filesize' => $filesize, 'download_url' => $downloadUrl, 'path' => $packagePath ]); return [ 'status' => 'completed', 'result' => $this->progress->getProgress()['result'] ]; } private function createManifest(array $tables, array $files): void { global $wpdb; $manifest = [ 'version' => ALLDAMI_VERSION, 'created_at' => gmdate('Y-m-d H:i:s'), 'site_url' => site_url(), 'home_url' => home_url(), 'wp_version' => get_bloginfo('version'), 'php_version' => PHP_VERSION, 'table_prefix' => $wpdb->prefix, 'tables_count' => count($tables), 'files_count' => count($files), 'serialized_data' => true, ]; file_put_contents( $this->workDir . 'manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ); // Replace map $replaceMap = [ 'site_url' => site_url(), 'home_url' => home_url(), 'content_url' => content_url(), 'upload_path' => wp_upload_dir()['basedir'], ]; file_put_contents( $this->workDir . 'meta/replace-map.json', json_encode($replaceMap, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ); } private function addDirectoryToZip(\ZipArchive $zip, string $dir, string $prefix): void { // Normalize directory path $dir = str_replace('\\', '/', rtrim($dir, '/\\')) . '/'; if (!is_dir($dir)) return; $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY ); foreach ($files as $file) { if ($file->isFile()) { $filePath = $file->getPathname(); // Windows uzun yol desteği (Prefix ekleyerek) if (strpos($filePath, '\\\\?\\') === false && strlen($filePath) > 240) { $realPath = '\\\\?\\' . str_replace('/', '\\', $filePath); } else { $realPath = $filePath; } // Dosya okunabilir mi? if (!is_readable($realPath)) continue; $normalizedFilePath = str_replace('\\', '/', $filePath); // Zip içine koyarken normal slash kullan $relativePath = $prefix . substr($normalizedFilePath, strlen($dir)); $zip->addFile($filePath, $relativePath); } } } private function recursiveDelete(string $dir): void { if (!is_dir($dir)) return; $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($files as $file) { $path = $file->getPathname(); if ($file->isDir()) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir @rmdir($path); } else { // Use wp_delete_file if available if (function_exists('wp_delete_file')) { wp_delete_file($path); } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink @unlink($path); } } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir @rmdir($dir); } private function countFilesInDir(string $dir): int { if (!is_dir($dir)) return 0; $count = 0; $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY ); foreach ($files as $file) { if ($file->isFile()) { $count++; } } return $count; } private function addDirectoryToZipWithCount(\ZipArchive $zip, string $dir, string $prefix): int { $dir = str_replace('\\', '/', rtrim($dir, '/\\')) . '/'; if (!is_dir($dir)) return 0; $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY ); $count = 0; foreach ($files as $file) { if ($file->isFile()) { $filePath = $file->getPathname(); // Windows uzun yol desteği if (strpos($filePath, '\\\\?\\') === false && strlen($filePath) > 240) { $realPath = '\\\\?\\' . str_replace('/', '\\', $filePath); } else { $realPath = $filePath; } if (!is_readable($realPath)) continue; $normalizedFilePath = str_replace('\\', '/', $filePath); $relativePath = $prefix . substr($normalizedFilePath, strlen($dir)); if ($zip->addFile($filePath, $relativePath)) { $count++; } } } return $count; } /** * Eski temp dosyalarını temizler */ private function cleanupOldTempFiles(): void { $tempDir = ALLDAMI_TEMP_DIR; if (!is_dir($tempDir)) { return; } $iterator = new \DirectoryIterator($tempDir); $now = time(); // 1 saat (3600 saniye) time-to-live $ttl = 3600; foreach ($iterator as $fileinfo) { if ($fileinfo->isDot()) continue; // Sadece dizileri temizle (job klasörleri) // .zip dosyalarını silme (kullanıcı indirmemiş olabilir) if ($fileinfo->isDir()) { if ($now - $fileinfo->getMTime() > $ttl) { $this->recursiveDelete($fileinfo->getPathname()); // Log eklemek progress'i bozabilir, sessizce sil } } } } }