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

170 lines
6.3 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use ZipArchive;
class NetgesconDistributionPullCommand extends Command
{
protected $signature = 'netgescon:update
{--channel= : Canale update (free/licensed)}
{--code= : Codice autorizzazione licensed}
{--auth= : Token autorizzazione licensed}
{--apply : Applica update se disponibile}
{--dry-run : Simula senza scrivere file}
{--force : Applica anche se versione uguale o inferiore}';
protected $description = 'Controlla e applica aggiornamenti NetGescon dal server distribuzione';
public function handle(): int
{
$baseUrl = rtrim((string) config('distribution.update_url', ''), '/');
if ($baseUrl === '') {
$this->error('NETGESCON_UPDATE_URL non configurato.');
return self::FAILURE;
}
$channel = (string) ($this->option('channel') ?: config('distribution.default_channel', 'free'));
$code = (string) ($this->option('code') ?: config('distribution.licensed_code', ''));
$auth = (string) ($this->option('auth') ?: config('distribution.licensed_auth', ''));
$nodeCode = (string) config('distribution.node_code', '');
$manifestUrl = $baseUrl . '/api/v1/distribution/updates/manifest';
$query = ['channel' => $channel];
if ($channel === 'licensed') {
if ($code === '' || $auth === '') {
$this->error('Canale licensed richiede --code e --auth (o variabili NETGESCON_LICENSED_*).');
return self::FAILURE;
}
$query['code'] = $code;
$query['auth'] = $auth;
}
$timeout = (int) config('distribution.http_timeout_seconds', 60);
$this->info("Controllo aggiornamenti su {$manifestUrl} (channel={$channel})...");
$response = Http::timeout($timeout)
->acceptJson()
->withHeaders(array_filter([
'X-Netgescon-Node' => $nodeCode !== '' ? $nodeCode : null,
]))
->get($manifestUrl, $query);
if (! $response->successful()) {
$this->error('Manifest non raggiungibile: HTTP ' . $response->status());
return self::FAILURE;
}
/** @var array<string,mixed> $manifest */
$manifest = $response->json();
$remoteVersion = (string) ($manifest['version'] ?? '');
$packageUrl = (string) ($manifest['package_url'] ?? '');
$hash = strtolower((string) ($manifest['sha256'] ?? ''));
$migrateRequired = (bool) ($manifest['migrate_required'] ?? false);
if ($remoteVersion === '' || $packageUrl === '' || $hash === '') {
$this->error('Manifest incompleto: richiesti version/package_url/sha256.');
return self::FAILURE;
}
$currentVersion = (string) config('netgescon.version', '0.0.0');
$hasNewVersion = version_compare($remoteVersion, $currentVersion, '>');
$this->line('Versione corrente: ' . $currentVersion);
$this->line('Versione remota: ' . $remoteVersion);
if (! $hasNewVersion && ! $this->option('force')) {
$this->info('Nessun aggiornamento necessario.');
return self::SUCCESS;
}
if (! $this->option('apply')) {
$this->info('Update disponibile. Esegui con --apply per installarlo.');
return self::SUCCESS;
}
if ($this->option('dry-run')) {
$this->warn('Dry-run: update rilevato, nessuna modifica applicata.');
return self::SUCCESS;
}
$tmpRoot = storage_path('app/update-temp');
$backupRoot = storage_path('app/update-backups');
File::ensureDirectoryExists($tmpRoot);
File::ensureDirectoryExists($backupRoot);
$stamp = date('Ymd-His');
$zipPath = $tmpRoot . '/netgescon-update-' . $stamp . '.zip';
$extractDir = $tmpRoot . '/extract-' . $stamp;
$envBackup = $backupRoot . '/.env-' . $stamp;
$this->info('Download pacchetto...');
$download = Http::timeout($timeout)->withOptions(['stream' => true])->get($packageUrl);
if (! $download->successful()) {
$this->error('Download fallito: HTTP ' . $download->status());
return self::FAILURE;
}
File::put($zipPath, $download->body());
$calcHash = strtolower(hash_file('sha256', $zipPath) ?: '');
if ($calcHash !== $hash) {
$this->error('Hash SHA256 non valido. Atteso=' . $hash . ' calcolato=' . $calcHash);
return self::FAILURE;
}
$this->info('Backup .env...');
if (File::exists(base_path('.env'))) {
File::copy(base_path('.env'), $envBackup);
}
$this->info('Estrazione pacchetto...');
File::ensureDirectoryExists($extractDir);
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
$this->error('Impossibile aprire ZIP update.');
return self::FAILURE;
}
$zip->extractTo($extractDir);
$zip->close();
$this->info('Applicazione file (esclusi .env e storage)...');
$this->syncDir($extractDir, base_path());
if (File::exists($envBackup)) {
File::copy($envBackup, base_path('.env'));
}
$this->call('optimize:clear');
if ($migrateRequired) {
$this->warn('Manifest richiede migrate: esecuzione php artisan migrate --force');
$this->call('migrate', ['--force' => true]);
}
$this->info('Aggiornamento applicato con successo.');
return self::SUCCESS;
}
private function syncDir(string $from, string $to): void
{
$items = File::allFiles($from);
foreach ($items as $item) {
$path = $item->getPathname();
$relative = ltrim(str_replace($from, '', $path), '/');
if ($relative === '.env' || str_starts_with($relative, 'storage/') || str_starts_with($relative, '.git/')) {
continue;
}
$target = $to . '/' . $relative;
File::ensureDirectoryExists(dirname($target));
File::copy($path, $target);
}
}
}