home *** CD-ROM | disk | FTP | other *** search
- {$A+,B-,D-,E-,F-,I-,L-,N-,O-,R-,S-,V-,X+}
- {$IFDEF Ver70} {$P-,Q-,T-,Y-} {$ENDIF}
- {$M 16384, 16384, 655360}
- (*========================================================*)
- (* DUP.PAS *)
- (* Copyright (c) 1993 Karsten Gieselmann & DMV-Verlag *)
- (* Turbo/Borland Pascal ab 6.0, Stony Brook Pascal+ 6.0 *)
- (* DOS ab 3.30 *)
- (*========================================================*)
- PROGRAM Dup;
-
- USES
- Crt, Dos, (* benötigte Units aus dem Runtime-System *)
- DupUtil; (* ausgelagerte allgemeine Hilfsroutinen *)
-
- CONST
- Version = '1.0a';
- BufferSize = $4000; (* Puffer für Dateivergleich *)
-
- CONST (* Return-Codes: *)
- Ok = 0; (* Programm normal beendet *)
- NoFilesFound = 1; (* keine Dateien gefunden *)
- HelpCalled = 8; (* Programmende mit Hilfe *)
- UserAbort = 9; (* Abbruch durch Benutzer *)
- InvalidParams = 10; (* unerlaubter/ungültig. Param *)
- InsufficientMem = 20; (* zu wenig Hauptspeicher *)
- FaultOnIO = 30; (* Fehler bei Dateiein/ausgabe *)
- WrongDos = 200; (* DOS-Version < 3.30 *)
- InternalError = 250; (* interner Fehler *)
-
- CONST
- StdErrHandle = 2; (* Handle für StdErr-Device *)
-
- TYPE
- tDriveStr = STRING[27]; (* Buchstabenfolge *)
- (* der Laufwerksliste *)
- CONST
- EqualContent : BOOLEAN = TRUE; (* Inhalt prüfen? *)
- EqualName : BOOLEAN = FALSE; (* gl. Namen prüfen? *)
- EqualNameExt : BOOLEAN = FALSE; (* Test Name+Suffix? *)
- LoggedDrives : BOOLEAN = FALSE; (* alle Laufwerke? *)
- RemoteDrives : BOOLEAN = FALSE; (* Netzwerklaufwerke? *)
- FileNamesOnly : BOOLEAN = FALSE; (* nur Dateinamen? *)
- SuppressSummary: BOOLEAN = FALSE; (* Statistik-Report? *)
- QuietSearch : BOOLEAN = FALSE; (* keine Pfadangabe? *)
- DriveList : tDriveStr = ''; (* Suchlaufwerke *)
- TotalFileCount : LongInt = 0; (* Dateizähler *)
- DuplicateCount : LongInt = 0; (* Duplikatzähler *)
- TotalFileSize : LongInt = 0; (* Dateigröße *)
- DuplicateSize : LongInt = 0; (* Duplikatgröße *)
-
- (*--------------------------------------------------------*)
- (* Deklaration für Pool-Strukturen *)
-
- CONST
- MaxFile = 1023; (* maximaler Hashcode Filepool *)
- MaxName = 512; (* maximaler Hashcode Namepool *)
-
- TYPE
- tFileIndex = 0..MaxFile;(* Wertebereich Filepool Indizes*)
- tNameIndex = 0..MaxName;(* Wertebereich Namepool Indizes*)
- fsFileName = STRING[12];(* Stringtyp für Dateinamen *)
- tStringPtr = ^STRING; (* Zeigertyp auf Dateinamen *)
-
- TYPE
- tFileDescPtr = ^tFileDesc; (* Zeiger auf Filepool Elem.*)
- tFileDesc = RECORD (* Beschreibung Filepool El.*)
- DirPtr : tStringPtr; (* Verweis auf Namepool *)
- Name : fsFileName; (* Dateiname *)
- NameExt : fsFileName; (* Dateiname und -endung *)
- Size : LongInt; (* Dateigröße in Bytes *)
- Time : LongInt; (* Datum-/Zeitstempel *)
- Prev, Next : tFileDescPtr;(* Nachbarn in Koll.-Liste *)
- END;
-
- tNameDescPtr = ^tNameDesc; (* Zeiger auf Namepool-Elem.*)
- tNameDesc = RECORD (* Beschreibung Namepool-El.*)
- HashKey : WORD; (* 16-Bit Hashschlüssel *)
- NamePtr : tStringPtr; (* Verweis auf DIR-Namen *)
- Prev, Next : tNameDescPtr;(* Nachbarn in Koll.-liste *)
- END;
-
- VAR
- FilePool: ARRAY[tFileIndex] OF tFileDescPtr;
- (* Hashtabelle Filepool *)
- NamePool: ARRAY[tNameIndex] OF tNameDescPtr;
- (* Hashtabelle Namepool *)
-
- (*--------------------------------------------------------*)
- (* allgemeine Variablen *)
-
- VAR
- FileSpec : PathStr; (* Suchmaske für Rekursion *)
- Buf1, Buf2 : ARRAY[1..BufferSize] OF CHAR;
- (* Puffer für Dateivergleich *)
- CollectTime : LongInt; (* Zeit für Suche auf Platte *)
- CompareTime : LongInt; (* Zeit für Suche in Hashtab. *)
- StdErr : Text; (* Gerät für Fehlermeldungen *)
- rc : WORD; (* letzter I/O-Returncode *)
-
- PROCEDURE OpenStdErr;
- (* Öffnet das StdErr-Device für die Ausgabe von Fehler- *)
- (* meldungen *)
- BEGIN
- Assign(StdErr, ''); Rewrite(StdErr);
- IF IOResult = 0 THEN BEGIN
- (* Handle eintragen und zeichenweise Ausgabe erzwingen *)
- TextRec(StdErr).Handle := StdErrHandle;
- TextRec(StdErr).BufSize := 1;
- END ELSE BEGIN
- WriteLn('Error opening standard error device');
- Halt(FaultOnIO);
- END;
- END;
-
- PROCEDURE OpenStdOut;
- (* Bildschirmausgaben über StdOut, um Umlenkung zu er- *)
- (* möglichen *)
- BEGIN
- Assign(Output, ''); Rewrite(Output);
- END;
-
- PROCEDURE OpenCrtOut;
- (* Bildschirmausgaben über Crt, um die Ausgabe zu *)
- (* beschleunigen *)
- BEGIN
- AssignCrt(Output); Rewrite(Output);
- END;
-
- FUNCTION UserBreak: BOOLEAN;
- (* Überprüft, ob zwischenzeitlich eine Abbruchtaste ge- *)
- (* drückt wurde *)
- VAR
- ch: CHAR;
- BEGIN
- UserBreak := FALSE;
- WHILE KeyPressed DO BEGIN
- ch := ReadKey;
- CASE ch OF
- ^@: UserBreak := (ReadKey = ^@); (* ^Break *)
- ^[, ^C: UserBreak := TRUE; (* ^C oder Esc *)
- END;
- END;
- END;
-
- (*--------------------------------------------------------*)
- (* Fehlerbehandlung *)
-
- FUNCTION RunErrorMsg(ErrorCode: WORD): STRING;
- (* Liefert Klartextmeldung zu einer Turbo-Pascal-Fehler- *)
- (* Nummer *)
- BEGIN
- CASE ErrorCode OF
- 0 : RunErrorMsg := '';
- 1 : RunErrorMsg := 'Invalid DOS function call number';
- 2 : RunErrorMsg := 'File not found';
- 3 : RunErrorMsg := 'Path not found';
- 4 : RunErrorMsg := 'Too many open files';
- 5 : RunErrorMsg := 'File access denied';
- 6 : RunErrorMsg := 'Invalid file handle';
- 8 : RunErrorMsg := 'Insufficient memory';
- 12 : RunErrorMsg := 'Invalid file access mode';
- 15 : RunErrorMsg := 'Invalid drive number';
- 16 : RunErrorMsg := 'Cannot remove current directory';
- 17 : RunErrorMsg := 'Cannot rename across drives';
- 18 : RunErrorMsg := 'No more files';
- 100 : RunErrorMsg := 'Disk read error';
- 101 : RunErrorMsg := 'Disk write error';
- 102 : RunErrorMsg := 'File not assigned';
- 103 : RunErrorMsg := 'File not open';
- 104 : RunErrorMsg := 'File not open for input';
- 105 : RunErrorMsg := 'File not open for output';
- 106 : RunErrorMsg := 'Invalid numeric format';
- 150 : RunErrorMsg := 'Disk is write protected';
- 151 : RunErrorMsg := 'Bad drive request structure length';
- 152 : RunErrorMsg := 'Drive not ready';
- 154 : RunErrorMsg := 'CRC error in data';
- 156 : RunErrorMsg := 'Disk seek error';
- 157 : RunErrorMsg := 'Unknown media type';
- 158 : RunErrorMsg := 'Sector not found';
- 159 : RunErrorMsg := 'Printer out of paper';
- 160 : RunErrorMsg := 'Device write fault';
- 161 : RunErrorMsg := 'Device read fault';
- 162 : RunErrorMsg := 'Hardware failure';
- ELSE RunErrorMsg := 'I/O Error #' + Long2Str(ErrorCode);
- END;
- END;
-
- PROCEDURE Abort(ErrorMsg: STRING; ExitCode: WORD);
- (* Programmabbruch bei einem folgenschweren Fehler; falls *)
- (* das Fehlverhalten auf eine I/O-Operation zurückzufüh- *)
- (* ren ist, wird die Fehlermeldung um einen erklärenden *)
- (* Text bezüglich der Fehlerursache ergänzt. *)
- BEGIN
- DelLine;
- IF (ExitCode = FaultOnIO) AND (rc <> 0) THEN
- (* vom Betriebsystem gemeldeter Ein/Ausgabefehler *)
- WriteLn(StdErr, ^M, ErrorMsg, ', ', RunErrorMsg(rc))
- ELSE IF ExitCode = InternalError THEN
- (* interner Fehler, sollte normal nicht vorkommen *)
- WriteLn(StdErr, ^M'Internal error: ', ErrorMsg)
- ELSE
- WriteLn(StdErr, ^M^J, ErrorMsg); (* alles andere *)
- Halt(ExitCode);
- END;
-
- PROCEDURE Attention(Warning: STRING);
- (* Ausgabe einer Warnung, das Programm wird fortgeführt *)
- BEGIN
- WriteLn(StdErr, '*Warning: ', Warning);
- END;
-
- FUNCTION CheckIO: BOOLEAN;
- (* Überprüft die letzte I/O-Operation auf mögliche Fehler *)
- BEGIN
- rc := IOResult;(* System-Fehlercode holen und auswerten *)
- IF DosError >= 150 THEN rc := DosError; (* krit. Fehler *)
- CheckIO := (rc = 0);
- END;
-
- FUNCTION HeapOverflow(Request: WORD): INTEGER; FAR;
- (* Eigene Heapüberlauf-Behandlung, bricht das Programm ab *)
- BEGIN
- IF Request <> 0 THEN
- Abort('Insufficient memory', InsufficientMem);
- END;
-
- (*--------------------------------------------------------*)
- (* Initialisierung, Vor- und Nachbereitung *)
-
- PROCEDURE InstallErrorHandler;
- (* Installiert die programmeigene Fehlerbehandlung *)
- BEGIN
- OpenStdErr; (* Ausgabekanal für Fehlermeldungen öffnen *)
- HeapError := @HeapOverflow;(* Heapüberlaufrout. install.*)
- END;
-
- PROCEDURE DosVersionCheck;
- (* Das Programm bricht bei einer DOS-Version < 3.30 ab. *)
- BEGIN
- IF (Lo(DosVersion) < 3) OR (* DOS 2.XX *)
- ((Lo(DosVersion) = 3) AND (Hi(DosVersion) < $1E)) THEN
- (* DOS 3.0 .. 3.21 *)
- Abort('Incorrect DOS version', WrongDos);
- END;
-
- PROCEDURE SignOn;
- (* Programm- und Versionsmeldung, Hilfsbildschirm *)
- BEGIN
- OpenStdOut;
- IF Pos('?', ParamStr(1)) <> 0 THEN BEGIN
- WriteLn(^M^J'Find duplicate files. Version ', Version,
- ^M^J'(C) 1993 Karsten Gieselmann & DMV-Verlag'^J +
- ^M^J'Usage: DUP [drive(s):] [options] [>output]' +
- ^M^J'Options are:' +
- ^M^J' -c Search for files with equal content' +
- ' (default)' +
- ^M^J' -n Search for files with equal names',
- ^M^J' -e Search for files with equal names ' +
- ' and extensions' +
- ^M^J' -d Search all logged drives (A:..Z:)' +
- ^M^J' -r Include remote drives in default' +
- ' drive search list' +
- ^M^J' -f Display file names only (for piping)'+
- ^M^J' -s Suppress search summary',
- ^M^J' -q Quiet, don''t display path names' +
- ' during search'^J +
- ^M^J'Default drive search list is all local hard' +
- ' disk drives.'^J,
- ^M^J'Example DUP commands:' +
- ^M^J' DUP cde: Search drives C:, D: and' +
- ' E: for files with equal content.',
- ^M^J' DUP a: -n -s Search drive A: for files ' +
- 'with equal names, no summary.' +
- ^M^J' DUP -e -d Search all drives for files'+
- ' with equal names and extensions.',
- ^M^J' DUP -r Search all local and ' +
- 'remote hard disk drives for files with'^M^J +
- ' equal content.');
- Halt(HelpCalled);
- END;
- END;
-
- PROCEDURE OS2Message;
- (* Ausgabe einer Meldung, falls OS/2 als Betriebssystem *)
- (* verwendet wird, da dort Dateien vom Betriebssystem *)
- (* oder von anderen Tasks verwendet sind / sein können. *)
- (* OS/2 1.X = Lo(DosVersion) = 10 *)
- (* OS/2 2.X = Lo(DosVersion) = 20 *)
- (* Windows N(ot)T(here) (=> OS/2 3.0) ist dann wohl zur *)
- (* Abgrenzung Lo(DosVersion) = 30 ????? *)
- BEGIN
- IF Lo(DosVersion) IN [10..20] THEN
- WriteLn(StdErr, ^M^J'Attention: ',
- 'You are using DUP in an OS/2 ',
- Lo(DosVersion) DIV 10, '.',
- Hi(DosVersion), ' DOS Box,'
- + ^M^J'files locked by other tasks cannot'
- + ' be accessed!'^M^J'Warnings will be '
- + 'displayed when checking such files.'^J)
- END;
-
- PROCEDURE GetParameters; (* Auswerten der Kommandozeile *)
- CONST
- Options = 'CDEFNRSQ'; (* erlaubte Optionssymbole *)
- Switches = 'CDEFNRSQ'; (* erlaubte Schaltersymbole *)
- Prefixes = '/-'; (* erlaubte Einleitungssymbole *)
- VAR
- Param : ComStr; (* aktueller Kommandozeilenparameter *)
- Option : CHAR; (* zu bearbeitendes Optionssymbol *)
- Count : BYTE; (* Zähler für Kommandozeilenparameter *)
- BEGIN
- Count := 1;
- WHILE Count <= ParamCount DO BEGIN
- Param := ParamStr(Count);
- IF (Length(Param) >= 2) AND
- (Pos(Param[1], Prefixes) > 0) AND
- (Pos(UpCase(Param[2]), Options) > 0) THEN BEGIN
- Option := UpCase(Param[2]); (* erlaubte Direktive *)
- IF Length(Param) = 2 THEN BEGIN
- IF Pos(Option, Switches) = 0 THEN BEGIN
- (* kein Schalter, nächsten Parameter als *)
- (* Argument holen *)
- Inc(Count);
- IF Count > ParamCount THEN
- Abort('Argument expected: ' + Param + '???',
- InvalidParams);
- Param := ParamStr(Count);
- END
- END ELSE BEGIN
- IF Pos(Option, Switches) > 0 THEN
- (* direkt hinter einem Schalter darf sonst *)
- (* nichts mehr stehen! *)
- Abort('Invalid use of a switch option: ' + Param,
- InvalidParams);
- END;
- (* benötigte Aktion entsprechend analysierter *)
- (* Direktive ausführen: *)
- CASE Option OF
- 'C', 'E', 'N':
- BEGIN
- EqualContent := (Option = 'C');
- EqualName := (Option = 'N');
- EqualNameExt := (Option = 'E');
- END;
- 'D': LoggedDrives := TRUE;
- 'F': FileNamesOnly := TRUE;
- 'R': RemoteDrives := TRUE;
- 'S': SuppressSummary := TRUE;
- 'Q': QuietSearch := TRUE;
- ELSE
- Abort('Option handling missing (/' + Option + ')',
- InternalError);
- END;
- END ELSE
- IF (Pos(Param[1], Prefixes) = 0) AND
- (Param[Length(Param)] = ':') THEN
- (* Laufwerksangabe übernehmen *)
- DriveList := To_Upper(Copy(Param, 1,
- Pred(Length(Param))))
- ELSE
- Abort('Invalid option: ' + Copy(Param, 1, 2),
- InvalidParams);
- Inc(Count);
- END;
- END;
-
- PROCEDURE Initialize;
- (* Initialisieren der Datenstrukturen *)
- VAR
- h : WORD;
- Drive : CHAR;
- d, p : BYTE;
- BEGIN
- DosError := 0; (* Work-Around für Bug in Turbo 6 *)
- CheckBreak := FALSE; (* Abbruch über Crt verbieten *)
- (* Hashtabellen initialisieren: *)
- FOR h := 0 TO MaxFile DO FilePool[h] := NIL;
- FOR h := 0 TO MaxName DO NamePool[h] := NIL;
- IF DriveList = '' THEN (* Laufwerksliste aufbauen: *)
- (* Suchlaufwerke automatisch bestimmen, falls nicht *)
- (* explizit angegeben *)
- FOR Drive := 'A' TO 'Z' DO BEGIN
- d := BYTE(Drive) - 64; (* 'A' - 1 *)
- (* Zugriff war ok, Laufwerk mit in Liste aufnehmen: *)
- IF (LoggedDrives AND (DiskSize(d) > 0)) OR
- (HardDiskDrive(d) AND (LocalDrive(d) OR
- RemoteDrives)) THEN
- IF CheckIO THEN DriveList := DriveList + Drive;
- (* den evtl. von DiskSize noch vorhandenen Fehler *)
- (* code löschen: *)
- CheckIO;
- END
- ELSE BEGIN
- (* angegebene Laufwerksliste auf Gültigkeit überprüfen *)
- FOR p := 1 TO Length(DriveList) DO BEGIN
- d := Succ(Ord(DriveList[p]) - Ord('A'));
- IF NOT (DriveList[p] IN ['A'..'Z']) THEN
- Abort('Invalid drive letter ' +
- UpCase(DriveList[p]) + ':', InvalidParams)
- ELSE IF (DiskSize(d) < 0) THEN (* ungültiges *)
- Abort('Cannot access drive ' + (* Laufwerk *)
- UpCase(DriveList[p]) + ':', FaultOnIO);
- END;
- END;
- END;
-
- (*--------------------------------------------------------*)
- (* Hashschlüssel-Berechnung für Strings *)
-
- FUNCTION ComputeHash(VAR s: STRING): WORD; ASSEMBLER;
- (* Hashschlüssel (= Summe aller ASCII-Codes im String) *)
- (* berechnen *)
- ASM
- MOV DX, DS (* Datensegment-Register sichern *)
- LDS SI, s (* DS:SI ==> s *)
- CLD (* Inkrementierender Stringzugriff *)
- XOR BX, BX (* Hashschlüssel in BX akkumulieren *)
- XOR AX, AX (* AH darf Berechnung nicht beeinflussen *)
- LODSB (* Längenbyte holen ... *)
- MOV CX, AX (* und als Schleifenzähler speichern *)
- OR AX, AX (* Falls Leerstring, dann schon fertig *)
- JZ @Done (* ... und tschüß *)
- @Next:
- LODSB (* nächstes Zeichen holen ... *)
- ADD BX, AX (* ASCII-Code zum Hashschlüssel addieren *)
- LOOPNZ @Next (* ... sooft bis alle Zeichen dran waren *)
- @Done:
- MOV DS, DX (* Datensegment-Register restaurieren *)
- MOV AX, BX (* Funktionswert in AX zurückgeben *)
- END;
-
- (*--------------------------------------------------------*)
- (* Zugriff auf Namepool *)
-
- FUNCTION EnterNamePool(PathName: STRING): tStringPtr;
- (* Trägt einen Verzeichnisnamen im Namepool ein bzw. *)
- (* liefert einen Verweis auf ein bereits existierendes *)
- (* Duplikat zurück *)
- VAR
- h : WORD; (* 16-Bit Hashschlüssel *)
- Slot : tNameIndex; (* Hashtabellen-Slot für Namepool *)
- n : tNameDescPtr; (* Verweis auf Pooleintrag *)
- BEGIN
- (* Hashschlüssel berechnen und Slot ermitteln *)
- h := ComputeHash(PathName);
- Slot := h MOD (Succ(MaxName));
- (* Kollisionsliste nach Namen durchsuchen *)
- n := NamePool[Slot];
- WHILE (n <> NIL) AND NOT ((n^.HashKey = h)
- AND (n^.NamePtr^ = PathName)) DO
- n := n^.Next;
- IF n = NIL THEN BEGIN
- New(n); (* neuen Eintrag anlegen und verankern *)
- WITH n^ DO BEGIN
- HashKey := h;
- Next := NamePool[Slot];
- Prev := NIL;
- GetMem(NamePtr, Succ(Length(PathName)));
- Move(PathName, NamePtr^, Length(PathName) + 1);
- END;
- NamePool[Slot] := n;
- END;
- (* Verweis auf Pool-Eintrag zurückliefern: *)
- EnterNamePool := n^.NamePtr;
- END;
-
- (*--------------------------------------------------------*)
- (* Zugriff auf Filepool *)
-
- PROCEDURE EnterFilePool(VAR Path : PathStr;
- VAR DirEntry: SearchRec);
- (* Legt einen neuen Filepool Eintrag an und verankert ihn *)
- (* entsprechend *)
- VAR
- Key : LongInt; (* 32-Bit Hashschlüssel *)
- Slot : tFileIndex; (* Hashtabellen-Slot für Filepool *)
- f : tFileDescPtr; (* Verweis auf neuen Pooleintrag *)
-
- FUNCTION StripExtension(NameExt: fsFileName): fsFileName;
- (* bereinigt einen Dateinamen um die Dateiendung *)
- VAR
- p: BYTE;
- BEGIN
- p := Pos('.', NameExt);
- IF p > 0 THEN
- StripExtension := Copy(NameExt, 1, Pred(p))
- ELSE
- StripExtension := NameExt;
- END;
-
- BEGIN
- (* Filepool Eintrag anlegen und Komponenten versorgen *)
- New(f);
- WITH f^ DO BEGIN
- DirPtr := EnterNamePool(To_Lower(Path));
- NameExt := To_Lower(DirEntry.Name);
- Name := StripExtension(NameExt);
- Size := DirEntry.Size;
- Time := DirEntry.Time;
- Prev := NIL;
- Next := NIL;
- Inc(TotalFileCount); (* Zähler akkumulieren *)
- Inc(TotalFileSize, Size);
- IF EqualContent THEN
- Key := Size (* Schlüssel berechnen *)
- (* Schlüssel ist einfach die Dateigröße *)
- (* strict Var-String-Check muß ausgeschaltet sein *)
- {$IFOPT V+} {$DEFINE V_ON} {$V-} {$ENDIF}
- ELSE IF EqualName THEN Key := ComputeHash(Name)
- (* alle im Dateinamen vorkommenden Zeichencodes *)
- (* aufaddieren *)
- ELSE Key := ComputeHash(NameExt);
- (* Falls vorher an, strict Var-String-Check einschalten: *)
- {$IFDEF V_ON} {$V+} {$UNDEF V_ON} {$ENDIF}
- (* Element in Filepool eintragen *)
- Slot := Key MOD (Succ(MaxFile));
- IF FilePool[Slot] = NIL THEN FilePool[Slot] := f
- (* Kollisionsliste für diesen Slot ist noch nicht *)
- (* vorhanden *)
- ELSE BEGIN
- (* neues Element als erstes vor der bestehenden *)
- (* Liste einhängen *)
- Next := FilePool[Slot];
- Next^.Prev := f;
- FilePool[Slot] := f;
- END;
- END;
- END;
-
- (*--------------------------------------------------------*)
- (* Absuchen der Laufwerke und Aufbau des Dateibaums *)
-
- PROCEDURE ScanDir(Path: PathStr);
- (* Rekursives Absuchen von »Path« und zugehörigen SubDirs *)
- CONST
- FilesOnly = ReadOnly + Hidden + SysFile + Archive;
- VAR
- DirEntry: SearchRec; (* Verzeichniseintrag *)
- BEGIN
- IF NOT QuietSearch THEN BEGIN (* wo sind wir denn *)
- Write(^M'Searching ', Path); (* jetzt gerade? *)
- ClrEoL;
- END;
- FileSpec := Path + '*.*';
- WITH DirEntry DO BEGIN
- (* zunächst alle Dateien im Verzeichnis bearbeiten ...*)
- FindFirst(FileSpec, FilesOnly, DirEntry);
- WHILE DosError = 0 DO BEGIN
- IF (Name[1] <> '.') THEN (* '.' und '..' ignorieren *)
- EnterFilePool(Path, DirEntry);
- FindNext(DirEntry);
- END;
- (* ... dann erst Unterverzeichnisse rekursiv abackern *)
- FindFirst(FileSpec, Directory, DirEntry);
- WHILE DosError = 0 DO BEGIN
- IF UserBreak THEN (* Programmabbruch ermöglichen *)
- Abort('Aborted by user', UserAbort);
- IF Attr AND Directory = Directory THEN
- IF (Name[1] <> '.') THEN (* '.' und '..' ignor. *)
- ScanDir(Path + Name + '\'); (* DIR bearbeiten *)
- FindNext(DirEntry);
- END;
- END;
- END;
-
- PROCEDURE ScanDrives(Drives: tDriveStr);
- (* Steuerungsroutine, um alle angegebenen Laufwerke abzu- *)
- (* suchen *)
- VAR
- d : BYTE; (* Index für Laufwerksliste *)
- Start, Stop : LongInt; (* Marken für Laufzeitmessung *)
- BEGIN
- OpenCrtOut;
- Start := GetTimerTicks;
- FOR d := 1 TO Length(Drives) DO
- ScanDir(UpCase(Drives[d]) + ':\');
- Write(^M);
- ClrEoL;
- Stop := GetTimerTicks;
- CollectTime := Stop - Start;
- END;
-
- (*--------------------------------------------------------*)
- (* Auswerten der im Filepool akkumulierten Informationen *)
-
- FUNCTION SameFile(a, b: tFileDescPtr): BOOLEAN;
- (* Überprüft, ob zwei gegebene Dateien inhaltlich gleich *)
- (* sind; bei einem Lesefehler wird Verschiedenheit der *)
- (* Dateien angenommen. Diese Routine verbraucht den *)
- (* Hauptteil der Laufzeit beim Suchen nach Dateien glei- *)
- (* chen Inhalts. Um unterschiedliche Dateien möglichst *)
- (* schnell identifizieren zu können, wird zunächst nur *)
- (* der erste Sektor zum Vergleich herangezogen. An- *)
- (* schließend wird mit einer deutlich höheren Blockgröße *)
- (* gearbeitet, um den Datendurchsatz von der Platte zu *)
- (* erhöhen. *)
- CONST
- InitialBlockSize = 512; (* Blockgröße bei Erstzugriff *)
- OpenReadShared = $20; (* Datei-Öffnungsmodus *)
- VAR
- fa, fb : FILE; (* Kontrollblöcke für Dateien *)
- aName, bName: PathStr; (* Dateinamen *)
- aResult, bResult, (* Kontrollzähler für Zugriff *)
- BlockSize : WORD; (* Blockgröße für Dateizugr. *)
- MisMatch : BOOLEAN; (* Flag für Dateivergleich *)
-
- BEGIN
- SameFile := FALSE; (* Default: keine Übereinstimmung *)
- aName := a^.DirPtr^ + a^.NameExt; (* Dateinamen wieder *)
- bName := b^.DirPtr^ + b^.NameExt; (* zusammensetzen *)
- (* beide Dateien mit Satzlänge 1 Byte zum Lesen öffnen *)
- Assign(fa, aName);
- FileMode := OpenReadShared;
- Reset(fa, 1);
- IF NOT CheckIO THEN BEGIN
- Attention('Cannot open ' + aName + ', skipping file');
- Exit;
- END;
- Assign(fb, bName);
- FileMode := OpenReadShared;
- Reset(fb, 1);
- IF NOT CheckIO THEN BEGIN
- Attention('Cannot open ' + bName + ', skipping file');
- Exit;
- END;
-
- (* ok, jetzt stückweise einlesen und vergleichen *)
- BlockSize := InitialBlockSize;
- REPEAT
- BlockRead(fa, Buf1, BlockSize, aResult);
- BlockRead(fb, Buf2, BlockSize, bResult);
- IF aResult <> bResult THEN BEGIN
- Attention('Error reading ' + aName + ' and ' + bName
- + ', skipping files');
- Exit;
- END;
- MisMatch := NOT SameBytes(Buf1, Buf2, aResult);
- BlockSize := SizeOf(Buf1);
- UNTIL MisMatch OR (aResult = 0);
- Close(fa); Close(fb); (* Dateien wieder schließen *)
- SameFile := NOT MisMatch; (* Funktionsresultat besetzen *)
- END;
-
- PROCEDURE DisplayFile(f: tFileDescPtr);
- (* Zeigt den Namen bzw. Directoryinformationen der gege- *)
- (* benen Datei an *)
- VAR
- p : DirStr; (* Verzeichnisname *)
- n : NameStr; (* Dateiname *)
- e : ExtStr; (* Dateisuffix *)
- t : DateTime; (* Datum/Zeitstempel *)
- BEGIN
- WITH f^ DO BEGIN
- IF FileNamesOnly THEN WriteLn(DirPtr^, NameExt)
- ELSE BEGIN
- FSplit(NameExt, p, n, e);
- UnPackTime(Time, t);
- WriteLn(' ', Pad(n, 8), Pad(e, 4), Size: 9,
- t.Day: 4, '.', PadNumZero(t.Month), '.',
- Copy(PadNumZero(t.Year), 3, 2),
- t.Hour: 4, ':', PadNumZero(t.Min), ' ',
- DirPtr^);
- END;
- END;
- END;
-
- PROCEDURE SearchDuplicates;
- (* Durchsucht den Filepool nach gleichartigen/gleichnami- *)
- (* gen Dateien *)
- VAR
- h : tFileIndex; (* Index für Hashtabellenzugriff *)
- a, b : tFileDescPtr; (* Laufvariablen *)
- Match, (* Flag für Dateivergleich *)
- NewItem: BOOLEAN; (* Neuer Dateigruppen-Abschnitt? *)
- Start,
- Stop : LongInt; (* Marken für Laufzeitmessung *)
- BEGIN
- OpenStdOut; (* Umlenkung für Ausgabe wieder ermöglichen *)
- Start := GetTimerTicks;
- (* alle Slots der Filepool Hashtabelle der Reihe nach *)
- (* abklappern *)
- FOR h := 0 TO MaxFile DO BEGIN
- a := FilePool[h];
- WHILE a <> NIL DO BEGIN
- NewItem := TRUE;
- b := a^.Next;
- WHILE b <> NIL DO BEGIN
- (* Elemente »a« und »b« gegeneinander prüfen *)
- IF (EqualContent AND (a^.Size = b^.Size) AND
- SameFile(a, b)) OR (EqualName AND
- (a^.Name = b^.Name)) OR (EqualNameExt AND
- (a^.NameExt = b^.NameExt)) THEN BEGIN
- (* Dateien als Duplikate behandeln ==> *)
- (* Namen ausgeben *)
- Inc(DuplicateCount);
- Inc(DuplicateSize, a^.Size);
- IF NewItem THEN BEGIN
- Inc(DuplicateCount);
- WriteLn;
- DisplayFile(a);
- NewItem := FALSE;
- END;
- DisplayFile(b);
- (* als Duplikat erkannte Datei aus der Liste *)
- (* streichen: *)
- WITH b^ DO BEGIN
- IF Prev <> NIL THEN Prev^.Next := Next;
- IF Next <> NIL THEN Next^.Prev := Prev;
- END;
- END;
- IF UserBreak THEN (* Programmabbruch ermöglichen *)
- Abort('Aborted by user', UserAbort);
- b := b^.Next;
- END;
- a := a^.Next;
- END;
- END;
- Stop := GetTimerTicks;
- CompareTime := Stop - Start;
- END;
-
- (*--------------------------------------------------------*)
- (* Zusammenfassung & Statistik *)
-
- PROCEDURE DisplaySummary;
- (* Ausgabe von Statistikinformationen *)
- CONST
- Plural: ARRAY[BOOLEAN] OF STRING[1] = ('', 's');
- CONST
- t = 12; (* Spaltennummer zur Ausrichtung *)
- VAR
- p, z : BYTE; (* Indizes zur Ausgabe der Laufwerksliste *)
- BEGIN
- IF NOT SuppressSummary THEN BEGIN
- z := Length(DriveList);
- Write(^M^J^J'===================== DUP SEARCH SUMMARY'
- + ': ====================='^M^J^J' '
- + ' Searching drive', Plural[z > 1], ' ');
- FOR p := 1 TO Pred(z) DO Write(DriveList[p], ':,');
- WriteLn(DriveList[z], ':');
- Write(' ^ for files with equal ');
- IF EqualContent THEN Write('content.');
- IF EqualName THEN Write('names.');
- IF EqualNameExt THEN Write('names and extensions.');
- WriteLn(^M^J^J, DuplicateCount: t,
- ' Duplicates found within'^M^J,
- TotalFileCount: t, ' Files in total.'^M^J^J,
- DuplicateSize: t,
- ' Bytes allocated by duplicate files,'^M^J,
- TotalFileSize: t,
- ' Bytes allocated by files on searched drive',
- Plural[z > 1], '.'^M^J^J,
- CollectTime / 18.2: t: 2,
- ' Seconds needed to collect files,'^M^J,
- CompareTime / 18.2: t: 2,
- ' Seconds needed to compare files.'^J);
- END;
- END;
-
- (*--------------------------------------------------------*)
- (* Hauptprogramm *)
- (*--------------------------------------------------------*)
-
- BEGIN
- InstallErrorHandler; (* Fehlerbehandlung installieren *)
- DosVersionCheck; (* Abbruch bei Version < DOS 3.30 *)
- SignOn; (* Hilfebildschirm, Meldungen *)
- OS2Message; (* Meldung für die OS/2 DOS-Box *)
- GetParameters; (* Kommandozeile auswerten *)
- Initialize; (* Datenstrukturen initialisieren *)
- ScanDrives(DriveList);(* Filepool aufbauen *)
- SearchDuplicates; (* Filepool durchsuchen *)
- DisplaySummary; (* Statistik ausgeben *)
- END.
-
- (*========================================================*)
- (* Ende von DUP.PAS *)
-