option('path') ?? storage_path('app/backups'); $pretty = $this->option('pretty') ?? false; $tablesFilter = $this->option('tables') ? array_map('trim', explode(',', $this->option('tables'))) : null; if (! File::isDirectory($path)) { File::makeDirectory($path, 0755, true); } $timestamp = now()->format('Y-m-d_His'); $backupDir = "{$path}/backup_{$timestamp}"; File::makeDirectory($backupDir, 0755, true); $this->info("Backup directory: {$backupDir}"); $this->newLine(); $tables = $this->getTables(); if ($tablesFilter) { $tables = array_filter($tables, fn ($t) => in_array($t, $tablesFilter)); } $this->info("Exporting " . count($tables) . " tables..."); $this->newLine(); $bar = $this->output->createProgressBar(count($tables)); $bar->start(); $totalRows = 0; foreach ($tables as $table) { $count = $this->exportTable($table, $backupDir, $pretty); $this->stats[$table] = $count; $totalRows += $count; $bar->advance(); } $bar->finish(); $this->newLine(2); // Write manifest $manifest = [ 'created_at' => now()->toIso8601String(), 'database' => config('database.default'), 'tables' => $this->stats, 'total_rows' => $totalRows, ]; File::put("{$backupDir}/manifest.json", json_encode($manifest, JSON_PRETTY_PRINT)); $this->printSummary($backupDir, $totalRows); ActivityLogger::log( 'export', __('feedback.log_export_done', ['tables' => count($tables), 'rows' => $totalRows]), null, [ 'tables' => $this->stats, 'total_rows' => $totalRows, 'path' => $backupDir, ], ); return self::SUCCESS; } private function getTables(): array { $driver = config('database.default'); if ($driver === 'sqlite') { $results = DB::select("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"); return array_map(fn ($r) => $r->name, $results); } return DB::getDoctrineSchemaManager()->listTableNames(); } private function exportTable(string $table, string $backupDir, bool $pretty): int { $filename = "{$backupDir}/{$table}.json"; $count = 0; $handle = fopen($filename, 'w'); fwrite($handle, "[\n"); $first = true; DB::table($table)->orderBy('id')->chunk(500, function ($rows) use ($handle, &$count, &$first, $pretty) { foreach ($rows as $row) { if (! $first) { fwrite($handle, ",\n"); } $first = false; $data = (array) $row; $json = $pretty ? json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : json_encode($data, JSON_UNESCAPED_UNICODE); fwrite($handle, $json); $count++; } }); fwrite($handle, "\n]\n"); fclose($handle); return $count; } private function printSummary(string $backupDir, int $totalRows): void { $rows = []; foreach ($this->stats as $table => $count) { $rows[] = [$table, $count]; } $rows[] = ['TOTAL', "{$totalRows}"]; $this->table(['Table', 'Rows'], $rows); $size = File::size($backupDir); $sizeFormatted = $this->formatBytes($size); $this->info("Backup size: {$sizeFormatted}"); $this->info("Location: {$backupDir}"); } private function formatBytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB']; $i = 0; while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; } return round($bytes, 2) . ' ' . $units[$i]; } }