Creo Distributed Batch falliva su 8 dei 22 assiemi con "[-4] Dati di input non completi", un messaggio che non nomina il file mancante. Erano due cause distinte, entrambe emerse aprendo gli assiemi da raw/ e cercando nell'albero il componente con l'icona rossa. Copia sbagliata del componente. Lo stesso codice esiste spesso in piu' cartelle della share con pari versione e contenuto diverso (13% dei codici): non sono revisioni, sono istantanee parallele lasciate da studi, allestimenti fiera e cartelle temporanee. La vista dell'indice rompeva il pareggio in modo arbitrario, e poteva vincere una copia obsoleta - nascondendo del tutto quella buona. Creo poi caricava un modello diverso da quello atteso, non risolveva i vincoli e non apriva l'assieme. Il .asm non dice quale copia usa (ne' percorso, ne' UOBJ_ID del figlio, ne' revisione attesa), quindi si replica la regola di Creo: vince la copia nella stessa cartella dell'assieme che la referenzia. La vista ora rompe il pareggio in modo deterministico e viene ricreata a ogni scan, cosi' un DB esistente prende la definizione nuova senza reindicizzare. Componenti soppressi. Spariscono da "members" ma restano citati dalla feature che li posiziona, e il loro file serve comunque per aprire l'assieme. Da quando la lettura usa il parser esatto - che legge solo "members", ed e' la scelta giusta per la BOM - non venivano piu' copiati. Ora si copiano a parte, senza entrare nel conteggio dei "non trovati": non sono componenti della distinta, segnalarli come mancanti sarebbe un falso allarme. Documentate entrambe in docs/formato-asm-record.md §7.3 e §7.4, insieme alla distinzione che le accomuna: la lista della distinta e la lista dei file necessari a Creo non coincidono, e la seconda e' piu' lunga. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
305 lines
13 KiB
C#
305 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|