peerIp !== '' && $this->token !== ''; } public function isFtpConfigured(): bool { return $this->peerIp !== '' && $this->ftpUser !== '' && $this->ftpRemotePath !== ''; } public function sync(): array { $report = []; $baseUrl = "http://{$this->peerIp}:{$this->peerPort}"; $ourLastSync = SyncLog::lastSyncId(); try { $pullResponse = Http::timeout(30) ->withHeader('X-Sync-Token', $this->token) ->get("{$baseUrl}/api/sync/export", ['since' => $peerCursor]); if ($pullResponse->successful()) { $peerChanges = $pullResponse->json('changes', []); if (! empty($peerChanges)) { app()->instance('sync.applying', true); DB::transaction(function () use ($peerChanges, &$report) { foreach ($peerChanges as $change) { $this->applyChange($change, 'دریافت', $report); } }); $maxPeerChangeId = max(array_column($peerChanges, 'id')); $this->savePeerCursor(max($peerCursor, $maxPeerChangeId)); if ($this->isFtpConfigured()) { $this->transferFilesViaFtp($peerChanges, 's2c', $report); } else { $this->syncFilesViaHttp($peerChanges, $baseUrl, $report); } } $report[] = count($peerChanges) . ' تغییر از سیستم مقابل دریافت شد.'; } else { $report[] = 'خطا در دریافت تغییرات از سیستم مقابل: ' . $pullResponse->status(); } } catch (\Throwable $e) { $report[] = 'خطا در اتصال به سیستم مقابل: ' . $e->getMessage(); return ['success' => false, 'report' => $report]; } $ourChanges = SyncLog::changesSince($ourLastSync)->toArray(); try { $pushResponse = Http::timeout(120) ->withHeader('X-Sync-Token', $this->token) ->post("{$baseUrl}/api/sync/apply", ['changes' => $ourChanges]); if ($pushResponse->successful()) { foreach ($pushResponse->json('report', []) as $line) { $report[] = 'ارسال: ' . $line; } $report[] = count($ourChanges) . ' تغییر به سیستم مقابل ارسال شد.'; if ($this->isFtpConfigured()) { $this->transferFilesViaFtp($ourChanges, 'c2s', $report); } else { $this->pushFilesViaHttp($ourChanges, $baseUrl, $report); } } else { $report[] = 'خطا در ارسال تغییرات به سیستم مقابل: ' . $pushResponse->status(); } } catch (\Throwable $e) { $report[] = 'خطا در ارسال به سیستم مقابل: ' . $e->getMessage(); } SyncLog::markSynced(request()->ip() ?? '127.0.0.1'); return ['success' => true, 'report' => $report]; } public function applyChanges(array $changes): array { $report = []; app()->instance('sync.applying', true); DB::transaction(function () use ($changes, &$report) { foreach ($changes as $change) { $this->applyChange($change, '', $report); } }); SyncLog::markSynced(request()->ip() ?? ''); return $report; } private function transferFilesViaFtp(array $changes, string $side, array &$report): void { if (empty($changes)) { return; } $ftp = $this->connectFtp(); if (! $ftp) { $report[] = 'خطا در اتصال FTP به سیستم مقابل'; return; } $fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files']; $localBase = rtrim(Storage::disk('public')->path(''), '/\\'); $remoteBase = rtrim($this->ftpRemotePath, '/\\'); foreach ($changes as $change) { $data = $change['changed_data'] ?? []; foreach ($fileFields as $field) { if (empty($data[$field])) { continue; } $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)) { continue; } @ftp_mkdir($ftp, $remoteFolder); if (ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY)) { $report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓"; } else { $report[] = "فایل [{$filePath}] ارسال نشد (FTP) ✗"; } } else { if (file_exists($localFile)) { continue; } $localFolder = dirname($localFile); if (! is_dir($localFolder)) { mkdir($localFolder, 0755, true); } if (ftp_get($ftp, $localFile, $remoteFile, FTP_BINARY)) { $report[] = "فایل [{$filePath}] دریافت شد (FTP) ✓"; } else { $report[] = "فایل [{$filePath}] دریافت نشد (FTP) ✗"; } } } } } ftp_close($ftp); } private function connectFtp(): mixed { if (! function_exists('ftp_connect')) { return null; } $ftp = @ftp_connect($this->peerIp, $this->ftpPort, 30); if (! $ftp) { return null; } if (! @ftp_login($ftp, $this->ftpUser, $this->ftpPass)) { ftp_close($ftp); return null; } ftp_pasv($ftp, true); return $ftp; } private function pushFilesViaHttp(array $changes, string $baseUrl, array &$report): void { $fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files']; foreach ($changes as $change) { $data = $change['changed_data'] ?? []; foreach ($fileFields as $field) { if (empty($data[$field])) { continue; } $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(); } } } } } private function syncFilesViaHttp(array $changes, string $baseUrl, array &$report): void { $fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files']; foreach ($changes as $change) { $data = $change['changed_data'] ?? []; foreach ($fileFields as $field) { if (empty($data[$field])) { continue; } $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(); } } } } } private function applyChange(array $change, string $prefix, array &$report): void { $table = $change['table_name']; $recordId = $change['record_id']; $action = $change['action']; $data = $change['changed_data'] ?? []; $label = $prefix ? "{$prefix} [{$table}#{$recordId}]" : "[{$table}#{$recordId}]"; try { if ($action === 'deleted') { DB::table($table)->where('id', $recordId)->delete(); $report[] = "{$label} حذف ✓"; } elseif ($action === 'created') { $exists = DB::table($table)->where('id', $recordId)->exists(); if ($exists) { DB::table($table)->where('id', $recordId)->update($this->sanitize($data)); $report[] = "{$label} ایجاد→بروزرسانی ✓"; } else { DB::table($table)->insert($this->sanitize(array_merge(['id' => $recordId], $data))); $report[] = "{$label} ایجاد ✓"; } } elseif ($action === 'updated') { DB::table($table)->where('id', $recordId)->update($this->sanitize($data)); $report[] = "{$label} بروزرسانی ✓"; } } catch (\Throwable $e) { $report[] = "{$label} {$action} ✗ " . $e->getMessage(); } } private function savePeerCursor(int $cursor): void { \App\Models\OsurgInitial::updateOrCreate( ['init_parameter' => 'sync_peer_cursor'], ['init_value' => $cursor], ); cache()->forget('osurg_initial.sync_peer_cursor'); } private function sanitize(array $data): array { $result = []; foreach ($data as $key => $value) { if (is_array($value)) { $result[$key] = json_encode($value, JSON_UNESCAPED_UNICODE); } elseif (is_scalar($value) || $value === null) { $result[$key] = $value; } } return $result; } }