Restructure Form1 into a sequential wizard (database -> import -> raw -> recursive component resolution -> completeness/STEP report), fix case-sensitivity bugs in codice/tipo_file lookups and a word-boundary gap in the assembly scanner that missed _D100-style sub-assembly references, and treat the distinta's general assembly as an ordinary CSV row (derived from the DBP_<codice>_<timestamp> filename) instead of special-casing it throughout. Also adds CadSearchGui.Tests (xUnit) covering the scanner, filename parsing, and the case-insensitive DB lookup behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
280 lines
12 KiB
C#
280 lines
12 KiB
C#
using System.Diagnostics;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
namespace CadSearchGui;
|
|
|
|
public sealed record ScanProgress(
|
|
string Cartella,
|
|
long FattiCartella,
|
|
long PrevistiCartella,
|
|
long FattiTotali,
|
|
long PrevistiTotali,
|
|
int Nuovi,
|
|
int Aggiornati,
|
|
int Invariati,
|
|
int Rimossi,
|
|
TimeSpan Trascorso);
|
|
|
|
public static class Indexer
|
|
{
|
|
private static readonly Regex FilenameRegex =
|
|
new(@"^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\d+)$", RegexOptions.Compiled);
|
|
|
|
internal const string Schema = """
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id INTEGER PRIMARY KEY,
|
|
percorso_completo TEXT UNIQUE NOT NULL,
|
|
percorso_relativo TEXT,
|
|
nome_file TEXT,
|
|
cartella_radice TEXT,
|
|
famiglia TEXT,
|
|
codice TEXT,
|
|
tipo_file TEXT,
|
|
versione INTEGER,
|
|
dimensione_byte INTEGER,
|
|
data_modifica TEXT,
|
|
scansionato_il TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_files_codice ON files(codice);
|
|
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
|
|
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
|
|
|
|
CREATE VIEW IF NOT EXISTS files_ultima_versione AS
|
|
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
|
|
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
|
|
FROM (
|
|
SELECT f.*, ROW_NUMBER() OVER (
|
|
PARTITION BY cartella_radice, codice, tipo_file
|
|
ORDER BY versione DESC
|
|
) AS rn
|
|
FROM files f
|
|
) WHERE rn = 1;
|
|
""";
|
|
|
|
public static Task ScanAsync(string root, string dbPath, IProgress<ScanProgress> progress, CancellationToken ct)
|
|
=> Task.Run(() => Scan(root, dbPath, progress, ct), ct);
|
|
|
|
private static void Scan(string root, string dbPath, IProgress<ScanProgress> progress, CancellationToken ct)
|
|
{
|
|
using var connection = new SqliteConnection($"Data Source={dbPath}");
|
|
connection.Open();
|
|
using (var schemaCmd = connection.CreateCommand())
|
|
{
|
|
schemaCmd.CommandText = Schema;
|
|
schemaCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// Indicizza tutte le cartelle di primo livello sotto la radice (non un elenco fisso),
|
|
// così una nuova cartella creata sulla share entra nell'indice senza modificare il codice.
|
|
var cartelleRadice = Directory.GetDirectories(root).Select(p => Path.GetFileName(p)!).ToArray();
|
|
|
|
var previsti = new Dictionary<string, long>();
|
|
foreach (var cartella in cartelleRadice)
|
|
{
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = "SELECT COUNT(*) FROM files WHERE cartella_radice = @c";
|
|
cmd.Parameters.AddWithValue("@c", cartella);
|
|
previsti[cartella] = (long)(cmd.ExecuteScalar() ?? 0L);
|
|
}
|
|
long previstiTotali = previsti.Values.Sum();
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
long fattiTotali = 0;
|
|
|
|
foreach (var cartella in cartelleRadice)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
string rootPath = Path.Combine(root, cartella);
|
|
if (!Directory.Exists(rootPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string now = DateTime.UtcNow.ToString("o");
|
|
int nuovi = 0, aggiornati = 0, invariati = 0;
|
|
long fattiCartella = 0;
|
|
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
using var cmdSelect = connection.CreateCommand();
|
|
cmdSelect.Transaction = transaction;
|
|
cmdSelect.CommandText = "SELECT dimensione_byte, data_modifica FROM files WHERE percorso_completo = @p";
|
|
var pSelect = cmdSelect.Parameters.Add("@p", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
|
|
using var cmdInsert = connection.CreateCommand();
|
|
cmdInsert.Transaction = transaction;
|
|
cmdInsert.CommandText = """
|
|
INSERT INTO files
|
|
(percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
|
|
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il)
|
|
VALUES (@percorso, @relativo, @nome, @cartella, @famiglia, @codice, @tipo, @versione,
|
|
@dimensione, @modifica, @scansionato)
|
|
""";
|
|
AggiungiParametriRecord(cmdInsert);
|
|
cmdInsert.Parameters.AddWithValue("@scansionato", now);
|
|
|
|
using var cmdUpdate = connection.CreateCommand();
|
|
cmdUpdate.Transaction = transaction;
|
|
cmdUpdate.CommandText = """
|
|
UPDATE files SET percorso_relativo=@relativo, nome_file=@nome, cartella_radice=@cartella,
|
|
famiglia=@famiglia, codice=@codice, tipo_file=@tipo, versione=@versione,
|
|
dimensione_byte=@dimensione, data_modifica=@modifica, scansionato_il=@scansionato
|
|
WHERE percorso_completo=@percorso
|
|
""";
|
|
AggiungiParametriRecord(cmdUpdate);
|
|
cmdUpdate.Parameters.AddWithValue("@scansionato", now);
|
|
|
|
using var cmdTouch = connection.CreateCommand();
|
|
cmdTouch.Transaction = transaction;
|
|
cmdTouch.CommandText = "UPDATE files SET scansionato_il=@scansionato WHERE percorso_completo=@percorso";
|
|
cmdTouch.Parameters.AddWithValue("@scansionato", now);
|
|
var pTouchPercorso = cmdTouch.Parameters.Add("@percorso", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
|
|
foreach (var file in Walk(rootPath))
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
string percorsoCompleto = file.FullName;
|
|
string percorsoRelativo = Path.GetRelativePath(rootPath, percorsoCompleto);
|
|
string nomeFile = file.Name;
|
|
string[] parti = percorsoRelativo.Split(Path.DirectorySeparatorChar);
|
|
string? famiglia = parti.Length > 1 ? parti[0] : null;
|
|
var (codice, tipo, versione) = ParseFilename(nomeFile);
|
|
long dimensione = file.Length;
|
|
string modifica = file.LastWriteTimeUtc.ToString("o");
|
|
|
|
pSelect.Value = percorsoCompleto;
|
|
object? esistenteDim = null;
|
|
string? esistenteMod = null;
|
|
bool esiste = false;
|
|
using (var reader = cmdSelect.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
esiste = true;
|
|
esistenteDim = reader.IsDBNull(0) ? null : reader.GetInt64(0);
|
|
esistenteMod = reader.IsDBNull(1) ? null : reader.GetString(1);
|
|
}
|
|
}
|
|
|
|
if (!esiste)
|
|
{
|
|
ImpostaParametriRecord(cmdInsert, percorsoCompleto, percorsoRelativo, nomeFile, cartella,
|
|
famiglia, codice, tipo, versione, dimensione, modifica);
|
|
cmdInsert.ExecuteNonQuery();
|
|
nuovi++;
|
|
}
|
|
else if (!(esistenteDim is long d && d == dimensione && esistenteMod == modifica))
|
|
{
|
|
ImpostaParametriRecord(cmdUpdate, percorsoCompleto, percorsoRelativo, nomeFile, cartella,
|
|
famiglia, codice, tipo, versione, dimensione, modifica);
|
|
cmdUpdate.ExecuteNonQuery();
|
|
aggiornati++;
|
|
}
|
|
else
|
|
{
|
|
pTouchPercorso.Value = percorsoCompleto;
|
|
cmdTouch.ExecuteNonQuery();
|
|
invariati++;
|
|
}
|
|
|
|
fattiCartella++;
|
|
fattiTotali++;
|
|
|
|
if (fattiTotali % 200 == 0)
|
|
{
|
|
progress.Report(new ScanProgress(cartella, fattiCartella, previsti[cartella],
|
|
fattiTotali, previstiTotali, nuovi, aggiornati, invariati, 0, sw.Elapsed));
|
|
}
|
|
}
|
|
|
|
using (var cmdDelete = connection.CreateCommand())
|
|
{
|
|
cmdDelete.Transaction = transaction;
|
|
cmdDelete.CommandText = "DELETE FROM files WHERE cartella_radice = @c AND scansionato_il != @now";
|
|
cmdDelete.Parameters.AddWithValue("@c", cartella);
|
|
cmdDelete.Parameters.AddWithValue("@now", now);
|
|
int rimossi = cmdDelete.ExecuteNonQuery();
|
|
|
|
transaction.Commit();
|
|
|
|
progress.Report(new ScanProgress(cartella, fattiCartella, previsti[cartella],
|
|
fattiTotali, previstiTotali, nuovi, aggiornati, invariati, rimossi, sw.Elapsed));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AggiungiParametriRecord(SqliteCommand cmd)
|
|
{
|
|
cmd.Parameters.Add("@percorso", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@relativo", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@nome", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@cartella", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@famiglia", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@tipo", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
cmd.Parameters.Add("@versione", Microsoft.Data.Sqlite.SqliteType.Integer);
|
|
cmd.Parameters.Add("@dimensione", Microsoft.Data.Sqlite.SqliteType.Integer);
|
|
cmd.Parameters.Add("@modifica", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
}
|
|
|
|
private static void ImpostaParametriRecord(SqliteCommand cmd, string percorso, string relativo, string nome,
|
|
string cartella, string? famiglia, string codice, string tipo, int? versione, long dimensione, string modifica)
|
|
{
|
|
cmd.Parameters["@percorso"].Value = percorso;
|
|
cmd.Parameters["@relativo"].Value = relativo;
|
|
cmd.Parameters["@nome"].Value = nome;
|
|
cmd.Parameters["@cartella"].Value = cartella;
|
|
cmd.Parameters["@famiglia"].Value = (object?)famiglia ?? DBNull.Value;
|
|
cmd.Parameters["@codice"].Value = codice;
|
|
cmd.Parameters["@tipo"].Value = tipo;
|
|
cmd.Parameters["@versione"].Value = (object?)versione ?? DBNull.Value;
|
|
cmd.Parameters["@dimensione"].Value = dimensione;
|
|
cmd.Parameters["@modifica"].Value = modifica;
|
|
}
|
|
|
|
internal static (string codice, string tipo, int? versione) ParseFilename(string nomeFile)
|
|
{
|
|
var match = FilenameRegex.Match(nomeFile);
|
|
if (match.Success)
|
|
{
|
|
return (match.Groups["codice"].Value, match.Groups["tipo"].Value,
|
|
int.Parse(match.Groups["versione"].Value));
|
|
}
|
|
return (nomeFile, "altro", null);
|
|
}
|
|
|
|
private static IEnumerable<FileInfo> Walk(string root)
|
|
{
|
|
var stack = new Stack<string>();
|
|
stack.Push(root);
|
|
while (stack.Count > 0)
|
|
{
|
|
string current = stack.Pop();
|
|
IEnumerable<FileSystemInfo> entries;
|
|
try
|
|
{
|
|
entries = new DirectoryInfo(current).EnumerateFileSystemInfos();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var entry in entries)
|
|
{
|
|
if (entry is DirectoryInfo dir)
|
|
{
|
|
stack.Push(dir.FullName);
|
|
}
|
|
else if (entry is FileInfo file)
|
|
{
|
|
yield return file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|