PROGRAM TreeDuplicate; {008} CONST VersionIdentification = '2.0A'; (*********************************************************************** This software has been placed into the public domain by Digital Equipment Corporation. DISCLAIMER: The information herein is subject to change without notice and should not be construed as a commitment by Digital Equipment Corporation. Digital Equipment Corporation assumes no responsibility for the use or reliability of this software. This software is provided "as is," without any warranty of any kind, express or implied. Digital Equipment Corporation will not be liable in any event for any damages including any loss of data, profit, or savings, claims against the user by any other party, or any other incidental or consequential damages arising out of the use of, or inability to use, this software, even if Digital Equipment Corporation is advised of the possibility of such damage. DEFECT REPORTING AND SUGGESTIONS: Please send reports of defects or suggestions for improvement directly to the author: Brian Hetrick Digital Equipment Corporation 110 Spit Brook Road ZKO1-3/J10 Nashua NH 03062-2698 Do NOT file a Software Performance Report on this software, call the Telephone Support Center regarding this software, contact your Digital Field Office regarding this software, or use any other mechanism provided for Digital's supported and warranted software. FACILITY: General user utilities ABSTRACT: Duplicates one directory tree into another, attempting not to copy{008} data if possible. Intended for use as a backup utility using a DEC- net-DOS virtual disk as the backup medium. ENVIRONMENT: MS-DOS compiled with Borland International's TURBO Pascal AUTHOR: Brian Hetrick, CREATION DATE: 27 May 1986. MODIFICATION HISTORY: Brian Hetrick, 27-May-86: Version 1.0 000 - Original creation of module. Released to Easynet 28-May-86. Brian Hetrick, 30-May-86: Version 1.1 001 - Attributes on directories were not updated. Cause was that dir- ectory modification date cannot be set, and IDAttrMatch routine was testing modification date for directories. Main program then attempted to replace the target directory, but ReplaceFile simply returned. Fix is to have IDAttrMatch not look at mod- ification dates for directories; main program now uses Match- File to update the attributes. 002 - Included program name and version in banner. Released to Easynet 30-May-86 Brian Hetrick, 31-May-86: Version 1.2 003 - Introduce hook for having files accumulate on target volume, to match hook for event logging. 004 - Introduce procedure to check for MS-DOS error, instead of always explicitly checking low bit of returned Flags register. 005 - Introduce function to form name from root directory, relative directory, and file in relative directory, rather than always building directly from volume letter, absolute directory, and file in absolute directory, as a hook for later permitting root to be any directory. 006 - Avoid exteraneous copy in ExpandDirectory. 007 - Use only ASCII in message text--replace MCS copyright symbol with (c) as program may run on IBM PCs without MCS. Not released to Easynet as no user-visible improvements. Brian Hetrick, 03-Jun-86: Version 2.0 008 - Change name from VOLCOPY to TREEDUPL, as now will copy trees rooted at other than the volume root directory. 009 - Use Bela Lubkin's public domain CommandLineArgument routine to parse the command line. 010 - Deleted copyright notice as program will be submitted to DECUS program library. Released to Easynet on 3 June 1986. Submitted to DECUS Program Library in September 1986. Brian Hetrick, 03-Dec-86: Version 2.0A 011 - Discovered error day before verification master received from DECUS Program Library. Error was command qualifiers were parsed incorrectly if abbreviated. ***********************************************************************) {.PA} (* * INCLUDE FILES: *) {$I CLA.PAS} {009} (* * LABEL DECLARATIONS: *) (* * CONSTANT DECLARATIONS: *) CONST DOSFunctionChangeAttributes = $43; DOSFunctionCloseFile = $3E; DOSFunctionCreateFile = $3C; DOSFunctionCreateSubDirectory = $39; DOSFunctionDeleteDirectoryEntry = $41; DOSFunctionFindMatchFile = $4E; DOSFunctionGetDTA = $2F; DOSFunctionOpenFile = $3D; DOSFunctionReadFromFile = $3F; DOSFunctionRemoveDirectoryEntry = $3A; DOSFunctionSetDTA = $1A; DOSFunctionSetFileDateTime = $57; DOSFunctionStepThroughDirectory = $4F; DOSFunctionWriteToFile = $40; CONST DirectoryAttrMask = $10; { Attribute bit for directory } DirectoryEntrySize = 5; { Base length of DirectoryEntry } FileEntrySize = 20; { Base length of FileEntry } FileSpecLength = 12; { Length of MS-DOS base name } PathSpecLength = 127; { Length of MS-DOS path specification } ReadOnlyAttrMask = $01; { Attribute bit for read-only } (* * TYPE DECLARATIONS: *) TYPE FileSpec = STRING [FileSpecLength]; PathSpec = STRING [PathSpecLength]; DirectoryEntryPtr = ^ DirectoryEntry; DirectoryEntry = RECORD Next : DirectoryEntryPtr; Name : PathSpec END; FileEntryPtr = ^ FileEntry; FileEntry = RECORD Next : FileEntryPtr; Prev : FileEntryPtr; Size : REAL; Time : INTEGER; Date : INTEGER; Attr : BYTE; Name : FileSpec END; FileEntryQueue = RECORD Head : FileEntryPtr; Tail : FileEntryPtr END; RegPack = RECORD CASE INTEGER OF 0: (AX, BX, CX, DX, BP, SI, DI, DS, ES, Flags : INTEGER); 1: (AL, AH, BL, BH, CL, CH, DL, DH : BYTE) END; (* * OWN STORAGE: *) VAR Accumulating : BOOLEAN; Logging : BOOLEAN; SourceRoot : PathSpec; TargetRoot : PathSpec; (* * TABLE OF CONTENTS: *) {.PA} PROCEDURE ParseCommandLine; {009} (***********************************************************************{009} FUNCTIONAL DESCRIPTION: {009} Parses the program command line. {009} FORMAL PARAMETERS: {009} None. {009} RETURN VALUE: {009} None. {009} IMPLICIT INPUTS: {009} None. {009} IMPLICIT OUTPUTS: {009} Accumulating - The BOOLEAN telling whether files on the target are{009} to be retained if they are not on the source. {009} Logging - The BOOLEAN telling whether messages informing the user of{009} actions taken are to be written to the standard output. {009} SourceRoot - The root directory of the source directory tree. {009} TargetRoot - The root directory of the target directory tree. {009} SIDE EFFECTS: Will Halt the program if an error in the command line is discovered.{009} ***********************************************************************){009} VAR {009} CharIndex : INTEGER; {009} CommandValid : BOOLEAN; {009} SwitchSense : BOOLEAN; {009} SwitchText : BigString; {009} FUNCTION IsPrefix (Str1 : BigString; Str2 : BigString) : BOOLEAN; {009} VAR {009} CharIndex : INTEGER; {009} BEGIN {009} IF Length (Str1) > Length (Str2) {009} THEN {009} IsPrefix := FALSE {009} ELSE {009} IsPrefix := Str1 = Copy (Str2, 1, Length (Str1)) {011} END; {009} PROCEDURE UpCaseString (VAR Str : PathSpec); {009} VAR {009} CharIndex : INTEGER; {009} BEGIN {009} FOR CharIndex := 1 TO Length (Str) {009} DO {009} Str [CharIndex] := UpCase (Str [CharIndex]) {009} END; {009} BEGIN {009} (* {009} * Get source and destination roots {009} *) {009} SourceRoot := CommandLineArgument {009} ('Source directory: ', '/', FALSE); {009} UpCaseString (SourceRoot); {009} TargetRoot := CommandLineArgument {009} ('Destination directory: ', '/', FALSE); {009} UpCaseString (TargetRoot); {009} (* {009} * Set defaults {009} *) {009} Accumulating := TRUE; {009} Logging := TRUE; {009} (* {009} * Process switches {009} *) {009} CommandValid := TRUE; {009} SwitchText := CommandLineArgument ('', '', TRUE); {009} WHILE CommandValid AND (Length (SwitchText) > 0) {009} DO {009} BEGIN {009} UpCaseString (SwitchText); {009} (* {009} * Get rid of the leading slash {009} *) {009} Delete (SwitchText, 1, 1); {009} IF Length (SwitchText) = 0 {009} THEN {009} BEGIN {009} WriteLn ('Invalid switch: "/"'); {009} CommandValid := FALSE; {009} END; {009} IF CommandValid {009} THEN {009} (* {009} * Check for "NO" prefix {009} *) {009} IF Copy (SwitchText, 1, 2) = 'NO' {009} THEN {009} BEGIN {009} SwitchSense := FALSE; {009} Delete (SwitchText, 1, 2); {009} IF Length (SwitchText) = 0 {009} THEN {009} BEGIN {009} WriteLn ('Invalid switch: "/NO"'); {009} CommandValid := FALSE {009} END {009} END {009} ELSE {009} SwitchSense := TRUE; {009} IF CommandValid {009} THEN {009} BEGIN {009} (* {009} * Check for switch names {009} *) {009} IF IsPrefix (SwitchText, 'LOG') {009} THEN {009} Logging := SwitchSense {009} ELSE IF IsPrefix (SwitchText, 'ACCUMULATE') {009} THEN {009} Accumulating := SwitchSense {009} ELSE {009} BEGIN {009} Write ('Invalid switch: "/'); {009} IF SwitchSense = FALSE {009} THEN {009} Write ('NO'); {009} WriteLn (SwitchText, '"'); {009} CommandValid := FALSE {009} END {009} END; {009} IF CommandValid {009} THEN {009} SwitchText := CommandLineArgument ('', '', TRUE) {009} END; {009} IF NOT CommandValid {009} THEN {009} Halt {009} END; {009} {.PA} FUNCTION ErrorReturn {004} ( Registers : RegPack) : BOOLEAN; {004} (***********************************************************************{004} FUNCTIONAL DESCRIPTION: {004} Checks a set of registers returned from the MsDos procedure and de-{004} termines whether the function completed successfully. {004} FORMAL PARAMETERS: {004} Registers - A RegPack expression giving the register values returned{004} by the MsDos procedure. {004} RETURN VALUE: {004} TRUE - The MsDos function failed. {004} FALSE - The MsDos function succeeded. {004} IMPLICIT INPUTS: {004} None. {004} IMPLICIT OUTPUTS: {004} None. {004} SIDE EFFECTS: {004} None. {004} ***********************************************************************){004} BEGIN {004} ErrorReturn := (Registers . Flags AND 1) <> 0 {004} END; {004} {.PA} FUNCTION ConstructFileName {005} ( RootDirectory : PathSpec; {005} RelativeDirectory : PathSpec; {005} FileName : FileSpec) : PathSpec; {005} (***********************************************************************{005} FUNCTIONAL DESCRIPTION: {005} Constructs a path specification from a root directory, a relative{005} directory, and file name by concatenating these elements, separating{005} them by backslash if there is not already a separator. {005} FORMAL PARAMETERS: {005} RootDirectory - A PathSpec expression giving the root directory of{005} the eventual path specification. {005} RelativeDirectory - A PathSpec expression giving the directory{005} relative to RootDirectory of the eventual path specification. {005} FileName - A FileSpec expression giving the file name of the even-{005} tual path specification. {005} RETURN VALUE: {005} The resultant path specification. {005} IMPLICIT INPUTS: {005} None. {005} IMPLICIT OUTPUTS: {005} None. {005} SIDE EFFECTS: {005} None. {005} ***********************************************************************){005} CONST {005} Separator : SET OF CHAR = [':', '\', '/']; {005} VAR {005} TempName : PathSpec; {005} BEGIN {005} TempName := RootDirectory; {005} IF (Length (TempName) > 0) AND (Length (RelativeDirectory) > 0) {005} THEN {005} IF NOT (TempName [Length (TempName)] IN Separator) {005} THEN {005} Insert ('\', TempName, Length (TempName) + 1); {005} Insert (RelativeDirectory, TempName, Length (TempName) + 1); {005} IF (Length (TempName) > 0) AND (Length (FileName) > 0) {005} THEN {005} IF NOT (TempName [Length (TempName)] IN Separator) {005} THEN {005} Insert ('\', TempName, Length (TempName) + 1); {005} Insert (FileName, TempName, Length (TempName) + 1); {005} ConstructFileName := TempName {005} END; {005} {.PA} PROCEDURE ExpandDirectory ( RootDirectory : PathSpec; {005} DirectoryToExpand : DirectoryEntryPtr; VAR FileQueue : FileEntryQueue); (*********************************************************************** FUNCTIONAL DESCRIPTION: Finds and lexicographically sorts the names of all files in a di- rectory FORMAL PARAMETERS: RootDirectory - A PathSpec expression giving the root directory to{005} which DirectoryName is a relative directory. {005} DirectoryName - A DirectoryEntryPtr expression pointing to the Di- recoryEntry describing the directory to be examined FileQueue - A FileEntryQueue object which is modified to point to a newly created queue of the names of files in the directory RETURN VALUE: None. IMPLICIT INPUTS: None. IMPLICIT OUTPUTS: None. SIDE EFFECTS: Modifies and resets the DTA. This should be observable only by in- terrupt routines. Dynamically allocates storage with GetMem. ***********************************************************************) VAR FoundPos : BOOLEAN; FileNameLength : INTEGER; FileName : FileSpec; MSDOSBlock : RECORD Reserved : ARRAY [1..21] OF BYTE; Attribute : BYTE; Time : INTEGER; Date : INTEGER; SizeLow : INTEGER; SizeHigh : INTEGER; Name : ARRAY [1..13] OF CHAR END; NextFile : FileEntryPtr; OldDTA : ^ CHAR; PrevFile : FileEntryPtr; Registers : RegPack; SearchSpec : PathSpec; ThisFile : FileEntryPtr; BEGIN (* * Initialize the file queue *) FileQueue . Head := NIL; FileQueue . Tail := NIL; (* * Save the old DTA *) Registers.AH := DOSFunctionGetDTA; MsDos (Registers); OldDTA := Ptr (Registers.ES, Registers.BX); (* * Set the DTA to be the MS-DOS information block *) Registers.AH := DOSFunctionSetDTA; Registers.DS := Seg (MSDOSBlock); Registers.DX := Ofs (MSDOSBlock); MsDos (Registers); (* * Find the contents of the directory *) SearchSpec := ConstructFileName (RootDirectory, {005} DirectoryToExpand ^. Name, '*.*'); {005} SearchSpec [Length (SearchSpec) + 1] := #$00; {005} Registers.AH := DOSFunctionFindMatchFile; Registers.DS := Seg (SearchSpec [1]); Registers.DX := Ofs (SearchSpec [1]); Registers.CX := $37; MsDos (Registers); WHILE NOT ErrorReturn (Registers) {004} DO BEGIN (* * Extract the file name *) FileNameLength := 1; WHILE MSDOSBlock . Name [FileNameLength] <> #$00 DO FileNameLength := FileNameLength + 1; FileNameLength := FileNameLength - 1; FileName := Copy (MSDOSBlock . Name, 1, FileNameLength); (* * Ignore relative directories *) IF (FileName <> '.') AND (FileName <> '..') THEN BEGIN (* * Create a file entry for this file *) GetMem (ThisFile, FileEntrySize + FileNameLength); ThisFile ^. Attr := MSDOSBlock . Attribute; ThisFile ^. Time := MSDOSBlock . Time; ThisFile ^. Date := MSDOSBlock . Date; IF MSDOSBlock . SizeHigh < 0 THEN ThisFile ^. Size := MSDOSBlock . SizeHigh + 65536.0 ELSE ThisFile ^. Size := MSDOSBlock . SizeHigh; ThisFile ^. Size := ThisFile ^. Size * 65536.0; IF MSDOSBlock . SizeLow < 0 THEN ThisFile ^. Size := ThisFile ^. Size + MSDOSBlock . SizeLow + 65536.0 ELSE ThisFile ^. Size := ThisFile ^. Size + MSDOSBlock . SizeLow; ThisFile ^. Name := FileName; {006} (* * Insert the newly allocated entry into the sorted queue *) NextFile := FileQueue . Head; PrevFile := NIL; FoundPos := FALSE; WHILE NOT FoundPos DO BEGIN IF NextFile = NIL THEN FoundPos := TRUE ELSE IF NextFile ^. Name > ThisFile ^. Name THEN FoundPos := TRUE ELSE BEGIN PrevFile := NextFile; NextFile := NextFile ^. Next END END; ThisFile ^. Prev := PrevFile; IF PrevFile = NIL THEN FileQueue . Head := ThisFile ELSE PrevFile ^. Next := ThisFile; ThisFile ^. Next := NextFile; IF NextFile = NIL THEN FileQueue . Tail := ThisFile ELSE NextFile ^. Prev := ThisFile END; (* * Get the next file in the directory *) Registers.AH := DOSFunctionStepThroughDirectory; MsDos (Registers) END; (* * The directory has been expanded. Reset the DTA *) Registers.AH := DOSFunctionSetDTA; Registers.DS := Seg (OldDTA ^); Registers.DX := Ofs (OldDTA ^); MsDos (Registers) END; {.PA} PROCEDURE ExtractDirectories ( CurrentDirectory : DirectoryEntryPtr; FileQueue : FileEntryQueue; VAR DirectoryList : DirectoryEntryPtr); (*********************************************************************** FUNCTIONAL DESCRIPTION: Examines the contents of the current directory, extracts the full path names of all subdirectories, and places these subdirectory names on a queue of pending directories. FORMAL PARAMETERS: CurrentDirectory - A DirectoryEntryPtr pointing to a DirectoryEntry describing the directory whose contents are given by FileQueue. FileQueue - A FileEntryQueue pointing to a list of FileEntry objects describing the files in the directory described by Current- Directory. DirectoryList - A DirectoryEntryPtr pointing to a list of Directory- Entry objects. New DirectoryEntry objects are created for the subdirectories found on the list of FileEntry objects pointed to by FileQueue, and are placed onto this list. RETURN VALUE: None. IMPLICIT INPUTS: None. IMPLICIT OUTPUTS: None. SIDE EFFECTS: Dynamically allocates storage with GetMem. ***********************************************************************) VAR DirectoryText : PathSpec; ThisDirectory : DirectoryEntryPtr; ThisEntry : FileEntryPtr; BEGIN (* * Scan list backwards, looking for directories *) ThisEntry := FileQueue . Tail; WHILE ThisEntry <> NIL DO BEGIN IF (ThisEntry ^. Attr AND DirectoryAttrMask) <> 0 THEN BEGIN (* * This entry is a directory. *) DirectoryText := {005} ConstructFileName (CurrentDirectory ^. Name, {005} ThisEntry ^. Name, ''); {005} GetMem (ThisDirectory, DirectoryEntrySize + Length (DirectoryText)); ThisDirectory ^. Next := DirectoryList; ThisDirectory ^. Name := DirectoryText; DirectoryList := ThisDirectory END; ThisEntry := ThisEntry ^. Prev END END; {.PA} PROCEDURE AdvanceFile (VAR FileQueue : FileEntryQueue); (*********************************************************************** FUNCTIONAL DESCRIPTION: Deletes the first item on a file entry queue. FORMAL PARAMETERS: FileQueue - A FileEntryQueue object pointing to a queue of FileEntry objects. The item pointed at by the Head pointer is deleted, and the queue is adjusted for this deletion. RETURN VALUE: None. IMPLICIT INPUTS: None. IMPLICIT OUTPUTS: None. SIDE EFFECTS: Dynamically frees storage with FreeMem. ***********************************************************************) VAR ThisEntry : FileEntryPtr; BEGIN (* * Ensure that there is an item to delete *) ThisEntry := FileQueue . Head; IF ThisEntry <> NIL THEN BEGIN (* * There is. First, relink the queue around the item *) FileQueue . Head := ThisEntry ^. Next; IF FileQueue . Head = NIL THEN FileQueue . Tail := NIL ELSE FileQueue . Head ^. Prev := NIL; (* * Now free the item's storage *) FreeMem (ThisEntry, FileEntrySize + Length (ThisEntry ^. Name)) END END; {.PA} FUNCTION IDAttrMatch ( FileEntry1 : FileEntryPtr; FileEntry2 : FileEntryPtr) : BOOLEAN; (*********************************************************************** FUNCTIONAL DESCRIPTION: Determine whether two files are putatively identical. Two files are considered to be identical if they have the same name, same directory attribute, and, in the case of non-directory files,{001} the same creation/modification date and time and size. NO COM-{001} PARISON OF THE FILE CONTENTS IS MADE. FORMAL PARAMETERS: File1Desc - A FileEntryPtr pointing to a FileEntry object describing the first of the two files. File2Desc - A FileEntryPtr pointing to a FileEntry object describing the second of the two files. RETURN VALUE: TRUE - The files are considered to be identical. FALSE - The files are not considered to be identical. IMPLICIT INPUTS: None. IMPLICIT OUTPUTS: None. SIDE EFFECTS: None. ***********************************************************************) VAR Difference : BOOLEAN; BEGIN Difference := FALSE; IF FileEntry1 ^. Name <> FileEntry2 ^. Name THEN Difference := TRUE; IF (FileEntry1 ^. Attr AND DirectoryAttrMask) <> (FileEntry2 ^. Attr AND DirectoryAttrMask) THEN Difference := TRUE; IF (FileEntry1 ^. Attr AND DirectoryAttrMask) = 0 {001} THEN {001} BEGIN {001} IF FileEntry1 ^. Time <> FileEntry2 ^. Time THEN Difference := TRUE; IF FileEntry1 ^. Date <> FileEntry2 ^. Date THEN Difference := TRUE; IF FileEntry1 ^. Size <> FileEntry2 ^. Size THEN Difference := TRUE {001} END; {001} IDAttrMatch := NOT Difference END; {.PA} PROCEDURE DeleteFile ( RootDirectory : PathSpec; {005} CurrentDirectory : DirectoryEntryPtr; FileInfo : FileEntryPtr); (*********************************************************************** FUNCTIONAL DESCRIPTION: Deletes a single file or an entire subdirectory tree. When deleting an entire subdirectory tree, recurses to the depth of the subdirect- ory tree. FORMAL PARAMETERS: RootDirectory - A PathSpec expression giving the root directory to{005} which DirectoryName is a relative directory. {005} CurrentDirectory - A DirectoryEntryPtr expression pointing to a Di- rectoryEntry object describing the directory in which the file resides. FileInformation - A FileEntryPtr expression pointing to a FileEntry object describing the file to be deleted. RETURN VALUE: None. IMPLICIT INPUTS: Logging - The BOOLEAN telling whether event logging is currently on. IMPLICIT OUTPUTS: None. SIDE EFFECTS: None. ***********************************************************************) VAR NewDirEntry : DirectoryEntry; Registers : RegPack; SubDirQueue : FileEntryQueue; (* * A DirectoryEntry is used in place of a PathSpec for the name of * the single file to be deleted, in order to minimize local stor- * age requirements. This is important only as this routine is * recursive. *) BEGIN (* * If the "file" to be deleted is a directory, delete the entire * tree rooted there *) IF (FileInfo ^. Attr AND DirectoryAttrMask) <> 0 THEN BEGIN (* * Construct a directory entry for the directory *) NewDirEntry . Name := {005} ConstructFileName (CurrentDirectory ^. Name, {005} FileInfo ^. Name, ''); {005} (* * Get contents of directory *) ExpandDirectory (RootDirectory, Addr (NewDirEntry), {005} SubDirQueue); (* * Recursively delete the contents of the directory *) WHILE SubDirQueue . Head <> NIL DO BEGIN DeleteFile (RootDirectory, Addr (NewDirEntry), {005} SubDirQueue . Head); AdvanceFile (SubDirQueue) END END; (* * Generate the file specification *) NewDirEntry . Name := ConstructFileName (RootDirectory, {005} CurrentDirectory ^. Name, FileInfo ^. Name); {005} (* * Put on the trailing NUL for MS-DOS calls *) NewDirEntry . Name [Length (NewDirEntry . Name) + 1] := #$00; (* * The Read-Only attribute implies that the file cannot be * deleted. If the Read-Only attribute is on, turn it off. *) IF (FileInfo ^. Attr AND ReadOnlyAttrMask) <> 0 THEN BEGIN Registers . AH := DOSFunctionChangeAttributes; Registers . DS := Seg (NewDirEntry . Name [1]); Registers . DX := Ofs (NewDirEntry . Name [1]); Registers . CX := FileInfo ^. Attr AND NOT {001} (ReadOnlyAttrMask OR DirectoryAttrMask); {001} Registers . AL := 1; MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot change attributes on ', NewDirEntry . Name); Halt END END; (* * Actually delete the file *) IF (FileInfo ^. Attr AND DirectoryAttrMask) <> 0 THEN Registers . AH := DOSFunctionRemoveDirectoryEntry ELSE Registers . AH := DOSFunctionDeleteDirectoryEntry; Registers . DS := Seg (NewDirEntry . Name [1]); Registers . DX := Ofs (NewDirEntry . Name [1]); MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN Write ('Cannot delete '); IF (FileInfo ^. Attr AND DirectoryAttrMask) <> 0 THEN Write ('directory '); WriteLn (NewDirEntry . Name); Halt END; (* * If logging is on, note the deletion *) IF Logging THEN WriteLn ('Deleted ', NewDirEntry . Name) END; {.PA} PROCEDURE CopyFile ( SourceRootDir : PathSpec; {005} CurrentDirectory : DirectoryEntryPtr; FileInfo : FileEntryPtr; DestinRootDir : PathSpec); {005} (*********************************************************************** FUNCTIONAL DESCRIPTION: Duplicates the source file on the destination. This duplication al-{005} ways includes relative directory and file name, and file attributes.{005} In the case of non-directory files, this also includes modification{005} date and time, and contents. {005} FORMAL PARAMETERS: SourceRootDirectory - A PathSpec expression giving the root direct-{005} ory to which DirectoryName is a relative directory for the{005} source file. {005} CurrentDirectory - A DirectoryEntryPtr pointing to a DirectoryEntry object describing the directory in which the source file resides and in which the target file is to reside. FileInfo - A FileEntryPtr pointing to a FileEntry object describing the source file, and which is to describe the target file. TargetRootDirectory - A PathSpec expression giving the root direct-{005} ory to which DirectoryName is a relative directory for the tar-{005} get file. {005} RETURN VALUE: None. IMPLICIT INPUTS: Logging - The BOOLEAN telling whether event logging is currently on. IMPLICIT OUTPUTS: None. SIDE EFFECTS: None. ***********************************************************************) CONST BufferSize = 1024; VAR CopyBuffer : ARRAY [1..BufferSize] OF CHAR; DestinHandle : INTEGER; DestinName : PathSpec; Registers : RegPack; SourceHandle : INTEGER; SourceName : PathSpec; TransferSize : INTEGER; BEGIN (* * Construct the source and destination file names *) SourceName := ConstructFileName (SourceRootDir, {005} CurrentDirectory ^. Name, FileInfo ^. Name); {005} DestinName := ConstructFileName (DestinRootDir, {005} CurrentDirectory ^. Name, FileInfo ^. Name); {005} SourceName [Length (SourceName) + 1] := #$00; DestinName [Length (DestinName) + 1] := #$00; (* * Now copy the files *) IF (FileInfo ^. Attr AND DirectoryAttrMask) <> 0 THEN BEGIN (* * For a directory, simply create the target directory *) Registers . AH := DOSFunctionCreateSubDirectory; Registers . DS := Seg (DestinName [1]); Registers . DX := Ofs (DestinName [1]); MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot create directory ', DestinName); Halt END END ELSE BEGIN (* * For a file, copy the data and set the creation date and time *) Registers . AH := DOSFunctionOpenFile; Registers . AL := 0; Registers . DS := Seg (SourceName [1]); Registers . DX := Ofs (SourceName [1]); MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot open ', SourceName); Halt END; SourceHandle := Registers . AX; Registers . AH := DOSFunctionCreateFile; Registers . CX := 0; Registers . DS := Seg (DestinName [1]); Registers . DX := Ofs (DestinName [1]); MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot create ', DestinName); Halt END; DestinHandle := Registers . AX; Registers . AH := DOSFunctionReadFromFile; Registers . BX := SourceHandle; Registers . CX := BufferSize; Registers . DS := Seg (CopyBuffer); Registers . DX := Ofs (CopyBuffer); MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot read ', SourceName); Halt END; TransferSize := Registers . AX; WHILE TransferSize > 0 DO BEGIN Registers . AH := DOSFunctionWriteToFile; Registers . BX := DestinHandle; Registers . CX := TransferSize; Registers . DS := Seg (CopyBuffer); Registers . DX := Ofs (CopyBuffer); MsDos (Registers); IF ErrorReturn (Registers) OR {004} (TransferSize <> Registers . AX) THEN BEGIN WriteLn ('Cannot write ', DestinName); Halt END; Registers . AH := DOSFunctionReadFromFile; Registers . BX := SourceHandle; Registers . CX := BufferSize; Registers . DS := Seg (CopyBuffer); Registers . DX := Ofs (CopyBuffer); MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot read ', SourceName); Halt END; TransferSize := Registers . AX END; (* * The data have been copied. Set the creation date and time * to be that of the source file. *) Registers . AH := DOSFunctionSetFileDateTime; Registers . AL := 1; Registers . BX := DestinHandle; Registers . CX := FileInfo ^. Time; Registers . DX := FileInfo ^. Date; MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot set date and time on ', DestinName); Halt END; (* * Close the source and destination files *) Registers . AH := DOSFunctionCloseFile; Registers . BX := SourceHandle; MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot close ', SourceName); Halt END; Registers . AH := DOSFunctionCloseFile; Registers . BX := DestinHandle; MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot close ', DestinName); Halt END END; (* * Ensure that the source and target attributes match *) IF (FileInfo ^. Attr AND NOT DirectoryAttrMask) <> 0 THEN BEGIN Registers . AH := DOSFunctionChangeAttributes; Registers . AL := 1; Registers . DS := Seg (DestinName [1]); Registers . DX := Ofs (DestinName [1]); Registers . CX := FileInfo ^. Attr; MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot set attributes for ', DestinName); Halt END END; (* * If necessary, log the copying *) IF Logging THEN IF (FileInfo ^. Attr AND DirectoryAttrMask) <> 0 THEN WriteLn ('Created directory ', DestinName) ELSE WriteLn ('Copied ', SourceName, ' to ', DestinName) END; {.PA} PROCEDURE ReplaceFile ( SourceRootDir : PathSpec; {005} CurrentDirectory : DirectoryEntryPtr; SourceFile : FileEntryPtr; DestinRootDir : PathSpec; {005} DestinFile : FileEntryPtr); (*********************************************************************** FUNCTIONAL DESCRIPTION: Replaces a file on the destination drive with one of the same path specification from the source drive. FORMAL PARAMETERS: SourceRootDirectory - A PathSpec expression giving the root direct-{005} ory to which DirectoryName is a relative directory for the{005} source file. {005} CurrentDirectory - A DirectoryEntryPtr expression pointing to a DirectoryEntry object describing the directory in which the source and destination files are found. SourceFile - A FileEntryPtr expression pointing to a FileEntry object describing the source file. TargetRootDirectory - A PathSpec expression giving the root direct-{005} ory to which DirectoryName is a relative directory for the tar-{005} get file. {005} DestinationFile - A FileEntryPtr expression pointing to a FileEntry object describing the destination file. RETURN VALUE: None. IMPLICIT INPUTS: None. IMPLICIT OUTPUTS: None. SIDE EFFECTS: None. ***********************************************************************) BEGIN (* * For directories, nothing need be done *) IF ((SourceFile ^. Attr AND DirectoryAttrMask) = 0) OR ((DestinFile ^. Attr AND DirectoryAttrMask) = 0) THEN BEGIN (* * At least one is a file. Delete the existing thing, and copy * the new thing *) DeleteFile (DestinRootDir, CurrentDirectory, DestinFile); {005} CopyFile (SourceRootDir, CurrentDirectory, SourceFile, {005} DestinRootDir) {005} END END; {.PA} PROCEDURE MatchFile ( SourceRootDir : PathSpec; {005} CurrentDirectory : DirectoryEntryPtr; SourceFile : FileEntryPtr; DestinRootDir : PathSpec; {005} DestinFile : FileEntryPtr); (*********************************************************************** FUNCTIONAL DESCRIPTION: Modifies the non-directory attributes of a destination file to dup- licate those of a source file. FORMAL PARAMETERS: SourceRootDirectory - A PathSpec expression giving the root direct-{005} ory to which DirectoryName is a relative directory for the{005} source file. {005} CurrentDirectory - A DirectoryEntryPtr expression pointing to a Dir- ectoryEntry object describing the directory in which the destin- ation file is to be found. SourceFile - A FileEntryPtr expression pointing to a FileEntry ob- ject describing the source file. TargetRootDirectory - A PathSpec expression giving the root direct-{005} ory to which DirectoryName is a relative directory for the tar-{005} get file. {005} DestinationFile - A FileEntryPtr expression pointing to a FileEntry object describing the destination file. RETURN VALUE: None. IMPLICIT INPUTS: Logging - The BOOLEAN telling whether event logging is currently on. IMPLICIT OUTPUTS: None. SIDE EFFECTS: None. ***********************************************************************) VAR DestinName : PathSpec; Registers : RegPack; BEGIN (* * Ensure the attributes match *) IF SourceFile ^. Attr <> DestinFile ^. Attr THEN BEGIN (* * Copy attributes from the source to the destination *) DestinName := ConstructFileName (TargetRoot, {005} CurrentDirectory ^. Name, DestinFile ^. Name); {005} DestinName [Length (DestinName) + 1] := #$00; Registers . AH := DOSFunctionChangeAttributes; Registers . AL := 1; Registers . DS := Seg (DestinName [1]); Registers . DX := Ofs (DestinName [1]); Registers . CX := SourceFile ^. Attr AND NOT DirectoryAttrMask; {001} MsDos (Registers); IF ErrorReturn (Registers) {004} THEN BEGIN WriteLn ('Cannot change attributes on ', DestinName); Halt END; (* * If logging, note the change *) IF Logging THEN WriteLn ('Modified attributes of ', DestinName) END END; {.PA} (*********************************************************************** FUNCTIONAL DESCRIPTION: Modifies a target volume to duplicate as closely as possible a source volume. COMMAND LINE: [/[NO]LOG] [/[NO]ACCUMULATE] {009} RETURN VALUE: None. IMPLICIT INPUTS: SourceRoot - The root directory of the source directory tree. {009} TargetRoot - The root directory of the target directory tree. {009} Accumulating - The BOOLEAN telling whether files on the target are{009} to be retained if they are not on the source. {009} IMPLICIT OUTPUTS: None. SIDE EFFECTS: None. ***********************************************************************) VAR CurrentDirectory : DirectoryEntryPtr; DestinDirectory : FileEntryQueue; PendingDirectories : DirectoryEntryPtr; SourceDirectory : FileEntryQueue; BEGIN (* * Print the copyright notice *) WriteLn ('TREEDUPL version ', VersionIdentification); {008,002} WriteLn; (* {009} * Parse the command line {009} *) {009} ParseCommandLine; {009} (* * Initialize the directory needing duplication to be the root *) GetMem (PendingDirectories, DirectoryEntrySize); {005} PendingDirectories ^. Next := NIL; PendingDirectories ^. Name := ''; {005} (* * Copy the directories on the pending directory list *) WHILE PendingDirectories <> NIL DO BEGIN CurrentDirectory := PendingDirectories; PendingDirectories := PendingDirectories ^. Next; (* * Expand directories on the two volumes *) ExpandDirectory (SourceRoot, CurrentDirectory, SourceDirectory); {005} ExpandDirectory (TargetRoot, CurrentDirectory, DestinDirectory); {005} (* * Extract the directories from the source listing *) ExtractDirectories (CurrentDirectory, SourceDirectory, PendingDirectories); (* * Ensure that the contents of the source and destination direct- * ories match *) WHILE (SourceDirectory . Head <> NIL) OR (DestinDirectory . Head <> NIL) DO BEGIN IF SourceDirectory . Head = NIL THEN BEGIN (* * The source directory has been exhausted before the * destination directory. Delete the destination directory * file if not accumulating files. {003} *) IF NOT Accumulating {003} THEN {003} DeleteFile (TargetRoot, CurrentDirectory, {005} DestinDirectory . Head); AdvanceFile (DestinDirectory) END ELSE IF DestinDirectory . Head = NIL THEN BEGIN (* * The destination directory has been exhausted before the * source directory. Copy the file. *) CopyFile (SourceRoot, CurrentDirectory, {005} SourceDirectory . Head, TargetRoot); {005} AdvanceFile (SourceDirectory) END ELSE IF SourceDirectory . Head ^. Name < DestinDirectory . Head ^. Name THEN BEGIN (* * The destination directory does not have a file of the * same name as the file in the source directory. Copy the * file. *) CopyFile (SourceRoot, CurrentDirectory, {005} SourceDirectory . Head, TargetRoot); {005} AdvanceFile (SourceDirectory) END ELSE IF SourceDirectory . Head ^. Name > DestinDirectory . Head ^. Name THEN BEGIN (* * The destination directory has a file whose name is not * in the source directory. Delete the destinatin file if{003} * not accumulating files. {003} *) IF NOT Accumulating THEN DeleteFile (TargetRoot, CurrentDirectory, {005} DestinDirectory . Head); AdvanceFile (DestinDirectory) END ELSE IF NOT IDAttrMatch (SourceDirectory . Head, DestinDirectory . Head) THEN BEGIN (* * The source and destination directories have files of the * same name, but the identity attributes do not match. * Delete the file in the destination directory, and copy * the file from the source directory. *) ReplaceFile (SourceRoot, CurrentDirectory, {005} SourceDirectory . Head, TargetRoot, {005} DestinDirectory . Head); AdvanceFile (SourceDirectory); AdvanceFile (DestinDirectory) END ELSE BEGIN (* * The source and destination directories have files of the * same name and the identity attributes match. Make the * MS-DOS file attributes match. *) MatchFile (SourceRoot, CurrentDirectory, {005} SourceDirectory . Head, TargetRoot, {005} DestinDirectory . Head); AdvanceFile (SourceDirectory); AdvanceFile (DestinDirectory) END END; (* * The current directory has been handled. *) FreeMem (CurrentDirectory, DirectoryEntrySize + Length (CurrentDirectory ^. Name)); END END.