netgescon-day0/app/Console/Commands/NetgesconRestoreRecordCommand.php

139 lines
4.4 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class NetgesconRestoreRecordCommand extends Command
{
protected $signature = 'netgescon:restore-record
{table : Tabella da ripristinare}
{pk : Valore chiave primaria (default id)}
{--snapshot-id= : Snapshot specifico (default ultimo)}
{--pk-column=id : Nome colonna PK}
{--apply : Esegue il ripristino (altrimenti solo preview)}';
protected $description = 'Ripristina una singola scrittura da snapshot differenziale pre-update.';
public function handle(): int
{
$table = trim((string) $this->argument('table'));
$pk = trim((string) $this->argument('pk'));
$pkColumn = trim((string) $this->option('pk-column')) ?: 'id';
if ($table === '' || $pk === '') {
$this->error('Parametri table/pk obbligatori.');
return self::FAILURE;
}
$snapshotDir = $this->resolveSnapshotDir();
if ($snapshotDir === null) {
$this->error('Snapshot non trovato.');
return self::FAILURE;
}
$file = $snapshotDir . '/db/' . $table . '.ndjson';
if (! is_file($file)) {
$this->error('Export tabella non trovato nello snapshot: ' . $file);
return self::FAILURE;
}
$record = $this->findRecordByPk($file, $pk);
if (! is_array($record)) {
$this->error('Record non trovato nello snapshot per PK=' . $pk);
return self::FAILURE;
}
$this->line('Snapshot: ' . $snapshotDir);
$this->line('Tabella: ' . $table . ' · PK: ' . $pkColumn . '=' . $pk);
$this->line('Hash: ' . (string) ($record['hash'] ?? '-'));
$data = is_array($record['data'] ?? null) ? (array) $record['data'] : [];
if (! array_key_exists($pkColumn, $data)) {
$data[$pkColumn] = $pk;
}
if (! (bool) $this->option('apply')) {
$this->warn('Preview mode: nessuna scrittura eseguita. Usa --apply per ripristinare.');
$this->line(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return self::SUCCESS;
}
if (! DB::getSchemaBuilder()->hasTable($table)) {
$this->error('Tabella non esistente su DB corrente: ' . $table);
return self::FAILURE;
}
DB::table($table)->updateOrInsert(
[$pkColumn => $data[$pkColumn]],
$data
);
$this->info('Record ripristinato con successo.');
return self::SUCCESS;
}
private function resolveSnapshotDir(): ?string
{
$snapshotId = trim((string) $this->option('snapshot-id'));
$root = storage_path('app/update-backups/snapshots');
if ($snapshotId !== '') {
$dir = $root . '/' . $snapshotId;
return is_dir($dir) ? $dir : null;
}
$latest = storage_path('app/update-backups/latest-manifest.json');
if (is_file($latest)) {
$decoded = json_decode((string) @file_get_contents($latest), true);
if (is_array($decoded) && isset($decoded['snapshot_dir']) && is_string($decoded['snapshot_dir']) && is_dir($decoded['snapshot_dir'])) {
return $decoded['snapshot_dir'];
}
}
if (! is_dir($root)) {
return null;
}
$dirs = array_values(array_filter(scandir($root) ?: [], function (string $name) use ($root): bool {
return $name !== '.' && $name !== '..' && is_dir($root . '/' . $name);
}));
rsort($dirs, SORT_NATURAL);
if ($dirs === []) {
return null;
}
return $root . '/' . $dirs[0];
}
/** @return array<string,mixed>|null */
private function findRecordByPk(string $file, string $pk): ?array
{
$handle = fopen($file, 'rb');
if (! is_resource($handle)) {
return null;
}
try {
while (($line = fgets($handle)) !== false) {
$decoded = json_decode((string) $line, true);
if (! is_array($decoded)) {
continue;
}
$candidate = (string) ($decoded['pk'] ?? '');
if ($candidate === $pk) {
return $decoded;
}
}
} finally {
fclose($handle);
}
return null;
}
}