From 0e16ea60525101d4244f699d7787ac8151f256de Mon Sep 17 00:00:00 2001 From: SajjadMahmoody <117562560+SajjadMahmoody@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:16:34 +0330 Subject: [PATCH] fix: retry failed file transfers on next sync using pending queue in osurg_initials --- app/Services/SyncService.php | 333 ++++++++++++++++++++--------------- 1 file changed, 194 insertions(+), 139 deletions(-) diff --git a/app/Services/SyncService.php b/app/Services/SyncService.php index d61d269..2eaf775 100644 --- a/app/Services/SyncService.php +++ b/app/Services/SyncService.php @@ -118,15 +118,15 @@ public function sync(): array } else { $report[] = 'هشدار: peer_cursor پیش نرفت — برخی تغییرات دریافتی اعمال نشد و دفعه بعد retry می‌شه.'; } - - if ($this->isFtpConfigured()) { - $this->transferFilesViaFtp($peerChanges, 's2c', $report); - } else { - $this->syncFilesViaHttp($peerChanges, $baseUrl, $report); - } } - $report[] = count($peerChanges) . ' تغییر از سیستم مقابل دریافت شد.'; + if ($this->isFtpConfigured()) { + $this->transferFilesViaFtp($peerChanges ?? [], 's2c', $report); + } else { + $this->syncFilesViaHttp($peerChanges ?? [], $baseUrl, $report); + } + + $report[] = count($peerChanges ?? []) . ' تغییر از سیستم مقابل دریافت شد.'; } else { $report[] = 'خطا در دریافت تغییرات از سیستم مقابل: ' . $pullResponse->status(); } @@ -196,20 +196,9 @@ public function applyChanges(array $changes): array private function transferFilesViaFtp(array $changes, string $side, array &$report): void { - if (empty($changes)) { - return; - } - $ftp = $this->connectFtp(); - if (! $ftp) { - $report[] = 'خطا در اتصال FTP به سیستم مقابل'; - Log::error('FTP connection failed in transferFilesViaFtp'); - return; - } - $fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files']; - $localBase = rtrim(Storage::disk('public')->path(''), '/\\'); - $remoteBase = rtrim($this->ftpRemotePath, '/\\'); + $currentPaths = []; foreach ($changes as $change) { $data = $change['changed_data'] ?? []; foreach ($fileFields as $field) { @@ -218,101 +207,109 @@ private function transferFilesViaFtp(array $changes, string $side, array &$repor } $files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? []; foreach ($files as $filePath) { - if (! $filePath) { - continue; - } - - $filePath = ltrim(str_replace('\\', '/', $filePath), '/'); - $localFile = $localBase . '/' . $filePath; - $remoteFile = $remoteBase . '/' . $filePath; - $remoteFolder = dirname($remoteFile); - - if ($side === 'c2s') { - if (! file_exists($localFile)) { - $errorMsg = "فایل [{$filePath}] در سیستم محلی یافت نشد"; - $report[] = $errorMsg . ' ✗'; - Log::warning('FTP upload skipped - local file not found', [ - 'file' => $filePath, - 'local_path' => $localFile, - ]); - continue; - } - - if (! $this->createFtpDirectory($ftp, $remoteFolder)) { - $errorMsg = "ایجاد پوشه [{$remoteFolder}] برای فایل [{$filePath}] شکست خورد"; - $report[] = $errorMsg . ' ✗'; - Log::error('FTP mkdir failed', [ - 'file' => $filePath, - 'remote_folder' => $remoteFolder, - ]); - continue; - } - - if (ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY)) { - $report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓"; - Log::info('FTP file uploaded', [ - 'file' => $filePath, - 'size' => filesize($localFile), - ]); - } else { - $errorDetail = error_get_last(); - $errorMsg = "فایل [{$filePath}] ارسال نشد (FTP)"; - if ($errorDetail) { - $errorMsg .= " - " . $errorDetail['message']; - } - $report[] = $errorMsg . ' ✗'; - Log::error('FTP upload failed', [ - 'file' => $filePath, - 'local_file' => $localFile, - 'remote_file' => $remoteFile, - 'error' => $errorDetail, - ]); - } - } else { - if (file_exists($localFile)) { - Log::debug('FTP download skipped - file already exists', [ - 'file' => $filePath, - ]); - continue; - } - - $localFolder = dirname($localFile); - if (! is_dir($localFolder)) { - if (! mkdir($localFolder, 0755, true)) { - $report[] = "ایجاد پوشه [{$localFolder}] شکست خورد ✗"; - Log::error('Local mkdir failed', [ - 'folder' => $localFolder, - 'error' => error_get_last(), - ]); - continue; - } - } - - if (ftp_get($ftp, $localFile, $remoteFile, FTP_BINARY)) { - $report[] = "فایل [{$filePath}] دریافت شد (FTP) ✓"; - Log::info('FTP file downloaded', [ - 'file' => $filePath, - 'size' => filesize($localFile), - ]); - } else { - $errorDetail = error_get_last(); - $errorMsg = "فایل [{$filePath}] دریافت نشد (FTP)"; - if ($errorDetail) { - $errorMsg .= " - " . $errorDetail['message']; - } - $report[] = $errorMsg . ' ✗'; - Log::error('FTP download failed', [ - 'file' => $filePath, - 'local_file' => $localFile, - 'remote_file' => $remoteFile, - 'error' => $errorDetail, - ]); - } + if ($filePath) { + $currentPaths[] = ltrim(str_replace('\\', '/', $filePath), '/'); } } } } + $pendingPaths = $this->getPendingFiles($side); + $allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths))); + + if (empty($allPaths)) { + return; + } + + $ftp = $this->connectFtp(); + if (! $ftp) { + $report[] = 'خطا در اتصال FTP به سیستم مقابل'; + Log::error('FTP connection failed in transferFilesViaFtp'); + return; + } + + $localBase = rtrim(Storage::disk('public')->path(''), '/\\'); + $remoteBase = rtrim($this->ftpRemotePath, '/\\'); + $failedPaths = []; + + foreach ($allPaths as $filePath) { + $localFile = $localBase . '/' . $filePath; + $remoteFile = $remoteBase . '/' . $filePath; + $remoteFolder = dirname($remoteFile); + + if ($side === 'c2s') { + if (! file_exists($localFile)) { + $report[] = "فایل [{$filePath}] در سیستم محلی یافت نشد ✗"; + Log::warning('FTP upload skipped - local file not found', [ + 'file' => $filePath, + 'local_path' => $localFile, + ]); + continue; + } + + if (! $this->createFtpDirectory($ftp, $remoteFolder)) { + $report[] = "ایجاد پوشه [{$remoteFolder}] برای فایل [{$filePath}] شکست خورد ✗"; + Log::error('FTP mkdir failed', ['file' => $filePath, 'remote_folder' => $remoteFolder]); + $failedPaths[] = $filePath; + continue; + } + + if (ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY)) { + $report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓"; + Log::info('FTP file uploaded', ['file' => $filePath, 'size' => filesize($localFile)]); + } else { + $errorDetail = error_get_last(); + $errorMsg = "فایل [{$filePath}] ارسال نشد (FTP)"; + if ($errorDetail) { + $errorMsg .= ' - ' . $errorDetail['message']; + } + $report[] = $errorMsg . ' ✗'; + $failedPaths[] = $filePath; + Log::error('FTP upload failed', [ + 'file' => $filePath, 'local_file' => $localFile, + 'remote_file' => $remoteFile, 'error' => $errorDetail, + ]); + } + } else { + if (file_exists($localFile)) { + Log::debug('FTP download skipped - file already exists', ['file' => $filePath]); + continue; + } + + $localFolder = dirname($localFile); + if (! is_dir($localFolder)) { + if (! mkdir($localFolder, 0755, true)) { + $report[] = "ایجاد پوشه [{$localFolder}] شکست خورد ✗"; + $failedPaths[] = $filePath; + Log::error('Local mkdir failed', ['folder' => $localFolder, 'error' => error_get_last()]); + continue; + } + } + + if (ftp_get($ftp, $localFile, $remoteFile, FTP_BINARY)) { + $report[] = "فایل [{$filePath}] دریافت شد (FTP) ✓"; + Log::info('FTP file downloaded', ['file' => $filePath, 'size' => filesize($localFile)]); + } else { + $errorDetail = error_get_last(); + $errorMsg = "فایل [{$filePath}] دریافت نشد (FTP)"; + if ($errorDetail) { + $errorMsg .= ' - ' . $errorDetail['message']; + } + $report[] = $errorMsg . ' ✗'; + $failedPaths[] = $filePath; + Log::error('FTP download failed', [ + 'file' => $filePath, 'local_file' => $localFile, + 'remote_file' => $remoteFile, 'error' => $errorDetail, + ]); + } + } + } + + $this->savePendingFiles($side, $failedPaths); + if (! empty($failedPaths)) { + $report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.'; + } + ftp_close($ftp); Log::info('FTP connection closed'); } @@ -391,6 +388,7 @@ private function pushFilesViaHttp(array $changes, string $baseUrl, array &$repor { $fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files']; + $currentPaths = []; foreach ($changes as $change) { $data = $change['changed_data'] ?? []; foreach ($fileFields as $field) { @@ -399,32 +397,51 @@ private function pushFilesViaHttp(array $changes, string $baseUrl, array &$repor } $files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? []; foreach ($files as $filePath) { - if (! $filePath || ! Storage::disk('public')->exists($filePath)) { - continue; - } - try { - $response = Http::timeout(600) - ->withHeader('X-Sync-Token', $this->token) - ->attach('file', Storage::disk('public')->get($filePath), basename($filePath)) - ->post("{$baseUrl}/api/sync/receive-file", ['path' => $filePath]); - - if ($response->successful()) { - $report[] = "فایل [{$filePath}] ارسال شد (HTTP) ✓"; - } else { - $report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $response->status(); - } - } catch (\Throwable $e) { - $report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $e->getMessage(); + if ($filePath) { + $currentPaths[] = ltrim(str_replace('\\', '/', $filePath), '/'); } } } } + + $pendingPaths = $this->getPendingFiles('c2s'); + $allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths))); + $failedPaths = []; + + foreach ($allPaths as $filePath) { + if (! Storage::disk('public')->exists($filePath)) { + $report[] = "فایل [{$filePath}] در سیستم محلی یافت نشد ✗"; + continue; + } + try { + $response = Http::timeout(600) + ->withHeader('X-Sync-Token', $this->token) + ->attach('file', Storage::disk('public')->get($filePath), basename($filePath)) + ->post("{$baseUrl}/api/sync/receive-file", ['path' => $filePath]); + + if ($response->successful()) { + $report[] = "فایل [{$filePath}] ارسال شد (HTTP) ✓"; + } else { + $report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $response->status(); + $failedPaths[] = $filePath; + } + } catch (\Throwable $e) { + $report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $e->getMessage(); + $failedPaths[] = $filePath; + } + } + + $this->savePendingFiles('c2s', $failedPaths); + if (! empty($failedPaths)) { + $report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.'; + } } private function syncFilesViaHttp(array $changes, string $baseUrl, array &$report): void { $fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files']; + $currentPaths = []; foreach ($changes as $change) { $data = $change['changed_data'] ?? []; foreach ($fileFields as $field) { @@ -433,24 +450,43 @@ private function syncFilesViaHttp(array $changes, string $baseUrl, array &$repor } $files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? []; foreach ($files as $filePath) { - if (! $filePath || Storage::disk('public')->exists($filePath)) { - continue; - } - try { - $response = Http::timeout(60) - ->withHeader('X-Sync-Token', $this->token) - ->get("{$baseUrl}/api/sync/file", ['path' => $filePath]); - - if ($response->successful()) { - Storage::disk('public')->put($filePath, $response->body()); - $report[] = "فایل [{$filePath}] دریافت شد (HTTP) ✓"; - } - } catch (\Throwable $e) { - $report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $e->getMessage(); + if ($filePath) { + $currentPaths[] = ltrim(str_replace('\\', '/', $filePath), '/'); } } } } + + $pendingPaths = $this->getPendingFiles('s2c'); + $allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths))); + $failedPaths = []; + + foreach ($allPaths as $filePath) { + if (Storage::disk('public')->exists($filePath)) { + continue; + } + try { + $response = Http::timeout(60) + ->withHeader('X-Sync-Token', $this->token) + ->get("{$baseUrl}/api/sync/file", ['path' => $filePath]); + + if ($response->successful()) { + Storage::disk('public')->put($filePath, $response->body()); + $report[] = "فایل [{$filePath}] دریافت شد (HTTP) ✓"; + } else { + $report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $response->status(); + $failedPaths[] = $filePath; + } + } catch (\Throwable $e) { + $report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $e->getMessage(); + $failedPaths[] = $filePath; + } + } + + $this->savePendingFiles('s2c', $failedPaths); + if (! empty($failedPaths)) { + $report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.'; + } } private function applyChange(array $change, string $prefix, array &$report): void @@ -484,6 +520,25 @@ private function applyChange(array $change, string $prefix, array &$report): voi } } + private function getPendingFiles(string $direction): array + { + $record = \App\Models\OsurgInitial::where('init_parameter', "sync_pending_{$direction}")->first(); + if (! $record || ! $record->attachment) { + return []; + } + + return json_decode($record->attachment, true) ?: []; + } + + private function savePendingFiles(string $direction, array $files): void + { + $files = array_values(array_unique(array_filter($files))); + \App\Models\OsurgInitial::updateOrCreate( + ['init_parameter' => "sync_pending_{$direction}"], + ['attachment' => empty($files) ? null : json_encode($files, JSON_UNESCAPED_UNICODE)], + ); + } + private function saveOurCursor(string $cursor): void { \App\Models\OsurgInitial::updateOrCreate(