feat: granular permissions, export backup, user/role management
- PermissionSeeder: 29 permissions with role mapping (admin/manager/staff) - Policies updated to use hasPermissionTo() instead of hasRole() - ExportBackup command: JSON per-table backup with chunk processing - UserResource: CRUD users with role assignment (admin only) - RoleResource: CRUD roles with permission assignment (admin only) - UserPolicy + RolePolicy: admin-only access control - ImportCskh: detailed skip logging with color in dry-run mode - Widgets: permission-based visibility checks - Tests: 8/8 pass (21 assertions)
This commit is contained in:
146
app/Console/Commands/ExportBackup.php
Normal file
146
app/Console/Commands/ExportBackup.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ExportBackup extends Command
|
||||
{
|
||||
protected $signature = 'app:export-backup
|
||||
{--path= : Output directory path (default: storage/app/backups)}
|
||||
{--tables= : Comma-separated list of tables to export (default: all)}
|
||||
{--pretty : Pretty-print JSON output}';
|
||||
|
||||
protected $description = 'Export database backup to JSON files (one file per table)';
|
||||
|
||||
private array $stats = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = $this->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);
|
||||
|
||||
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[] = ['<bold>TOTAL</bold>', "<bold>{$totalRows}</bold>"];
|
||||
|
||||
$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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user