/* MRBackup: Backup Routines * Filename: Backup.c * Author: Mark R. Rinfret * Date: 09/04/87 * * History: (most recent change first) * * 12/29/87 -MRR- Enable "big file" backup. * * 11/19/87 -MRR- Add the listing file name to the exclusion list. * Also, add some more error handling. * * 09/04/87 -MRR- This package was (finally) extracted from Main.c. */ #define MAIN #include "MRBackup.h" #include ":src/lib/DiskMisc.h" T_FILE *AddFile(); void DisposeList(); USHORT FileSize(); T_FILE *MakeFileNode(); static unsigned filesInDir; /* number of files in current directory */ T_FILE *savedDir; /* Current directory at start of volume */ T_FILE_LIST savedList; /* File list at start of volume */ ^L /* Add a filename to the list of excluded files. * Called with: * name: filename to be excluded */ AddExclude(name) char *name; { T_PATTERN *p; p = (T_PATTERN *) calloc(sizeof(T_PATTERN), 1); p->pattern = calloc(strlen(name)+1, 1); strcpy(p->pattern, name); if ( ! excludeList ) excludeList = p; else lastExclude->next_pattern = p; lastExclude = p; } ^L /* Add a file/directory name to the list. * Called with: * name: file/dir name string * blocks: size of file/directory in blocks (dir => 1) * isDir: true => this is a directory name * list: address of list header * Returns: * pointer to new node or NULL (out of memory) * Notes: * The filename string MUST NOT contain the volume component, * since it is used to build both source and destination * pathnames. Also note that this routine inserts the name * into the list in sorted order (case sensitive). Though this * changes the "natural order" of the files, it makes them * easier to locate on the backup disks. */ T_FILE * AddFile(name, blocks, isDir, list) char *name; USHORT blocks; BOOL isDir; T_FILE_LIST *list; { T_FILE *fnode, *tnode; if (!(fnode = MakeFileNode(name))) return NULL; fnode->blocks = blocks; fnode->is_dir = isDir; if (!list->first_file) { /* file list is empty? */ list->first_file = fnode; /* this is the new head */ } else { /* Find the insertion point for this file. */ for (tnode = list->first_file; tnode; tnode = tnode->next) { if (strcmp(fnode->filename, tnode->filename) <= 0) { fnode->next = tnode; /* insert it here */ if (tnode->previous) tnode->previous->next = fnode; fnode->previous = tnode->previous; tnode->previous = fnode; if (list->first_file == tnode) list->first_file = fnode; return fnode; } } /* Append file node to the end of the list. */ fnode->previous = list->last_file; list->last_file->next = fnode; } list->last_file = fnode; return fnode; } ^L /* Main backup routine. */ Backup() { int compare_flag, status = 0; errorCount = 0; Speak("O K, let's get to work."); DateStamp(now); mainList.first_file = mainList.last_file = currentDir = NULL; if (doListing) { if ( OpenList(listPath) ) return; } /* Get the file comparison date. Only files created on or after * this date are backed up. */ do { *since = *now; since->ds_Days -= 1; /* default interval is 1 day */ since->ds_Minute = 0; since->ds_Tick = 0; Speak("Enter the date since your last backup."); DateRequest(mainWindow, "Backup files since:",since, since); if ( (compare_flag = CompareDS(since, now) ) >= 0) DisplayBeep(NULL); } while (compare_flag >= 0); BreakPath(homePath, srcVol, srcPath); strcat(srcVol, ":"); BreakPath(backPath, destDrive, destPath); #ifdef DEBUG sprintf(debugMsg, "destDrive = %s, destPath = %s\n",destDrive,destPath); DebugWrite(debugMsg); sprintf(debugMsg, "srcVol = %s, srcPath = %s\n", srcVol,srcPath); DebugWrite(debugMsg); #endif if (*destPath) { TypeAndSpeak("The backup path must be a device name."); status = ERR_ABORT; goto done; } strcat(destDrive,":"); /* !!! Failure of GetExcludes should not prevent backup. */ if (*excludePath) if (GetExcludes()) goto done; AddExclude(listPath); /* don't try to backup list (if file) */ level = 0; diskNumber = 0; sizeLeft = 0; totalSize = 0; savedList.first_file = NULL; SetGauge(sizeLeft, totalSize); /* Force a new disk right away. */ if (status = CheckSize(false)) goto done; if (*srcPath) { /* starting path is a directory */ if (!AddFile(srcPath, 1, true, &mainList)) status = ERROR_NO_FREE_STORE; } else /* starting path is a device */ status = CollectFiles(srcPath,&mainList); if (!status) { restart: while (mainList.first_file) { /* while something in the list */ if (status = BackupFiles(&mainList)) break; if (status = BackupDirs(&mainList)) break; } } done: if (status == 0) { TypeAndSpeak("I am done, and everything seems to be O K.\n"); TypeAndSpeak("It was a pleasure working with you.\n"); } else { if (status == ERR_RESTART_VOLUME) { if ( ! ( status = RestoreContext() ) ) { sizeLeft = 0; SetGauge(sizeLeft, totalSize); NewLine(2); ListLine("*** Restarting volume ***"); goto restart; } } else if (status != ERR_ABORT) { TypeAndSpeak("Things are not well, my friend.\n"); sprintf(conmsg,"Your backup terminated with error %d.\n",status); TypeAndSpeak(conmsg); } } DisposeList(&mainList); DisposeList(&savedList); SetCurVolumeGadget(""); } ^L /* Process the next directory in the file list. * This routine is recursive. * Called with: * list: file list to be processed * Returns: * status code (0 => success) */ int BackupDirs(list) T_FILE_LIST *list; { T_FILE *dirNode; T_FILE *saved_currentDir; int status = 0; T_FILE_LIST sublist; /* subdirectory list header */ sublist.first_file = sublist.last_file = NULL; saved_currentDir = currentDir; /* remember current context */ /* There are a couple of things to note here. The first is that once * we have reached here, there should be NO simple file nodes in "list". * That currently is not handled as an error, but probably should be. * Second, since this scan modifies the list, a removal of a directory * node starts the scan at the beginning of the list since we shouldn't * reference the links in the node we're removing. Since we should be * removing the first node in the list anyway, who cares? */ for (dirNode = list->first_file; dirNode; ) { if (dirNode->is_dir) { /* found one */ currentDir = dirNode; /* set current directory */ RemFile(dirNode, list); /* if (status = NewDir(currentDir->filename)) break; */ if (status = CollectFiles(currentDir->filename,&sublist)) break; if (status = BackupFiles(&sublist)) break; if (status = BackupDirs(&sublist)) break; dirNode = list->first_file; } else /* should never get here !!! */ dirNode = dirNode->next; } currentDir = saved_currentDir; return status; } ^L /* Backup all simple files in the current list. * Called with: * list: file list header * Returns: * status code (0 => success) */ int BackupFiles(list) T_FILE_LIST *list; { T_FILE *primary, *secondary; int status = 0; /* The following loop continually scans the file list (from the front), * looking for more file entries to process. If the primary choice * fails, an attempt is made to find a file which will fit in the * space remaining. If that attempt fails, then a new disk is formatted. */ filesInDir = 0; /* Nothing in directory yet */ while (primary = FindFile(list,false)) { /* Find next file to process. */ if (primary->blocks >= totalSize) { /* It's a biggy! */ if ( ! (doBigFiles || doFormat) ) { /* "Big files" may only be backed up if "Allow Big Files" is * enabled along with "Format Destination". */ sprintf(conmsg,"%s is too big to back up!\n", primary->filename); TypeAndSpeak(conmsg); TypeAndSpeak( "In order to back up big files, you will have to select the Allow Big Files" ); TypeAndSpeak( "and Format Destination options in the Flags menu." ); } else { /* Start crankin'! */ status = DoFile(primary); } } else { if (primary->blocks >= sizeLeft) { /* file doesn't fit */ if (!(secondary = FindFile(list,true))) { /* At this point, we know that there's at least one * file to back up, but none that fit. Start a new * disk. */ if (status = NewDisk(true)) return status; continue; /* try that file again */ } primary = secondary; /* use second choice */ } if (status = DoFile(primary)) return status; } RemFile(primary,list); /* delete the node */ } if (currentDir) { /* forget current directory */ FreeFile(currentDir); currentDir = NULL; } return status; } ^L /* Check the file name about to be added against the exclude patterns. * Called with: * name: pathname to be checked * Returns: * 0 => no match * 1 => name was matched, ignore it. */ int CheckExclude(name) char *name; { int match = 0; T_PATTERN *p; for (p = excludeList; p; p = p->next_pattern) { if (match = wildcmp(p->pattern, name)) { sprintf(conmsg,"Excluding %s\n", name); WriteConsole(conmsg); break; } } return match; } ^L /* Check the current number of disk blocks (sizeLeft) available. If * less than 1, it's time to format a new disk. * Called with: * create_dir: true => OK to create continuation directory * Returns: * 0 => success * 1 => failure */ int CheckSize(create_dir) int create_dir; { if (sizeLeft > 0) return 0; return NewDisk(create_dir); } ^L /* Collect file names from a starting path. * Called with: * name: starting pathname * Note that name may be a null string when copying * the entire home device. * list: pointer to file list header * Returns: * status code (0 => success) * Notes: * CollectFiles attempts to collect all file and directory names * for a given level, starting with "name". If a simple filename * is given as a starting path, only that name will be collected. * If a directory name is given, then all pathnames contained * within that directory (only) will be collected. For each * directory name collected, CollectFiles will be called again to * collect files for that particular directory. This iterative * approach (vs. recursive) saves memory and allows us to maintain * order at each directory level. */ int CollectFiles(name, list) char *name; T_FILE_LIST *list; { int status = 0; struct FileInfoBlock *FIB = NULL; T_FILE *fnode; /* file descriptor node */ struct Lock *lock = NULL; char path[PATH_MAX+1]; USHORT top_level; #ifdef DEBUG sprintf(debugMsg,"Collecting files from %s\n",name); DebugWrite(debugMsg); #endif if (CheckStop()) return ERR_ABORT; top_level = (*name == '\0'); /* empty name implies top level */ if (!(FIB = AllocMem((long)sizeof(struct FileInfoBlock), MEMF_CHIP|MEMF_CLEAR))) { TypeAndSpeak("CollectFiles: Can not allocate memory for FIB\n"); return ERROR_NO_FREE_STORE; } strcpy(path,srcVol); /* rebuild current home path */ strcat(path, name); if (!(lock = (struct Lock *) Lock( path, SHARED_LOCK ))) { status = IoErr(); sprintf(conmsg,"CollectFiles can not lock %s; error %d\n", path, status); TypeAndSpeak(conmsg); goto out2; } if ((Examine(lock,FIB))==0){ status = IoErr(); sprintf(conmsg,"CollectFiles can not examine %s; error: %d\n", name, status); TypeAndSpeak(conmsg); goto out2; } if (FIB->fib_DirEntryType > 0){ /* "name" is a directory */ while(!status && ExNext(lock, FIB)) { if (CheckStop()) { status = ERR_ABORT; goto out2; } /* First, check the file against the exclusion list. */ if (CheckExclude(FIB->fib_FileName)) continue; if (FIB->fib_DirEntryType < 0) { /* Check the file date. */ if (CompareDS(&FIB->fib_Date, since) < 0) { #ifdef DEBUG sprintf(debugMsg,"Skipping %s\n",&FIB->fib_FileName[0]); DebugWrite(debugMsg); #endif continue; } } if (top_level) *path = '\0'; else { strcpy(path,name); strcat(path,"/"); } strcat(path,FIB->fib_FileName); if (!AddFile(path, FileSize(FIB), (FIB->fib_DirEntryType >= 0), list)) status = ERROR_NO_FREE_STORE; } /* !!! Need check here for ERROR_NO_MORE_ENTRIES */ } else { if ( ! CheckExclude(FIB->fib_FileName) && (CompareDS(&FIB->fib_Date, since ) >= 0) ) { if( ! AddFile(name, FileSize(FIB), (FIB->fib_DirEntryType >= 0), list)) status = ERROR_NO_FREE_STORE; } } out2: UnLock(lock); out1: FreeMem(FIB, (long) sizeof(struct FileInfoBlock) ); /* Don't give up if somebody else is using the file - just * ignore the error. The user can cancel us if there's a * problem. */ if (status == ERROR_OBJECT_IN_USE) status = 0; return status; } ^L /* Dispose of a file info list. * Called with: * list: pointer to list structure */ void DisposeList(list) T_FILE_LIST *list; { while (list->first_file) RemFile(list->first_file, list); } ^L /* Process one file. * Called with: * fnode: node describing file * Returns: * 0 => success * 1 => failure */ int DoFile(fnode) T_FILE *fnode; { #define MAX_RETRY 3 int status = ERR_NONE; char destName[PATH_MAX+1]; BOOL fileIsBig; int oldSize; char srcName[PATH_MAX+1]; if (++filesInDir == 1 && currentDir) { /* Create directory on first file. */ if (status = NewDir(currentDir->filename)) return status; /* Directory info is listed in NewDir, need not be done here. */ } /* If we got here with a big file, it's because doBigFiles is true. * If we can't get at least 20 blocks of the file onto this disk, * then allow CheckSize to ask for a new one. */ fileIsBig = (fnode->blocks >= totalSize); oldSize = sizeLeft; sizeLeft -= fnode->blocks; /* deduct blocks for file */ if (!fileIsBig || (oldSize < 20)) if ( CheckSize(true) ) /* check disk space */ return ERR_ABORT; if (sizeLeft > oldSize) /* we just formatted */ oldSize = sizeLeft; /*#define NOCOPY*/ /* for fast debugging */ do { if (status = CheckStop()) /* does user want out? */ return status; sprintf(srcName,"%s%s",srcVol,fnode->filename); sprintf(conmsg,"Blocks left: %5d ",oldSize); WriteConsole(conmsg); if ( !doCompress || /* not compressing files? */ IsCompressed(srcName) || /* file already compressed? */ fileIsBig || /* file too big to compress? */ fnode->blocks < 4 /* file too small to compress? */ ) { sprintf(destName,"%s%s",destVol,fnode->filename); #ifndef NOCOPY if (fileIsBig) { sprintf(conmsg, "Doing multi-volume backup of %s\n",fnode->filename); WriteConsole(conmsg); status = BackupBigFile(fnode->filename); } else { sprintf(conmsg,"Copying %s\n",fnode->filename); WriteConsole(conmsg); status = CopyFile(srcName,destName); } #endif } else { sprintf(destName,"%s%s.Z",destVol,fnode->filename); sprintf(conmsg,"Compressing %s\n",fnode->filename); WriteConsole(conmsg); #ifndef NOCOPY if (!(status = compress(srcName,destName))) status = CopyFileDate(srcName,destName); #endif } if (status) { sprintf(conmsg, "Oh darn it! I got error %d on file %s.\n", status, fnode->filename); TypeAndSpeak(conmsg); NewLine(1); ListLine(conmsg); NewLine(1); unlink(destName); ++errorCount; SetErrorGadget(); /* Update error counter */ status = GetErrOpt( ERR_ABORT | ERR_IGNORE | ERR_RETRY_FILE | ERR_RESTART_VOLUME); } else ListFileInfo(destName); } while (status == ERR_RETRY_FILE); if (status == ERR_IGNORE) status = ERR_NONE; #ifndef NOCOPY if ( !status ){ if ((sizeLeft = DiskBlocksLeft(destVol)) < 0)/* update blocks left */ status = -sizeLeft; else SetGauge(sizeLeft, totalSize); } #endif return status; } /* Compute the size of a file, in blocks, from its file information block. * If the FIB pointer is NULL, assume that the file is a directory. * Called with: * FIB: file information block (can be NULL) * Returns: * Number of disk blocks required by file. */ USHORT FileSize(FIB) struct FileInfoBlock *FIB; { USHORT blocks; if (!FIB) { blocks = 1; } else if ( FIB->fib_DirEntryType >= 0 ) blocks = 1; /* assume 1 block for directory */ else { blocks = ((FIB->fib_Size/488)+2); /* 488 = bytesinBlock - ovhd */ blocks += (blocks/70); } return blocks; } ^L /* Attempt to find a file node in the file list. If the check_size * parameter is true, only look at files which will fit in the space * remaining on the disk. * Called with: * list: pointer to file list to be searched * check_size: false => find first file in the list * true => test space available * Returns: * file node or NULL (not found) */ T_FILE *FindFile(list,check_size) T_FILE_LIST *list; int check_size; { T_FILE *fnode; for (fnode = list->first_file; fnode; fnode = fnode->next) { if (!fnode->is_dir) { /* don't consider directory nodes */ if (!check_size) break; /* take this one */ if (fnode->blocks < sizeLeft) break; } } return fnode; } /* Free memory allocated to a file node. * Called with: * node: file node */ FreeFile(node) T_FILE *node; { free(node->filename); free(node); } ^L /* An exclude file pathname has been specified. Get the patterns it * contains. * Returns: * status: 0 => success, failure otherwise */ int GetExcludes() { USHORT i, length, nonwild; T_PATTERN *p; int status = 0; char str[PATH_MAX+1]; FILE *xcld; if (! excludeHasChanged) return 0; if (!(xcld = fopen(excludePath, "r"))) { sprintf(conmsg, "I couldn't open the exclude pattern file: error %d.", errno); TypeAndSpeak(conmsg); return errno; } /* Release any previous exclude list. */ for (p = excludeList; p; p = p->next_pattern) { free(p->pattern); free(p); } excludeList = lastExclude = NULL; while (fgets(str, PATH_MAX, xcld)) { if (length = strlen(str)) { --length; str[length] = '\0'; } if (length && *str != '#') { /* ignore blank lines and comments */ nonwild = 0; for (i = 0; i < length; ++i) { if (str[i] != '*' && str[i] != '?' && str[i] != '/') ++nonwild; } if (! nonwild ) { sprintf(conmsg, "Very funny! %s will exclude everything! Ha ha ha!\n", str); TypeAndSpeak(conmsg); status = ERR_ABORT; goto done; } AddExclude(str); } } done: fclose(xcld); if (! status ) excludeHasChanged = false; return status; } /* Create a new file information node. * Called with: * name: name of file/directory * Returns: * pointer to file node or NULL (out of memory) */ T_FILE * MakeFileNode(name) char *name; { T_FILE *fnode; if (!(fnode = (T_FILE *) calloc(1,sizeof(T_FILE)))) { nomem: TypeAndSpeak("I have run out of memory!\n"); } else { if ( ! (fnode->filename = calloc(1, strlen(name)+1) ) ) goto nomem; strcpy(fnode->filename, name); /* copy the filename */ } return fnode; } ^L /* Format a new diskette. * Called with: * create_dir: true => create continuation directory, if necessary * Returns: * false => success * true => failure */ int NewDisk(create_dir) int create_dir; { char datestr[20]; char *diskPrompt; int status = 0; if (diskNumber) /* not first disk? */ Delay(TICKS_PER_SECOND * 3L); /* let disk buffers flush */ ++diskNumber; /* Increment the volume ID */ if (doFormat) { Speak("Attention! I am ready to format a new disk."); diskPrompt = "Insert a disk to be formatted in "; } else { Speak("Hi ho! I am ready for the next backup disk."); diskPrompt = "Insert the next backup disk in "; } do { if (doFormat) { Inhibit(destDrive, 1); /* Inhibit disk validation. */ if (!RequestDisk(mainWindow, destDrive, diskPrompt)) { Inhibit(destDrive, 0); /* Uninhibit the drive. */ return ERR_ABORT; } /* Don't put the colon in the volume name - * FormatDisk will happily use it as part of * the name rather than as a delimiter. */ DS2Str(datestr, "%02m-%02d-%02y", now); sprintf(destVol,"Backup %s.%d", datestr, diskNumber); if (status = FormatDisk(destDrive, destVol)) { sprintf(conmsg, "I got error %d while formatting. Sorry about that.\n", status); TypeAndSpeak(conmsg); ++errorCount; SetErrorGadget(); } } else { /* Don't format disk. */ if (!RequestDisk(mainWindow, destDrive, diskPrompt)) { return ERR_ABORT; } if (GetVolumeName(destDrive, destVol) && *destVol) status = 0; else status = ERROR_NO_DISK; } } while (status); strcat(destVol, ":"); /* add colon */ if ( (totalSize = TotalDiskBlocks(destVol)) < 0) status = -totalSize; else if ((sizeLeft = DiskBlocksLeft(destVol)) < 0) status = -sizeLeft; else { SetGauge(sizeLeft, totalSize); SaveContext(); if (create_dir && (currentDir != NULL) ) status = NewDir(currentDir->filename); } if (!status) { Header(); SetCurVolumeGadget(destVol); } return status; } ^L /* Remove a file node from the list. * Called with: * node: file node pointer * list: file list header * Returns: * nothing */ RemFile(node,list) T_FILE *node; T_FILE_LIST *list; { if (node->previous) node->previous->next = node->next; if (node->next) node->next->previous = node->previous; if (node == list->first_file) list->first_file = node->next; if (node == list->last_file) list->last_file = node->previous; if (!node->is_dir) FreeFile(node); } /* Restore the disk context to what it was when we started this volume. */ int RestoreContext() { T_FILE *newNode, *oldNode; int status = ERR_NONE; currentDir = NULL; DisposeList(&mainList); /* free up main list */ for (oldNode = savedList.first_file; oldNode; oldNode = oldNode->next) { if (! (newNode = AddFile(oldNode->filename, oldNode->blocks, oldNode->is_dir, &mainList))) { status = ERROR_NO_FREE_STORE; break; } /* If the old node is the saved current directory, the new node is * then the current directory node. */ if (oldNode == savedDir) currentDir = newNode; } return status; } /* Save the context (file list, etc.) of the current volume in case we * have an error and must restart. */ int SaveContext() { T_FILE *newNode, *oldNode; int status = ERR_NONE; savedDir = NULL; DisposeList(&savedList); /* free up old list */ for (oldNode = mainList.first_file; oldNode; oldNode = oldNode->next) { if (! (newNode = AddFile(oldNode->filename, oldNode->blocks, oldNode->is_dir, &savedList))) { status = ERROR_NO_FREE_STORE; break; } /* If the old node is the current directory, the new node is * then its counterpart. */ if (oldNode == currentDir) savedDir = newNode; } return status; }