netgescon-day0/app/Http/Controllers/Admin/MigrationArchiveController.php

481 lines
21 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\MigrationSource;
use App\Models\MigrationDataset;
use App\Models\MigrationRecord;
use App\Models\UserSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Str;
class MigrationArchiveController extends Controller
{
/** Normalize user-provided path by trimming and removing hidden Unicode spaces/control chars */
protected function normalizePath(?string $path): string
{
$p = trim((string)$path);
// remove common zero-width/non-breaking/control spaces
$p = preg_replace('/[\x00-\x1F\x7F\x{00A0}\x{2000}-\x{200B}\x{202F}\x{FEFF}]/u', '', $p);
return $p;
}
public function __construct()
{
$this->middleware(['auth', 'role:admin|amministratore|super-admin']);
}
public function registerStabili(Request $request)
{
$path = $this->normalizePath($request->input('mdb_path') ?: UserSetting::get('gescon.mdb_stabili'));
if (!$path) {
return redirect()->back()->with('error', 'Percorso Stabili.mdb non configurato.');
}
// Non bloccare se non esiste, ma avvisa: l'ingest farà i controlli completi
$exists = @file_exists($path);
$source = MigrationSource::updateOrCreate(
['name' => 'GESCON Stabili'],
['type' => 'mdb', 'path' => $path, 'enabled' => true]
);
return redirect()->back()->with(
$exists ? 'success' : 'warning',
'Sorgente "GESCON Stabili" registrata.' . ($exists ? '' : ' Attenzione: il file non è stato trovato al momento della registrazione.')
);
}
public function ingestStabili(Request $request)
{
$source = MigrationSource::where('name', 'GESCON Stabili')->first();
if (!$source) {
return redirect()->back()->with('error', 'Registra prima la sorgente GESCON Stabili.');
}
$submitted = $this->normalizePath($request->input('mdb_path'));
$mdbPath = $submitted ?: $source->path;
$mdbPath = $this->normalizePath($mdbPath);
$real = @realpath($mdbPath) ?: $mdbPath;
// Fallback: se la path inviata non esiste ma la sorgente registrata sì, usa quella
if ($submitted && !@file_exists($submitted) && @file_exists($source->path)) {
$mdbPath = $source->path;
$real = @realpath($mdbPath) ?: $mdbPath;
}
if (!$mdbPath || !@file_exists($mdbPath)) {
return redirect()->back()->with('error', 'File MDB non trovato. Inviato: ' . ($submitted ?: '—') . ' | Sorgente: ' . ($source->path ?: '—') . ' — attenzione a maiuscole/minuscole (es. Stabili.mdb).');
}
$hasMdbTools = trim((string)@shell_exec('command -v mdb-export')) !== '';
if (!$hasMdbTools) {
return redirect()->back()->with('error', 'mdb-tools non disponibile sul server.');
}
// Verifica file e permessi
if (!$mdbPath || !is_file($mdbPath)) {
return redirect()->back()->with('error', 'File MDB non trovato: ' . ($mdbPath ?: '(vuoto)'));
}
if (!is_readable($mdbPath)) {
return redirect()->back()->with('error', 'File MDB non leggibile (permessi). Percorso: ' . $real);
}
// Esporta tabella Stabili con separatore pipe
$tableGuess = 'Stabili';
$tables = @shell_exec(sprintf('mdb-tables -1 %s 2>&1', escapeshellarg($mdbPath))) ?: '';
if ($tables && stripos($tables, 'Couldn') !== false) {
return redirect()->back()->with('error', 'Errore mdb-tables: ' . trim($tables));
}
if ($tables) {
foreach (preg_split('/\r?\n/', trim($tables)) as $t) {
if ($t === '') continue;
if (preg_match('/stabil/i', $t)) {
$tableGuess = trim($t);
break;
}
}
}
$tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_');
$cmd = sprintf(
'mdb-export -D %s -d %s -q %s -R "\\n" %s %s > %s 2>%s',
escapeshellarg('%Y-%m-%d'),
escapeshellarg('|'),
escapeshellarg('"'),
escapeshellarg($mdbPath),
escapeshellarg($tableGuess),
escapeshellarg($tmp),
escapeshellarg($tmp . '.err')
);
@shell_exec($cmd);
$errOut = @is_file($tmp . '.err') ? trim((string)@file_get_contents($tmp . '.err')) : '';
if (!is_file($tmp) || filesize($tmp) === 0) {
@unlink($tmp);
return redirect()->back()->with('error', 'Esportazione MDB vuota. Tabella utilizzata: ' . $tableGuess . '. ' . ($errOut ? ('Dettagli: ' . mb_strimwidth($errOut, 0, 300, '…')) : ''));
}
// Parse CSV pipe
$fh = fopen($tmp, 'r');
$headers = fgetcsv($fh, 0, '|');
$headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers ?: []);
DB::beginTransaction();
try {
// Dataset upsert
$dataset = MigrationDataset::updateOrCreate(
['source_id' => $source->id, 'key' => 'stabili'],
['title' => 'Stabili.mdb', 'state' => 'ready', 'schema' => $headers]
);
// Clear previous records
MigrationRecord::where('dataset_id', $dataset->id)->delete();
$count = 0;
while (($cols = fgetcsv($fh, 0, '|')) !== false) {
if ($cols === [null] || $cols === false) continue;
$cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols);
$countH = count($headers);
if (count($cols) < $countH) $cols = array_pad($cols, $countH, null);
if (count($cols) > $countH) $cols = array_slice($cols, 0, $countH);
$row = @array_combine($headers, $cols);
if (!$row) continue;
$legacy = $row['nome_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? null));
$legacy = $legacy ? trim((string)$legacy) : null;
$hash = substr(hash('sha256', json_encode($row)), 0, 64);
MigrationRecord::create([
'dataset_id' => $dataset->id,
'legacy_key' => $legacy,
'hash' => $hash,
'data' => $row,
]);
$count++;
}
fclose($fh);
@unlink($tmp);
$dataset->records_count = $count;
$dataset->save();
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
if (is_resource($fh)) fclose($fh);
@unlink($tmp);
return redirect()->back()->with('error', 'Errore ingest: ' . $e->getMessage());
}
return redirect()->back()->with('success', 'Acquisizione Stabili completata: ' . $count . ' record.');
}
/**
* Verifica server-side l'accessibilità del file MDB.
*/
public function verifyMdb(Request $request)
{
$path = $this->normalizePath($request->input('mdb_path'));
// Fallback suggerito: copia server-friendly in storage
if ((!$path || !@file_exists($path)) && @file_exists('/var/www/netgescon/storage/gescon/Stabili.mdb')) {
$path = '/var/www/netgescon/storage/gescon/Stabili.mdb';
}
$real = @realpath($path) ?: null;
$exists = $path ? @file_exists($path) : false;
$readable = $exists ? @is_readable($path) : false;
$size = ($exists && @is_file($path)) ? @filesize($path) : null;
$owner = $exists ? @fileowner($path) : null;
$group = $exists ? @filegroup($path) : null;
$perms = $exists ? substr(sprintf('%o', @fileperms($path)), -4) : null;
return response()->json([
'input' => $path,
'realpath' => $real,
'exists' => (bool)$exists,
'readable' => (bool)$readable,
'size' => $size,
'owner' => $owner,
'group' => $group,
'perms' => $perms,
'cwd' => getcwd(),
'user' => get_current_user(),
]);
}
public function status(Request $request)
{
$src = MigrationSource::where('name', 'GESCON Stabili')->first();
$ds = $src ? MigrationDataset::where('source_id', $src->id)->where('key', 'stabili')->first() : null;
return response()->json([
'source' => $src,
'dataset' => $ds ? ['id' => $ds->id, 'records' => $ds->records_count] : null,
]);
}
/**
* UI semplice per caricare/ingestione MDB in staging.
*/
public function ui(Request $request)
{
$settings = [
'gescon.source_path' => UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'),
'gescon.mdb_stabili' => UserSetting::get('gescon.mdb_stabili', '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'),
'gescon.import_conn' => UserSetting::get('gescon.import_conn', 'gescon_import'),
];
$prefillMdb = $this->normalizePath($request->query('mdb_path')) ?: null;
if ($prefillMdb) {
$settings['gescon.mdb_stabili'] = $prefillMdb;
}
// UI: se la path configurata non esiste, proponi automaticamente la copia in storage come suggerimento
if ((!$settings['gescon.mdb_stabili'] || !@file_exists($settings['gescon.mdb_stabili'])) && @file_exists('/var/www/netgescon/storage/gescon/Stabili.mdb')) {
$settings['gescon.mdb_stabili'] = '/var/www/netgescon/storage/gescon/Stabili.mdb';
}
$hasMdbTools = trim((string)@shell_exec('command -v mdb-export')) !== '';
return view('admin.gescon-import.archive-ui', compact('settings', 'hasMdbTools'));
}
/**
* Elenca le tabelle presenti nel file MDB.
*/
public function scanTables(Request $request)
{
$path = $this->normalizePath($request->input('mdb_path') ?: UserSetting::get('gescon.mdb_stabili'));
if ((!$path || !@file_exists($path)) && @file_exists('/var/www/netgescon/storage/gescon/Stabili.mdb')) {
$path = '/var/www/netgescon/storage/gescon/Stabili.mdb';
}
if (!$path || !@file_exists($path)) {
return response()->json(['error' => 'File MDB non trovato', 'path' => $path], 400);
}
$has = trim((string)@shell_exec('command -v mdb-tables')) !== '';
if (!$has) return response()->json(['error' => 'mdb-tools non disponibile'], 500);
$out = @shell_exec(sprintf('mdb-tables -1 %s 2>&1', escapeshellarg($path))) ?: '';
if (!$out) return response()->json(['tables' => []]);
$list = array_values(array_filter(array_map('trim', preg_split('/\r?\n/', trim($out)) ?? [])));
return response()->json(['tables' => $list, 'path' => $path]);
}
/**
* Importa tabelle selezionate dal MDB nel DB di staging (connessione gescon_import o come configurato).
*/
public function ingestTablesToDb(Request $request)
{
$path = $this->normalizePath($request->input('mdb_path') ?: UserSetting::get('gescon.mdb_stabili'));
if ((!$path || !@file_exists($path)) && @file_exists('/var/www/netgescon/storage/gescon/Stabili.mdb')) {
$path = '/var/www/netgescon/storage/gescon/Stabili.mdb';
}
$tables = (array)$request->input('tables', []);
$conn = UserSetting::get('gescon.import_conn', 'gescon_import');
// Opzionale: mappa tabella sorgente → tabella destinazione
$targets = (array)($request->input('targets', []));
$truncate = (bool)$request->boolean('truncate', true);
if (!$path || !@file_exists($path)) return response()->json(['error' => 'File MDB non trovato', 'path' => $path], 400);
$has = trim((string)@shell_exec('command -v mdb-export')) !== '';
if (!$has) return response()->json(['error' => 'mdb-tools non disponibile'], 500);
if (empty($tables)) return response()->json(['error' => 'Nessuna tabella selezionata'], 400);
$result = [];
foreach ($tables as $table) {
$table = trim((string)$table);
if ($table === '') continue;
$targetName = null;
if (isset($targets[$table]) && is_string($targets[$table]) && trim($targets[$table]) !== '') {
$targetName = trim($targets[$table]);
}
$count = $this->importSingleTable($conn, $path, $table, $targetName, $truncate);
$result[] = ['table' => $table, 'target' => $targetName ?: ('mdb_' . \Illuminate\Support\Str::snake($table)), 'rows' => $count];
}
return response()->json(['ok' => true, 'connection' => $conn, 'result' => $result]);
}
/**
* Import rapido per singolo_anno.mdb (stabile/anno) su tabelle di staging standard.
* Supporta multi-anno leggendo le sottocartelle sotto <source_path>/<stabile>.
*/
public function quickLoad(Request $request)
{
$stabile = $this->normalizePath($request->input('stabile'));
$year = $this->normalizePath($request->input('year'));
$allYears = $request->boolean('all_years');
$tables = array_values(array_filter((array)$request->input('tables', [])));
$truncate = $request->boolean('truncate', true);
if (!$stabile) {
return response()->json(['ok' => false, 'error' => 'Codice stabile richiesto.'], 422);
}
if (empty($tables)) {
return response()->json(['ok' => false, 'error' => 'Seleziona almeno una tabella.'], 422);
}
$allowedTables = [
'condomin',
'dett_tab',
'voc_spe',
'cre_deb_preced',
'operazioni',
'incassi',
'rate',
'giri_conti',
'straordinarie',
'dett_pers',
'detrazioni_fiscali',
];
$tables = array_values(array_filter($tables, fn ($t) => in_array($t, $allowedTables, true)));
if (empty($tables)) {
return response()->json(['ok' => false, 'error' => 'Nessuna tabella valida selezionata.'], 422);
}
$base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon');
$stableDir = rtrim((string)$base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $stabile;
$years = [];
if ($allYears) {
if (is_dir($stableDir)) {
foreach (glob($stableDir . DIRECTORY_SEPARATOR . '*') as $dir) {
if (!is_dir($dir)) continue;
$mdb = $dir . DIRECTORY_SEPARATOR . 'singolo_anno.mdb';
if (is_file($mdb)) {
$years[] = basename($dir);
}
}
sort($years, SORT_STRING);
}
if (empty($years)) {
return response()->json(['ok' => false, 'error' => 'Nessuna cartella anno con singolo_anno.mdb trovata.'], 404);
}
} else {
if (!$year) {
return response()->json(['ok' => false, 'error' => 'Anno/cartella richiesta o seleziona "tutti gli anni".'], 422);
}
$years = [$year];
}
$results = [];
foreach ($years as $idx => $yr) {
$mdb = $stableDir . DIRECTORY_SEPARATOR . $yr . DIRECTORY_SEPARATOR . 'singolo_anno.mdb';
if (!is_file($mdb)) {
$results[] = ['year' => $yr, 'error' => 'singolo_anno.mdb non trovato', 'path' => $mdb];
continue;
}
foreach ($tables as $t) {
$args = [
'--mdb' => $mdb,
'--table' => $t,
'--stabile' => $stabile,
'--legacy-year' => $yr,
];
if ($truncate && $idx === 0) {
$args['--truncate'] = true;
}
Artisan::call('gescon:load-mdb', $args);
$results[] = [
'year' => $yr,
'table' => $t,
'path' => $mdb,
'output' => trim((string)Artisan::output()),
];
}
}
return response()->json(['ok' => true, 'stabile' => $stabile, 'years' => $years, 'results' => $results]);
}
protected function importSingleTable(string $conn, string $mdbPath, string $tableName, ?string $targetName = null, bool $truncate = true): int
{
// Esporta CSV temporaneo
$tmp = tempnam(sys_get_temp_dir(), 'mdbi_');
$cmd = sprintf(
'mdb-export -D %s -d %s -q %s -R "\\n" %s %s > %s 2>%s',
escapeshellarg('%Y-%m-%d'),
escapeshellarg('|'),
escapeshellarg('"'),
escapeshellarg($mdbPath),
escapeshellarg($tableName),
escapeshellarg($tmp),
escapeshellarg($tmp . '.err')
);
@shell_exec($cmd);
if (!@is_file($tmp) || @filesize($tmp) === 0) {
@unlink($tmp);
return 0;
}
$fh = fopen($tmp, 'r');
if (!$fh) {
@unlink($tmp);
return 0;
}
$headers = fgetcsv($fh, 0, '|');
if (!$headers) {
fclose($fh);
@unlink($tmp);
return 0;
}
$headers = array_map(function ($h) {
return strtolower(Str::snake(trim((string)$h)));
}, $headers);
// Determina tabella destinazione (personalizzata o prefisso mdb_)
$stagingTable = $targetName ? Str::snake($targetName) : ('mdb_' . Str::snake($tableName));
// Crea/aggiorna tabella con colonne testo per ogni header
if (!Schema::connection($conn)->hasTable($stagingTable)) {
Schema::connection($conn)->create($stagingTable, function (Blueprint $t) use ($headers) {
$t->bigIncrements('id');
$t->string('row_hash', 64)->index();
$t->string('legacy_key')->nullable()->index();
foreach ($headers as $col) {
$safe = substr($col, 0, 191);
if ($safe === 'id') $safe = 'col_id';
$t->text($safe)->nullable();
}
$t->timestamps();
});
} else {
// Aggiungi eventuali nuove colonne
$existing = Schema::connection($conn)->getColumnListing($stagingTable);
$toAdd = array_values(array_diff($headers, $existing));
if (!empty($toAdd)) {
Schema::connection($conn)->table($stagingTable, function (Blueprint $t) use ($toAdd) {
foreach ($toAdd as $col) {
$safe = substr($col, 0, 191);
if ($safe === 'id') $safe = 'col_id';
$t->text($safe)->nullable();
}
});
}
}
// Pulisci tabella prima di importare (opzionale, default true)
if ($truncate) {
DB::connection($conn)->table($stagingTable)->truncate();
}
$count = 0;
while (($cols = fgetcsv($fh, 0, '|')) !== false) {
if ($cols === [null]) continue;
$cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols);
$data = $cols;
// Allinea a headers
$countH = count($headers);
if (count($cols) < $countH) $cols = array_pad($cols, $countH, null);
if (count($cols) > $countH) $cols = array_slice($cols, 0, $countH);
$row = @array_combine($headers, $cols);
if (!$row) continue;
$legacy = $row['nome_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? ($row['id'] ?? null)));
$hash = substr(hash('sha256', json_encode($row)), 0, 64);
$rowToInsert = array_merge($row, [
'row_hash' => $hash,
'legacy_key' => $legacy ? (string)$legacy : null,
'created_at' => now(),
'updated_at' => now(),
]);
// Upsert semplice basato su row_hash se non si tronca
if ($truncate) {
DB::connection($conn)->table($stagingTable)->insert($rowToInsert);
} else {
$exists = DB::connection($conn)->table($stagingTable)->where('row_hash', $hash)->exists();
if (!$exists) {
DB::connection($conn)->table($stagingTable)->insert($rowToInsert);
} else {
DB::connection($conn)->table($stagingTable)->where('row_hash', $hash)->update(array_merge($row, [
'legacy_key' => $rowToInsert['legacy_key'],
'updated_at' => now(),
]));
}
}
$count++;
}
fclose($fh);
@unlink($tmp);
return $count;
}
}