argument('file'); if (!is_file($file) || !is_readable($file)) { $this->error('File backup tidak ditemukan atau tidak bisa dibaca: ' . $file); return Command::FAILURE; } $baseUrl = rtrim((string) ($this->option('url') ?: config('cloud_backup.api_url')), '/'); $token = (string) ($this->option('token') ?: config('cloud_backup.api_token')); $backupName = (string) ($this->option('name') ?: config('cloud_backup.backup_name', 'lismikro')); $chunkSize = (int) ($this->option('chunk-size') ?: config('cloud_backup.chunk_size', 5 * 1024 * 1024)); $retries = max(1, (int) $this->option('retries')); $retryDelay = max(0, (int) $this->option('retry-delay')); $timeout = max(30, (int) $this->option('timeout')); if ($baseUrl === '' || $token === '') { $this->error('CLOUD_BACKUP_API_URL dan CLOUD_BACKUP_API_TOKEN wajib diisi.'); return Command::FAILURE; } if ($chunkSize < 1024 * 1024) { $this->error('Chunk size minimal 1 MB.'); return Command::FAILURE; } $size = filesize($file); $checksum = hash_file('sha256', $file); $totalChunks = (int) ceil($size / $chunkSize); $uploadId = now()->format('YmdHis') . '_' . Str::random(16); $endpoint = $baseUrl . '/api/backups/chunk'; $this->info('Upload backup: ' . basename($file)); $this->info('Upload ID: ' . $uploadId); $this->info('Total chunk: ' . $totalChunks); $handle = fopen($file, 'rb'); if (!$handle) { $this->error('Gagal membuka file backup.'); return Command::FAILURE; } try { for ($index = 0; $index < $totalChunks; $index++) { $chunk = fread($handle, $chunkSize); if ($chunk === false) { $this->error('Gagal membaca chunk ke-' . $index); return Command::FAILURE; } $response = $this->uploadChunk( endpoint: $endpoint, token: $token, chunk: $chunk, chunkIndex: $index, payload: [ 'backup_name' => $backupName, 'upload_id' => $uploadId, 'filename' => basename($file), 'chunk_index' => $index, 'total_chunks' => $totalChunks, 'total_size' => $size, 'checksum' => $checksum, ], retries: $retries, retryDelay: $retryDelay, timeout: $timeout ); if (!$response || !$response->successful()) { $this->error('Upload chunk ke-' . $index . ' gagal. HTTP ' . ($response?->status() ?: '-')); $this->line($response?->body() ?: 'Tidak ada response dari server.'); return Command::FAILURE; } $this->line('Chunk ' . ($index + 1) . '/' . $totalChunks . ' terkirim.'); } } finally { fclose($handle); } $this->info('Upload backup selesai.'); return Command::SUCCESS; } private function uploadChunk( string $endpoint, string $token, string $chunk, int $chunkIndex, array $payload, int $retries, int $retryDelay, int $timeout ): ?Response { $response = null; for ($attempt = 1; $attempt <= $retries; $attempt++) { try { $response = Http::withToken($token) ->timeout($timeout) ->attach('chunk', $chunk, 'chunk_' . $chunkIndex . '.part') ->post($endpoint, $payload); if ($response->successful()) { return $response; } $this->warn('Chunk ' . ($chunkIndex + 1) . ' gagal percobaan ' . $attempt . '/' . $retries . '. HTTP ' . $response->status()); } catch (\Throwable $e) { $this->warn('Chunk ' . ($chunkIndex + 1) . ' gagal percobaan ' . $attempt . '/' . $retries . ': ' . $e->getMessage()); } if ($attempt < $retries && $retryDelay > 0) { sleep($retryDelay); } } return $response; } }