Datei double finder.dart

Aus Info-Theke
Zur Navigation springen Zur Suche springen

Links[Bearbeiten]

Zielsetzung[Bearbeiten]

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[Bearbeiten]

Die Klasse implementiert ("realisiert") die Suche nach Duplikaten.

Die Attribute[Bearbeiten]

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[Bearbeiten]

DoubleFinder({this.verboseLevel, this.blockSize, this.startLength});
  • Keine Besonderheiten.

Die Methode calcHash()[Bearbeiten]

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 Paket crypt 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, maximal startLength Bytes.
  • rc = hashBuilder.convert(buffer); Die Prüfsumme wird aus dem Dateiinhaltsblock berechnet.

Im Fall ganzer Dateiinhalt:

  • final output = AccumulatorSink<Digest>(); Ein Objekt der Klasse AccumulatorSink<Digest> kann mit Prüfsummen umgehen.
  • final input = hashBuilder.startChunkedConversion(output); Die Klassenvariable hashBuilder 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 mit break 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()[Bearbeiten]

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 Typ File. Wir haben den Generator FileSupplier() aber so konfiguriert, dass nur Dateien geliefert werden können, daher muss supplier.currentEntity vom Typ File sein.
    • Daher können wir den Typ konvertieren mit as Type.
  • final size = file.lengthSync(); Die Variable file hat durch die obige Konvertierung den Typ File und mit file.lengthSync() können wir auf die Dateigröße zugreifen, was beim Typ FileSystemEntitiy 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ällen newEntry auf null gesetzt wird.
  • if (!processedFiles.containsKey(size)) Die Klassenvariablen processedFiles ist eine Map 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, ob processedFile null ist. Wenn ja (und nur dann) wird der Hashwert berechnet und der Variablen zugewiesen.
  • if (processedFile.hashStart == null) Ist der Wert trotz obiger Zeile noch null, ist ein Fehler in calcHash() 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 einer for-Schleife verwendet wird, daher merken wir den Eintrag in der extra dafür vorgesehenen Liste toRemove ...
  • continue ... und sind für diesen Schleifendurchlauf fertig
  • newEntry.hashStart ??= calcHash(file, calculateStartBlock: true); Wenn newEntry.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 abbrechen
  • if (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.