'', // 'export' veya 'import' 'status' => 'pending', // pending, running, paused, completed, error 'current_step' => '', // Mevcut adım adı 'current_step_progress' => 0, // Mevcut adım yüzdesi (0-100) 'overall_progress' => 0, // Toplam ilerleme yüzdesi (0-100) 'steps' => [], // Adım detayları 'resume_pointer' => [], // Resume için pointer bilgileri 'chunk_config' => [], // Chunk konfigürasyonu 'messages' => [], // Log mesajları 'error' => null, // Hata mesajı (varsa) 'result' => null, // Sonuç verisi (export: dosya yolu, vs.) 'created_at' => '', 'updated_at' => '', ]; /** * Returns export step definitions with translated labels. * * @return array */ private function getExportSteps(): array { return [ 'init' => ['label' => __('Initializing', 'alldami-site-migration'), 'weight' => 5], 'analyze' => ['label' => __('Environment Analysis', 'alldami-site-migration'), 'weight' => 5], 'db_export' => ['label' => __('Database Export', 'alldami-site-migration'), 'weight' => 40], 'file_archive' => ['label' => __('File Archiving', 'alldami-site-migration'), 'weight' => 40], 'package' => ['label' => __('Creating Package', 'alldami-site-migration'), 'weight' => 10], ]; } /** * Returns import step definitions with translated labels. * * @return array */ private function getImportSteps(): array { return [ 'init' => ['label' => __('Initializing', 'alldami-site-migration'), 'weight' => 2], 'upload' => ['label' => __('File Upload', 'alldami-site-migration'), 'weight' => 20], 'extract' => ['label' => __('Extracting Archive', 'alldami-site-migration'), 'weight' => 10], 'preflight' => ['label' => __('Preflight Check', 'alldami-site-migration'), 'weight' => 3], 'db_import' => ['label' => __('Database Import', 'alldami-site-migration'), 'weight' => 30], 'file_restore' => ['label' => __('File Restore', 'alldami-site-migration'), 'weight' => 25], 'replace' => ['label' => __('URL Replacement', 'alldami-site-migration'), 'weight' => 8], 'finalize' => ['label' => __('Finalizing', 'alldami-site-migration'), 'weight' => 2], ]; } /** * Yeni bir job başlatır * * @param string $type 'export' veya 'import' * @param ChunkConfig|null $config Chunk konfigürasyonu * @return string Job ID */ public function init(string $type, ?ChunkConfig $config = null): string { // Unique job ID oluştur $this->jobId = $this->generateJobId(); $this->stateFile = $this->getStateFilePath($this->jobId); // Eski state dosyalarını temizle (1 saatten eski) $this->cleanupOldStateFiles(); // State'i başlat $this->state = $this->defaultState; $this->state['type'] = $type; $this->state['status'] = 'running'; $this->state['created_at'] = gmdate('Y-m-d H:i:s'); $this->state['updated_at'] = gmdate('Y-m-d H:i:s'); // Set up steps with translated labels $steps = ($type === 'export') ? $this->getExportSteps() : $this->getImportSteps(); foreach ($steps as $key => $step) { $this->state['steps'][$key] = [ 'label' => $step['label'], 'weight' => $step['weight'], 'status' => 'pending', // pending, running, completed, error 'progress' => 0, 'message' => '', ]; } // Chunk config'i kaydet if ($config) { $this->state['chunk_config'] = $config->toArray(); } // State'i kaydet $this->save(); return $this->jobId; } /** * Mevcut bir job'ı yükler * * @param string $jobId Job ID * @return bool Başarılı mı */ public function load(string $jobId): bool { $this->jobId = $jobId; $this->stateFile = $this->getStateFilePath($jobId); if (!file_exists($this->stateFile)) { return false; } $content = file_get_contents($this->stateFile); $data = json_decode($content, true); if (!$data) { return false; } $this->state = array_merge($this->defaultState, $data); return true; } /** * State'i dosyaya kaydeder */ public function save(): void { $this->state['updated_at'] = gmdate('Y-m-d H:i:s'); // Dizin yoksa oluştur $dir = dirname($this->stateFile); if (!is_dir($dir)) { wp_mkdir_p($dir); } file_put_contents( $this->stateFile, json_encode($this->state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ); } /** * Bir adımı aktif olarak işaretler ve ilerleme günceller * * @param string $stepKey Adım anahtarı * @param int $current Mevcut değer * @param int $total Toplam değer * @param string $message Opsiyonel mesaj */ public function setStep(string $stepKey, int $current, int $total, string $message = ''): void { // Önceki adımları completed yap $found = false; foreach ($this->state['steps'] as $key => &$step) { if ($key === $stepKey) { $found = true; $step['status'] = 'running'; $step['progress'] = ($total > 0) ? round(($current / $total) * 100) : 0; $step['message'] = $message; } elseif (!$found) { // Bu adımdan önceki adımlar completed olmalı if ($step['status'] === 'pending' || $step['status'] === 'running') { $step['status'] = 'completed'; $step['progress'] = 100; } } } $this->state['current_step'] = $stepKey; $this->state['current_step_progress'] = $this->state['steps'][$stepKey]['progress'] ?? 0; // Toplam ilerlemeyi hesapla $this->calculateOverallProgress(); $this->save(); } /** * Bir adımı tamamlandı olarak işaretler * * @param string $stepKey Adım anahtarı */ public function completeStep(string $stepKey): void { if (isset($this->state['steps'][$stepKey])) { $this->state['steps'][$stepKey]['status'] = 'completed'; $this->state['steps'][$stepKey]['progress'] = 100; } $this->calculateOverallProgress(); $this->save(); } /** * Toplam ilerleme yüzdesini hesaplar (ağırlıklı) */ private function calculateOverallProgress(): void { $totalWeight = 0; $completedWeight = 0; foreach ($this->state['steps'] as $step) { $weight = $step['weight']; $totalWeight += $weight; $completedWeight += ($step['progress'] / 100) * $weight; } $this->state['overall_progress'] = ($totalWeight > 0) ? round(($completedWeight / $totalWeight) * 100) : 0; } /** * Resume pointer'ı ayarlar * * Kesintiden sonra kaldığımız yeri işaretler. * * @param array $pointer Resume pointer verisi * Örnek: [ * 'db_table_index' => 5, * 'db_row_offset' => 2000, * 'file_index' => 120, * 'file_path' => 'uploads/', * 'chunk_number' => 3 * ] */ public function setResumePointer(array $pointer): void { $this->state['resume_pointer'] = array_merge( $this->state['resume_pointer'], $pointer ); $this->save(); } /** * Resume pointer'ı döndürür * * @return array */ public function getResumePointer(): array { return $this->state['resume_pointer']; } /** * Resume edilebilir mi kontrol eder * * @return bool */ public function canResume(): bool { // Paused veya running durumunda ve resume pointer varsa devam edilebilir $resumableStatuses = ['paused', 'running']; return in_array($this->state['status'], $resumableStatuses) && !empty($this->state['resume_pointer']); } /** * Resume pointer'ı temizler */ public function clearResumePointer(): void { $this->state['resume_pointer'] = []; $this->save(); } /** * Mesaj ekler (log) * * @param string $message Mesaj * @param string $level Log seviyesi: info, warning, error */ public function addMessage(string $message, string $level = 'info'): void { $this->state['messages'][] = [ 'time' => gmdate('H:i:s'), 'level' => $level, 'message' => $message, ]; // Son 1000 mesajı tut if (count($this->state['messages']) > 1000) { $this->state['messages'] = array_slice($this->state['messages'], -1000); } $this->save(); } /** * Hata durumunu ayarlar * * @param string $message Hata mesajı */ public function setError(string $message): void { $this->state['status'] = 'error'; $this->state['error'] = $message; // Mevcut adımı da error olarak işaretle if (!empty($this->state['current_step'])) { $step = $this->state['current_step']; if (isset($this->state['steps'][$step])) { $this->state['steps'][$step]['status'] = 'error'; $this->state['steps'][$step]['message'] = $message; } } $this->addMessage($message, 'error'); $this->save(); } /** * İşlemi tamamlandı olarak işaretler * * @param mixed $result Sonuç verisi (opsiyonel) */ public function markComplete($result = null): void { $this->state['status'] = 'completed'; $this->state['overall_progress'] = 100; $this->state['result'] = $result; // Tüm adımları completed yap foreach ($this->state['steps'] as $key => &$step) { if ($step['status'] !== 'error') { $step['status'] = 'completed'; $step['progress'] = 100; } } $this->addMessage('İşlem başarıyla tamamlandı.', 'info'); $this->save(); } /** * İşlemi duraklatır */ public function pause(): void { $this->state['status'] = 'paused'; $this->addMessage('İşlem duraklatıldı.', 'info'); $this->save(); } /** * İşlemi devam ettirir */ public function resume(): void { $this->state['status'] = 'running'; $this->addMessage('İşlem devam ediyor.', 'info'); $this->save(); } /** * Mevcut ilerleme durumunu döndürür * * @return array */ public function getProgress(): array { return [ 'job_id' => $this->jobId, 'type' => $this->state['type'], 'status' => $this->state['status'], 'current_step' => $this->state['current_step'], 'current_step_label' => $this->state['steps'][$this->state['current_step']]['label'] ?? '', 'current_step_progress' => $this->state['current_step_progress'], 'overall_progress' => $this->state['overall_progress'], 'steps' => $this->state['steps'], 'messages' => $this->state['messages'], // Tüm mesajları gönder 'error' => $this->state['error'], 'result' => $this->state['result'], 'can_resume' => $this->canResume(), ]; } /** * Tüm state verisini döndürür * * @return array */ public function getState(): array { return $this->state; } /** * Job ID'yi döndürür * * @return string */ public function getJobId(): string { return $this->jobId; } /** * Chunk config'i geri yükler * * @return ChunkConfig|null */ public function getChunkConfig(): ?ChunkConfig { if (empty($this->state['chunk_config'])) { return null; } return ChunkConfig::fromArray($this->state['chunk_config']); } /** * State dosyasını ve kaynakları temizler */ public function cleanup(): void { if (file_exists($this->stateFile)) { wp_delete_file($this->stateFile); } } /** * Eski state dosyalarını temizler (24 saatten eski) * * @param int $maxAge Saniye cinsinden maksimum yaş (varsayılan: 24 saat) */ public static function cleanupOldStates(int $maxAge = 86400): void { $stateDir = ALLDAMI_STATE_DIR; if (!is_dir($stateDir)) { return; } $files = glob($stateDir . '*.json'); $now = time(); foreach ($files as $file) { $mtime = filemtime($file); if ($now - $mtime > $maxAge) { wp_delete_file($file); } } } /** * Unique job ID oluşturur * * @return string */ private function generateJobId(): string { return sprintf( '%s_%s_%s', gmdate('Ymd_His'), substr(md5(uniqid((string) wp_rand(), true)), 0, 8), wp_generate_password(4, false, false) ); } /** * State dosyası yolunu döndürür * * @param string $jobId Job ID * @return string */ private function getStateFilePath(string $jobId): string { return ALLDAMI_STATE_DIR . $jobId . '.json'; } /** * Eski state dosyalarını temizler */ private function cleanupOldStateFiles(): void { $stateDir = ALLDAMI_STATE_DIR; if (!is_dir($stateDir)) { return; } $iterator = new \DirectoryIterator($stateDir); $now = time(); $ttl = 3600; // 1 saat foreach ($iterator as $fileinfo) { if ($fileinfo->isDot()) continue; if ($fileinfo->isFile() && $fileinfo->getExtension() === 'json') { if ($now - $fileinfo->getMTime() > $ttl) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink @unlink($fileinfo->getPathname()); } } } } /** * Belirli bir job'ın var olup olmadığını kontrol eder * * @param string $jobId Job ID * @return bool */ public static function exists(string $jobId): bool { $stateFile = ALLDAMI_STATE_DIR . $jobId . '.json'; return file_exists($stateFile); } /** * Tüm aktif job'ları listeler * * @return array */ public static function listActiveJobs(): array { $stateDir = ALLDAMI_STATE_DIR; $jobs = []; if (!is_dir($stateDir)) { return $jobs; } $files = glob($stateDir . '*.json'); foreach ($files as $file) { $content = file_get_contents($file); $data = json_decode($content, true); if ($data && in_array($data['status'], ['running', 'paused'])) { $jobs[] = [ 'job_id' => basename($file, '.json'), 'type' => $data['type'] ?? '', 'status' => $data['status'], 'progress' => $data['overall_progress'] ?? 0, 'created_at' => $data['created_at'] ?? '', ]; } } return $jobs; } }