Datei double finder.dart
Links
Zielsetzung
Hier werden nur Besonderheiten der Datei double_finder.dart besprochen.
Viele Teile dieser Datei sind ähnlich wie [Datei search_engine.dart], z. B. das Festlegen der Optionen.
Die Klasse SearchEngine
Die Klasse implementiert ("realisiert") die Suche nach Duplikaten.
Die Attribute
Zuerst werden die Attribute definiert:
class DoubleFinder { static bool storeResult = false; final startTime = DateTime.now(); Map<int, List<FileInfo>> processedFiles = {}; final int verboseLevel; final startLength; final blockSize; int startBlockCount = 0; int fullBlockCounts = 0; int fullBlockFiles = 0; int doubles = 0; final lines = <String>[]; var hashBuilder = md5;
Der Konstruktor
DoubleFinder({this.verboseLevel, this.blockSize, this.startLength});
- Keine Besonderheiten.
Die Methode calcHash()
Diese Methode berechnet die Prüfsumme einer Datei.
Mit dem Parameter calculateStartBlock
wird festgelegt, ob die Prüfsumme nur für den Dateianfang oder für die ganze Datei berechnet wird.
/// Calculates the hash value of the [file]. /// If [calculateStartBlock] is true the begin of the file is processed. /// Otherwise the whole file content. /// Returns an instance of [Digest] containing the hash value. Digest calcHash(File file, {bool calculateStartBlock}) { Digest rc; try { final file2 = file.openSync(); if (calculateStartBlock) { final buffer = file2.readSync(startLength); rc = hashBuilder.convert(buffer); startBlockCount++; } else { fullBlockFiles++; final output = AccumulatorSink<Digest>(); var input = hashBuilder.startChunkedConversion(output); try { do { final buffer = file2.readSync(blockSize); if (buffer.isEmpty) { break; } input.add(buffer); fullBlockCounts++; } while (true); input.close(); input = null; rc = output.events.single; } finally { if (input != null) { input.close(); } output.close(); } } file2.close(); } on FileSystemException { rc = null; } return rc; }
Digest rc;
Diese Klasse aus dem Paketcrypt
verwaltet eine Prüfsumme.final file2 = file.openSync();
Die Datei wird "geöffnet", damit wird ein Zugriff auf den Inhalt der Datei möglich.
Im Fall nur Startblock:
buffer = file2.readSync(startLength);
Es wird ein Block aus dem Dateiinhalt gelesen, maximalstartLength
Bytes.rc = hashBuilder.convert(buffer);
Die Prüfsumme wird aus dem Dateiinhaltsblock berechnet.
Im Fall ganzer Dateiinhalt:
final output = AccumulatorSink<Digest>();
Ein Objekt der KlasseAccumulatorSink<Digest>
kann mit Prüfsummen umgehen.final input = hashBuilder.startChunkedConversion(output);
Die KlassenvariablehashBuilder
ist ein Objekt, das eine Prüfsumme berechnen kann, in unserem Fall MD5.startChunkedConversion(output)
liefert ein Objekt, das Prüfsummen aus Einzelblöcken berechnen kann.
do {...} while(true);
Eine Endlosschleife, die zwischendrin mitbreak
beendet wird.final buffer = file2.readSync(blockSize);
Lesen eines Blockes...if (buffer.isEmpty())
Konnte nichts gelesen werden, also Dateiende erreicht?break
Schleife beenden.rc = output.events.single;
input.add(buffer);
Block für Prüfsumme verarbeiten.finally
Egal ob mit oder ohne Ausnahme (Exception):input.close(); output.close();
Resourcen freigeben.
Die Methode find()
Diese Methode erledigt den Kern des Programms.
/// Finds the duplicates in a list of [filePatterns] respecting [fileOptions]. void find(List<String> filePatterns, FileOptions fileOptions) { fileOptions.yieldDirectory = fileOptions.yieldLinkToDirectory = fileOptions.yieldLinkToFile = false; fileOptions.recursive = true; final supplier = FileSupplier( fileOptions: fileOptions, filePatterns: filePatterns, verboseLevel: verboseLevel); final toRemove = <FileInfo>[]; for (var filename in supplier.next()) { final file = supplier.currentEntity as File; final size = file.lengthSync(); if (size == 0){ continue; } var newEntry = FileInfo(filename, size); if (!processedFiles.containsKey(size)) { if (verboseLevel >= 4) { print('= new size $size'); } processedFiles[size] = [newEntry]; continue; } for (var processedFile in processedFiles[size]) { processedFile.hashStart ??= calcHash(file, calculateStartBlock: true); if (processedFile.hashStart == null) { toRemove.add(processedFile); continue; } newEntry.hashStart ??= calcHash(file, calculateStartBlock: true); if (newEntry.hashStart == null) { newEntry = null; break; } if (processedFile.hashStart == newEntry.hashStart) { if (verboseLevel >= 4) { print('= start block is the same: $filename ${processedFile.name}'); } processedFile.hashFull ??= calcHash(File(processedFile.name), calculateStartBlock: false); if (processedFile.hashFull == null) { toRemove.add(processedFile); continue; } newEntry.hashFull ??= calcHash(file, calculateStartBlock: false); if (newEntry.hashFull == null) { newEntry = null; break; } if (processedFile.hashFull == newEntry.hashFull) { doubles++; final line = '${newEntry.name} = ${processedFile.name} size: $size'; if (verboseLevel >= 2) { if (storeResult) { lines.add(line); } else { print(line); } } // we search only one duplicate. break; } } } for (var processedFile in toRemove) { processedFiles[size].remove(processedFile); } toRemove.clear(); if (newEntry != null) { processedFiles[size].add(newEntry); } } if (verboseLevel >= 1) { print('= duplicates: $doubles start blocks: $startBlockCount full blocks: $fullBlockFiles files with $fullBlockCounts blocks'); print(supplier.summary.join('\n')); final diff = DateTime.now().difference(startTime); final milliSeconds = (diff.inMilliseconds % 1000).toString().padLeft(3, '0'); print( '= runtime: ${diff.inHours}h${diff.inMinutes % 60}m${diff.inSeconds % 60}.$milliSeconds'); } if (!DoubleFinder.storeResult) { // Exit at once: release resources faster. exit(0); } }
fileOptions.yieldDirectory =
fileOptions.yieldLinkToDirectory = fileOptions.yieldLinkToFile = false;
Der Filenamensgenerator soll nur die Namen von reguläre Dateien liefern (keine Verzeichnisnamen, keine Links).final toRemove = <FileInfo>[];
In dieser Liste merken wir uns die Einträge, die gelöscht werden müssen. Erklärung weiter unten.for (var filename in supplier.next())
Die zentrale Schleife über alle Dateien der gewünschten Verzeichnisse.final file = supplier.currentEntity as File;
- Wir brauchen mehr Infos über die Datei als den Dateinamen. Diese Info ist in der Klassenvariblen
supplier.currentEntity
vorhanden. - Diese Variable hat aber den Typ
FileSystemEntitity
, einer Oberklasse von TypFile
. Wir haben den Generator FileSupplier() aber so konfiguriert, dass nur Dateien geliefert werden können, daher musssupplier.currentEntity
vom TypFile
sein. - Daher können wir den Typ konvertieren mit
as Type
.
- Wir brauchen mehr Infos über die Datei als den Dateinamen. Diese Info ist in der Klassenvariblen
final size = file.lengthSync();
Die Variablefile
hat durch die obige Konvertierung den TypFile
und mitfile.lengthSync()
können wir auf die Dateigröße zugreifen, was beim TypFileSystemEntitiy
nicht möglich ist.var newEntry = FileInfo(filename, size);
Wir erstellen eine Info der Datei. Wir können nicht final verwenden, da in bestimmten FällennewEntry
aufnull
gesetzt wird.if (!processedFiles.containsKey(size))
Die KlassenvariablenprocessedFiles
ist eineMap
mit einem Eintrag für jede schon gefundene Dateilänge. Der Wert des Schlüssel-Werte-Paares ist eine Liste der Infos über die Dateien mit dieser Länge. Wir prüfen also, ob es schon Dateien mit der Länge der aktuellen Datei gibt, genauer (wegen des!
(not)), ob es keinen Eintrag gibt.processedFiles[size] = [newEntry];
Kein Eintrag, dann erzeugen wir einen...continue
... und sind für diesen Schleifendurchgang fertig, da es nichts zu vergleichen gibt.- Der weitere Code wird nur durchlaufen, wenn es schon Dateien mit dieser Länge gibt.
for (var processedFile in processedFiles[size])
Diese Schleife iteriert über alle schon gefundenen Dateien mit der Dateilänge der zu testenden Datei.processedFile.hashStart ??= calcHash(file, calculateStartBlock: true);
- Der Operator
??=
bewirkt, dass getestet wird, obprocessedFile
null
ist. Wenn ja (und nur dann) wird der Hashwert berechnet und der Variablen zugewiesen.
- Der Operator
if (processedFile.hashStart == null)
Ist der Wert trotz obiger Zeile nochnull
, ist ein Fehler incalcHash()
aufgetreten. In diesem Fall kann also die Prüfsumme nicht berechnet werden. Da dieser Fehler bei erneutem Test (wenn noch eine Datei mit dieser Länge auftaucht) wieder auftreten könnte, entfernen wird den Eintrag aus der Liste, damit das nicht nochmal passiert:toRemove.add(processedFile);
Wir dürfen nicht sofort löschen, da die Liste in einerfor
-Schleife verwendet wird, daher merken wir den Eintrag in der extra dafür vorgesehenen ListetoRemove
...continue
... und sind für diesen Schleifendurchlauf fertignewEntry.hashStart ??= calcHash(file, calculateStartBlock: true);
WennnewEntry.hashStart
noch null ist, wird die Prüfsumme berechnet.if (newEntry.hashStart == null)
Ist das Berechnen missglückt?newEntry = null;
Fehler merken...break;
... und Schleife abbrechenif (processedFile.hashStart == newEntry.hashStart)
Sind die Prüfsummen für den ersten Block gleich?processedFile.hashFull ??= calcHash(File(processedFile.name), calculateStartBlock: false);
Wenn noch nicht berechnet, dann berechne die Prüfsumme der Gesamtdatei.newEntry.hashFull ??= calcHash(file, calculateStartBlock: false);
Berechnet die Prüfsumme, wenn noch nicht erledigt.if (processedFile.hashFull == newEntry.hashFull)
Sind die Prüfsummen der ganzen Dateiinhalte gleich?final line = '${newEntry.name} = ${processedFile.name} size: $size';
Auszugebende Zeile zusammenstellen...print(line);
... und ausgeben.if (storeResult)
Im Falle von Unittests...lines.add(line);
Zeile in Liste merken, für Überprüfung im Unittest.for (var processedFile in toRemove)
Über alle Einträge der "Zu-löschen-Liste":processedFiles[size].remove(processedFile);
Eintrag löschen.if (newEntry != null)
Wenn oben kein Fehler war..processedFiles[size].add(newEntry);
Die Daten der aktuellen Datei für diese Dateigröße merken.