Files
prep-step/PrepStep/Indexer.cs
T

304 lines
13 KiB
C#
Raw Normal View History

using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite;
namespace PrepStep;
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);
-- DROP+CREATE invece di "IF NOT EXISTS": la vista e' ricalcolata a ogni avvio di scan,
-- cosi' un DB gia' esistente prende la definizione corrente senza reindicizzare.
DROP VIEW IF EXISTS files_ultima_versione;
CREATE VIEW 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
-- A parita' di versione (stesso codice in due sottocartelle della stessa
-- radice, es. una copia vecchia in una cartella di lavoro) vince il file
-- modificato piu' di recente: senza questo criterio il pareggio e' rotto in
-- modo arbitrario e puo' vincere la copia obsoleta, nascondendo del tutto
-- quella buona. Il file copiato in "raw" sarebbe allora un modello diverso da
-- quello che l'assieme si aspetta, e Creo non riesce ad aprirlo.
ORDER BY versione DESC, data_modifica DESC, percorso_completo
) 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();
}
// Le cartelle "temp_*" (temp_paris, temp_davide, temp_studi esterni, ...) sono lavori
// personali/temporanei: contengono spesso copie datate o versioni scollegate di codici
// che esistono ANCHE nel resto della share, con lo stesso codice ma contenuto diverso.
// Un .asm nella struttura "ufficiale" può referenziare per coincidenza numerica un file
// dentro una di queste cartelle (vedi docs/formato-asm.md, falso positivo su
// "2502020809"), trascinando dentro un intero assieme di un progetto scollegato. Vanno
// escluse a monte, non solo filtrate caso per caso nelle singole query.
using (var deleteTempCmd = connection.CreateCommand())
{
deleteTempCmd.CommandText = @"DELETE FROM files WHERE cartella_radice LIKE 'temp\_%' ESCAPE '\'";
deleteTempCmd.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)!)
.Where(c => !c.StartsWith("temp_", StringComparison.OrdinalIgnoreCase))
.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;
}
}
}
}
}