page@swan.ulowell.edu (Bob Page) (11/05/88)
Submitted-by: dillon@postgres.berkeley.edu (Matt Dillon) Posting-number: Volume 2, Issue 52 Archive-name: dos/fs/backup201.1 There are some serious bugs in backup/restore version V2.00. This is version 2.01, and fixes the following problems: (1) Restore would not put files in the proper sub-directory (2) Append mode for backup does not work These bugs were found by Jan Sven Trabandt. # This is a shell archive. # Remove everything above and including the cut line. # Then run the rest of the file through sh. #----cut here-----cut here-----cut here-----cut here----# #!/bin/sh # shar: Shell Archiver # Run the following text with /bin/sh to create: # backup.c # Makefile # backup.doc # This archive created: Fri Nov 4 17:11:15 1988 cat << \SHAR_EOF > backup.c /* * BACKUP.C * * (C)Copyright 1986-88, Matthew Dillon, All Rights Reserved. * Permission is granted to distribute for non-profit only. * * Thanks to Jan Sven Trabandt for finding some major bugs! * * This program will backup a filesystem or directory, creating a single * output file which can later be RESTORE'd from. It is a quick way to * backup your work to a 'backup' disk. * * backup [options] path path path ... path [-ooutputfile] * * NOTE: if -o is not specified, not output file will be created * * options: * * -A ARCHIVE. Clear the archive bit on backed up files * * -U UPDATE. Backup only those files which have the archive bit * cleared. * * -f[#KB] Floppy... actually, this option is used to force the backup * program to automatically split up the backup files in #KB * sections (default 800). I.E. -f with nothing else will * use 800KB chunks. -f200 would use 200KB chunks, etc... * * PLEASE SEE THE DOCS FOR MORE INFORMATION ON FLOPPY BACKUP * AND RESTORE * * -Fvol example: -FDF0: -FDF1: This command specifies the ordering * and number of (floppy) drives to backup to. This allows * one to change floppies in one drive while it is backing up * to another. It automatically cycles through the drives but * you still must specify an initial output file (-ofile) to * determine the base name for the files. * * This command also forces user prompting. * * PLEASE SEE THE DOCS FOR MORE INFORMATION ON FLOPPY BACKUP * AND RESTORE * * -b backup (default if executable name is 'backup') * * -r restore (default if executable nam is 'restore') * * -a append to the destination file. * * -c Compress files during backup * * -s Only display directories as we go along. * * -l/-t (either option) Causes a RESTORE to only LIST the files, * not do an actual restore. * * -T (Restore) TIMESTAMP, COMMENT, AND PROTECTION BITS ONLY. * NO files are created. It is assumed the files already * exist. The timestamp and other fields are transfered * from the backup file to the already-restored files (useful * if the files were restored with a copy command that did * non copy the timestamps. Even if the backup file is old, * you can recover most of your time data). * * -S Silent. Don't display files or directories. * * -nKB Set buffer size to use, in KB. * * -v Verbose... Additionaly, display those files which will NOT * be backed up. * * -pPAT only file-paths matching this pattern are backed up. You * may specify more than one '-p' option. * * -dPAT file-paths matching this pattern are NOT backed up. you * may specify more than one '-d' option. * * -ofile Output File * * --------------------------------------------------------------------- * * destination file format: * * HDR = <HDR><N.B><datestamp> -Backup Date * VOL = <VOL><name_size.B><name> -VOLUME base * DDS = <DDS><name_size.B><name> -down-directory * END = <END><0.B> -up directory * DAT = <DAT><N.B><datestamp> -datestamp for file * PRO = <PRO><4.B><protection> -protection for file * COM = <COM><N.B><comment> -comment for file * FIL0= <FIL0><N.B><name><size.L><data> -uncompressed file * FIL1= <FIL1><N.B><name><size.L><usize.L><data> -compressed form #1 * INC = <INC><12.B><ttlsize><strt><segsize> -next file is part of an * incomplete file */ #include <stdio.h> #include <fcntl.h> #include <local/typedefs.h> #define SDIR struct _SDIR #define SCOMP struct _SCOMP SCOMP { MNODE Node; uword Bytes; /* allocated bytes */ uword N; }; SDIR { MNODE Node; /* node in dir tree */ ubyte Type; /* XDDS or XVOL */ ubyte HaveFile; /* Something was backed up in here */ char *Element; /* path element */ }; #define XVOL 0x01 #define XDDS 0x02 #define XEND 0x03 #define XDAT 0x04 #define XPRO 0x05 #define XCOM 0x06 #define XFIL0 0x87 #define XFIL1 0x88 #define XHDR 0x09 #define XINC 0x0A ubyte Break; ubyte Restore; ubyte ListOnly; ubyte ShowFiles = 1; ubyte ShowDirs = 1; ubyte Verbose; ubyte Archive; ubyte Update; ubyte Append; ubyte Compress; ubyte TimeStampOnly; char *OutFile; long BacBytes; short BacCnt = 1; uword NumPicks; uword NumDels; char *Picks[32]; char *Dels[32]; char DirPath[256]; short DPLen; long BufSize = 65536; long BufI; ubyte *Buf; long InBufSize = 8192; long InBufI, InBufN; ubyte *InBuf; MLIST VList; /* Volume list (-F), of NODEs */ MLIST DList; /* Directory Stack */ long CLen; /* Actual compressed file output length */ MLIST CList; /* List of memory buffers */ SCOMP *CWrite; /* Current memory buffer pointer */ extern int Enable_Abort; extern void seekinputend(); extern FIB *GetFileInfo(); extern SCOMP *NewSComp(); extern void *malloc(), *GetHead(), *GetTail(), *GetSucc(), *GetPred(); main(ac, av) char *av[]; { register short i, notdone; register char *str; NewList(&VList); NewList(&DList); NewList(&CList); Enable_Abort = 0; for (str = av[0] + strlen(av[0]); str >= av[0] && *str != '/' && *str != ':'; --str); ++str; if ((*str|0x20) == 'r') Restore = 1; if (ac == 1) { printf("Backup/Restore V2.01, (c)Copyright 1988 Matthew Dillon, All Rights Reserved\n", str); printf("%s -rbactlvASTU -d<pat> -p<pat> -f[#kb] -F<vol> -n<#kb> -ofile\n", str); } for (i = 1; i < ac; ++i) { str = av[i]; if (*str != '-') continue; notdone = 1; ++str; while (notdone && *str) { switch(*str) { case 'r': Restore = 1; break; case 'b': Restore = 0; break; case 'a': Append = 1; break; case 'c': Compress = 1; break; case 'd': Dels[NumDels++] = str + 1; notdone = 0; break; case 'f': BacBytes = 800 * 1024; if (str[1] >= '0' && str[1] <= '9') { BacBytes = atoi(str+1) * 1024; notdone = 0; } break; case 'F': { /* strlen(str+1)+1 */ register NODE *node = malloc(sizeof(NODE)+strlen(str)); node->ln_Name = (char *)(node+1); strcpy(node+1, str+1); AddTail(&VList, node); } notdone = 0; break; case 'n': BufSize = atoi(str+1) * 1024; if (BufSize <= 0) BufSize = 65536; notdone = 0; break; case 'o': OutFile = str + 1; notdone = 0; break; case 'p': Picks[NumPicks++] = str + 1; notdone = 0; break; case 's': ShowFiles = 0; break; case 't': case 'l': ListOnly = 1; break; case 'v': Verbose= 1; break; case 'A': Archive= 1; break; case 'S': ShowFiles = 0; ShowDirs = 0; break; case 'T': TimeStampOnly = 1; break; case 'U': Update = 1; break; default: puts("failure"); exit(20); } ++str; } } Buf = malloc(BufSize); if (Buf == NULL) { printf("Unable to malloc %ld bytes\n", BufSize); exit(20); } if (ListOnly) InBufSize = 512; /* small buffer to avoid read overhead */ /* since we are skipping the meat */ InBuf = malloc(InBufSize); if (InBuf == NULL) { printf("Unable to malloc %ld bytes\n", InBufSize); exit(20); } if (Restore) RestoreFiles(ac,av); else BackupFiles(ac,av); } long SaveLock; BackupFiles(ac, av) char *av[]; { register short i; register char *str, *ptr; char notdone; if (OutFile && openoutput(OutFile, Append, ((BacBytes)?1:0)) == 0) exit(20); if (OutFile) { /* write header */ DATESTAMP Date; DateStamp(&Date); outentry(XHDR, sizeof(DATESTAMP), &Date); } SaveLock = CurrentDir(DupLock(((PROC *)FindTask(NULL))->pr_CurrentDir)); for (i = 1; i < ac; ++i) { str = av[i]; if (*str == '-') continue; /* * Push DDS entries for each name segment of the path */ notdone = 1; while (notdone) { for (ptr = str; *ptr && *ptr != ':' && *ptr != '/'; ++ptr); switch(*ptr) { case '/': /* normal directory */ *ptr = 0; PushDir(str, XDDS); str = ptr + 1; *ptr = '/'; break; case ':': /* volume */ *ptr = 0; PushDir(str, XVOL); str = ptr + 1; *ptr = ':'; break; default: /* directory or file */ { char *path = av[i]; FIB *fib; long lock; if (fib = GetFileInfo(path, &lock)) { if (fib->fib_DirEntryType > 0) { if (str[0]) PushDir(str, XDDS); lock = scan_directory(fib, lock); if (str[0]) PopDirs(1); } else { lock = scan_file(fib, lock); } FreeFileInfo(fib, lock); } else { printf("Unable to get info for %s\n", av[i]); } } notdone = 0; break; } } PopDirs(-1); } UnLock(CurrentDir(SaveLock)); if (OutFile) closeoutput(); } DATESTAMP Date; char Comment[256]; char Scr[256]; long Protection; long IncSize; /* Size of entire file */ long IncSeek; /* Seek offset into file */ long IncLen; /* # bytes in this segment */ RestoreFiles(ac, av) char *av[]; { register short i; register char *str; char notdone; char havedate; char havepro; char havecom; char haveinc; long bytes; long actual; long lock; SaveLock = CurrentDir(lock = DupLock(((PROC *)FindTask(NULL))->pr_CurrentDir)); for (i = 1; i < ac; ++i) { str = av[i]; if (*str == '-') continue; if (openinput(str) == 0) { printf("Unable to open %s for input\n", str); continue; } notdone = 1; havedate = havepro = havecom = haveinc = 0; while (notdone) { short c = oreadchar(); short l = oreadchar(); switch(c) { case -1: notdone = 0; break; case XVOL: oread(Scr, l); Scr[l] = 0; if (OutFile) { /* Restore to OutFile instead */ register short j = strlen(OutFile); strcpy(Scr, OutFile); if (j && OutFile[j-1] == '/') { c = XDDS; Scr[j-1] = 0; } else if (j && OutFile[j-1] == ':') { c = XVOL; Scr[j-1] = 0; } else c = XDDS; } PushDir(Scr, c); if (ListOnly) break; lock = Lock(DirPath, SHARED_LOCK); /* DirPath incs ':' */ if (lock == NULL && c == XDDS) { if (lock = CreateDir(Scr)) /* Scr excludes '/' */ UnLock(lock); lock = Lock(Scr, SHARED_LOCK); } { SDIR *sd = GetTail(&DList); sd->HaveFile = 1; /* don't remove dir */ } if (lock == NULL) { printf("Unable to create directory %s\n", Scr); notdone = 0; } else { UnLock(CurrentDir(lock)); } break; case XDDS: oread(Scr, l); Scr[l] = 0; PushDir(Scr, XDDS); if (ListOnly) break; lock = Lock(Scr, SHARED_LOCK); if (lock == NULL) { if (lock = CreateDir(Scr)) UnLock(lock); lock = Lock(Scr, SHARED_LOCK); } else { SDIR *sd = GetTail(&DList); sd->HaveFile = 1; /* don't remove dir */ } if (lock == NULL) { printf("Unable to create directory %s\n", Scr); notdone = 0; } else { UnLock(CurrentDir(lock)); } break; case XEND: { SDIR *sd = GetTail(&DList); ubyte type; c = 1; if (!sd) break; type = sd->Type; strcpy(Scr, sd->Element); c = PopDirs(1); if (ListOnly) break; if (type == XVOL) /* no parent directory */ break; lock = ParentDir(lock); if (lock == NULL) { puts("Unable to ParentDir!"); notdone = 0; } else { if (c == 0) DeleteFile(Scr); UnLock(CurrentDir(lock)); } } break; case XDAT: if (l != sizeof(DATESTAMP)) { puts("expected sizeof datestamp"); notdone = 0; break; } oread(&Date, l); havedate = 1; break; case XPRO: if (l != 4) { puts("Expected 4 bytes for protection"); notdone = 0; break; } oread(&Protection, l); havepro = 1; break; case XCOM: oread(Comment, l); Comment[l] = 0; havecom = 1; break; case XFIL0: case XFIL1: if (!havepro) Protection = 0; if (!havecom) Comment[0] = 0; if (!havedate) DateStamp(&Date); if (!haveinc) IncSize = 0; oread(Scr, l); Scr[l] = 0; oread(&bytes, 4); /* length of file */ actual = bytes; if (c == XFIL1) { oread(&actual, 4); bytes -= 4; } setinputbound(bytes); { short res = read_file(c, Scr, bytes, actual); seekinputend(); if (res < 0) goto bend; } if (ListOnly) goto bend; if (Archive) SetProtection(Scr, Protection|FIBF_ARCHIVE); else SetProtection(Scr, Protection&~FIBF_ARCHIVE); if (havedate) setfiledate(Scr, &Date); if (havecom && Comment[0]) SetComment(Scr, Comment); bend: havecom = havedate = havepro = haveinc = 0; break; case XHDR: if (l != sizeof(DATESTAMP)) { puts("expected sizeof datestamp"); notdone = 0; break; } oread(&Date, l); printf(" ----- BACKUP ----- BACKUP DATE: %s\n", datetos(&Date, Scr, NULL)); break; case XINC: if (l != 12) { puts("expected 12 bytes for XINC"); notdone = 0; break; } oread(&IncSize, 4); oread(&IncSeek, 4); oread(&IncLen, 4); haveinc = 1; break; default: printf("Unknown Record Type: %02x\n", c); notdone = 0; break; } setinputbound(-1); if (mycheckbreak()) { Break = 1; notdone = 0; break; } } if (Break) break; } UnLock(CurrentDir(SaveLock)); } FIB * GetFileInfo(path, plock) char *path; long *plock; { register long lock; register FIB *fib; *plock = NULL; if (lock = Lock(path, SHARED_LOCK)) { if (fib = malloc(sizeof(FIB))) { if (Examine(lock, fib)) { *plock = lock; return(fib); } free(fib); } UnLock(lock); } return(NULL); } FreeFileInfo(fib, lock) FIB *fib; long lock; { if (fib) free(fib); if (lock) UnLock(lock); } PushDir(element, type) char *element; { register SDIR *sd = malloc(sizeof(SDIR)); register char *str = malloc(strlen(element)+1); strcpy(str, element); sd->Type = type; sd->HaveFile = 0; sd->Element = str; AddTail(&DList, sd); strcat(DirPath+DPLen, str); if (type == XVOL) strcat(DirPath+DPLen, ":"); else strcat(DirPath+DPLen, "/"); DPLen += strlen(DirPath+DPLen); } PopDirs(num) uword num; { register SDIR *sd, *sp; char lasthave = 0; while (num && (sd = GetTail(&DList))) { lasthave |= sd->HaveFile; if (sd->HaveFile) /* MUST write end-block */ outentry(XEND, 0, NULL); if (sp = GetPred(sd)) sp->HaveFile |= sd->HaveFile; Remove(sd); DPLen -= strlen(sd->Element) + 1; if (DPLen < 0) { puts("DPLEN ERROR"); DPLen = 0; } DirPath[DPLen] = 0; free(sd->Element); free(sd); --num; } return(lasthave); } /* * SCAN_DIRECTORY() (CORE OF BACKUP) */ scan_directory(dirfib, dirlock) FIB *dirfib; long dirlock; { register FIB *fib; long lock; long save = CurrentDir(dirlock); while (ExNext(dirlock, dirfib) && (fib = GetFileInfo(dirfib->fib_FileName, &lock))) { if (fib->fib_DirEntryType > 0) { PushDir(fib->fib_FileName, XDDS); if (ShowDirs) printf("%-40s (DIR)\n", DirPath); lock = scan_directory(fib, lock); PopDirs(1); } else { lock = scan_file(fib, lock); } FreeFileInfo(fib, lock); if (Break || mycheckbreak()) { Break = 1; break; } } CurrentDir(save); return(dirlock); } mycheckbreak() { if (SetSignal(0, (SIGBREAKF_CTRL_C|SIGBREAKF_CTRL_D)) & (SIGBREAKF_CTRL_C|SIGBREAKF_CTRL_D)) { puts(" ***** BREAK *****"); return(1); } return(0); } /* * SCAN_FILE() * * If the file is accepted, write out pending directory entries, do * compression if any, and write out the file. */ scan_file(fib, lock) FIB *fib; long lock; { long save; long n; char dbuf[32]; strcat(DirPath, fib->fib_FileName); if (Update && (fib->fib_Protection & FIBF_ARCHIVE)) goto nomatch; { register short i; for (i = 0; i < NumPicks; ++i) { if (wildcmp(Picks[i], DirPath)) break; } if (i && i == NumPicks) goto nomatch; for (i = 0; i < NumDels; ++i) { if (wildcmp(Dels[i], DirPath)) goto nomatch; } } if (ShowFiles) printf("%-40s %6ld %s\n", DirPath, fib->fib_Size, datetos(&fib->fib_Date, dbuf, NULL)); { register SDIR *sd; SDIR *sdb = NULL; for (sd = GetTail(&DList); sd; sd = GetPred(sd)) { if (sd->HaveFile == 0) sdb = sd; } for (sd = sdb; sd; sd = GetSucc(sd)) { sd->HaveFile = 1; outentry(sd->Type, strlen(sd->Element), sd->Element); } } if (OutFile) { save = CurrentDir(lock); if (openinput("") == 0) { CurrentDir(save); printf("Unable to open %s\n", fib->fib_FileName); goto nomatch; } if (Compress && CompressFile(fib->fib_FileName, fib->fib_Size)) { if (OutFile && BacBytes && outbytes() + CLen > BacBytes) { if (newfile() == 0) goto skip1; } writeheaders(fib); outentry(XFIL1, strlen(fib->fib_FileName), fib->fib_FileName); CLen += 4; owrite(&CLen, 4); CLen -= 4; owrite(&fib->fib_Size, 4); transfer1(fib->fib_Size); } else { if (OutFile && BacBytes && outbytes() + fib->fib_Size > BacBytes) { if (newfile() == 0) goto skip1; } if (Compress) rollbackinput(); writeheaders(fib); outentry(XFIL0, strlen(fib->fib_FileName), fib->fib_FileName); owrite(&fib->fib_Size, 4); transfer0(fib->fib_Size); } skip1: closeinput(); CurrentDir(save); } if (Break) goto nomatch; if (Archive && !(fib->fib_Protection & FIBF_ARCHIVE)) { if (save = ParentDir(lock)) { UnLock(lock); save = CurrentDir(save); SetProtection(fib->fib_FileName, fib->fib_Protection | FIBF_ARCHIVE); lock = CurrentDir(save); } } DirPath[DPLen] = 0; return(lock); nomatch: if (Verbose) printf("%-40s (NOT ACCEPTED)\n", DirPath, fib->fib_Size, datetos(&fib->fib_Date, dbuf, NULL)); DirPath[DPLen] = 0; return(lock); } writeheaders(fib) register FIB *fib; { outentry(XDAT, sizeof(DATESTAMP), &fib->fib_Date); outentry(XPRO, 4, &fib->fib_Protection); if (fib->fib_Comment[0]) outentry(XCOM, strlen(fib->fib_Comment), fib->fib_Comment); } /* * (1) Write out XEND's to finish this archive, * (2) Open a new output file * (3) Write out XDDS's to build back to the current position */ newfile() { { register SDIR *sd; for (sd = GetTail(&DList); sd; sd = GetPred(sd)) { if (sd->HaveFile) outentry(XEND, 0, NULL); } } ++BacCnt; if (OutFile && openoutput(OutFile, Append, 1) == 0) { Break = 1; return(0); } { register SDIR *sd; DATESTAMP Date; DateStamp(&Date); outentry(XHDR, sizeof(DATESTAMP), &Date); for (sd = GetHead(&DList); sd; sd = GetSucc(sd)) { if (sd->HaveFile) outentry(sd->Type, strlen(sd->Element), sd->Element); } } return(1); } read_file(type, fname, inbytes, outbytes) short type; char *fname; { char dbuf[32]; strcat(DirPath, fname); { register short i; for (i = 0; i < NumPicks; ++i) { if (wildcmp(Picks[i], DirPath)) break; } if (i && i == NumPicks) { if (Verbose) printf("%-40s (NOT ACCEPTED)\n", DirPath); goto nomatch; } for (i = 0; i < NumDels; ++i) { if (wildcmp(Dels[i], DirPath)) { if (Verbose) printf("%-40s (NOT ACCEPTED)\n", DirPath); goto nomatch; } } } printf("%-40s %6ld %6ld %s %s\n", DirPath, inbytes, outbytes, datetos(&Date, dbuf, NULL), Comment); if (ListOnly) goto nomatch; if (TimeStampOnly) goto matchskip; openoutput(fname, 0, 0); switch(type) { case XFIL0: transfer0(inbytes); break; case XFIL1: UnCompressFile(inbytes); transfer1(outbytes); break; } closeoutput(); matchskip: DirPath[DPLen] = 0; return(1); nomatch: DirPath[DPLen] = 0; return(-1); } /* * FILE SUPPORT */ static int Infd = -1; static int Outfd = -1; static long OutBytes; openoutput(name, append, enabtail) char *name; { char *ptr = name; static NODE *VNode; /* Volume node */ long lock; extern int errno; if (Outfd >= 0) { dumpoutput(); close(Outfd); Outfd = -1; } if (enabtail) { if (VNode) VNode = GetSucc(VNode); if (!VNode) VNode = GetHead(&VList); if (VNode) { ptr = malloc(strlen(VNode->ln_Name)+strlen(name)+8); sprintf(ptr, "%s%s.%02ld", VNode->ln_Name, name, BacCnt); } else { ptr = malloc(strlen(name)+8); sprintf(ptr, "%s.%02ld", name, BacCnt); } } OutBytes = 0; while (GetHead(&VList)) { short c; short d; fprintf(stderr, "Ready for %s (y=go,n=abort) -", ptr); fflush(stderr); if ((c = getc(stdin)) == EOF) { fprintf(stderr, "EOF, aborted\n"); c = 'n'; } while ((d = getc(stdin)) != EOF && d != '\n'); if ((c|0x20) == 'y') break; if ((c|0x20) == 'n') goto skip; } if (enabtail && SaveLock) /* original directory */ lock = CurrentDir(SaveLock); if (append) { Outfd = open(ptr, O_WRONLY|O_CREAT|O_APPEND); if (Outfd >= 0) { OutBytes = lseek(Outfd, 0L, 2); if (!append) lseek(Outfd, 0L, 0); } } else { Outfd = open(ptr, O_WRONLY|O_CREAT|O_TRUNC); } if (enabtail && SaveLock) /* back to before */ CurrentDir(lock); if (Outfd < 0) printf("Unable to open output file %s (%ld)\n", ptr, errno); skip: BufI = 0; if (enabtail) free(ptr); return(Outfd >= 0); } oputc(v) char v; { ++OutBytes; if (Outfd >= 0) { if (BufI == BufSize) dumpoutput(); Buf[BufI++] = v; } } owrite(buf, n) register char *buf; register long n; { register long avail; OutBytes += n; if (Outfd >= 0) { while (BufI + n > BufSize) { avail = BufSize - BufI; bmov(buf, Buf + BufI, avail); n -= avail; buf+= avail; BufI = BufSize; dumpoutput(); } bmov(buf, Buf + BufI, n); BufI += n; } } dumpoutput() { if (Outfd >= 0 && BufI) { write(Outfd, Buf, BufI); BufI = 0; } } closeoutput() { if (Outfd >= 0) { dumpoutput(); close(Outfd); Outfd = -1; } } outbytes() { return(OutBytes); } /* * <type><len><buf> */ outentry(type, len, buf) ubyte type; ubyte len; ubyte *buf; { OutBytes += len + 2; if (Outfd >= 0) { if (BufI + len + 2 >= BufSize) dumpoutput(); Buf[BufI+0] = type; Buf[BufI+1] = len; bmov(buf, Buf+BufI+2, len); BufI += len + 2; } } ulong OMax; openinput(name) char *name; { if (Infd >= 0) close(Infd); Infd = open(name, O_RDONLY); InBufI = InBufN = 0; OMax = -1; return(Infd >= 0); } closeinput() { if (Infd >= 0) close(Infd); Infd = -1; } void seekinputend() { register long inbuf = InBufI - InBufN; register long forward = OMax; if (forward > inbuf) { lseek(Infd, forward - inbuf, 1); OMax = InBufI = InBufN = 0; return; } InBufN += forward; } setinputbound(max) { OMax = max; } oread(buf, n) char *buf; long n; { long x = 0; long avail; if (Infd < 0) return(0); if (n > OMax) n = OMax; while (n > (avail = InBufI - InBufN)) { if (InBufN == -1) return(0); bmov(InBuf + InBufN, buf, avail); OMax-= avail; n -= avail; buf += avail; x += avail; InBufI = read(Infd, InBuf, InBufSize); InBufN = 0; if (InBufI <= 0) { InBufI = 0; return(x); } } bmov(InBuf + InBufN, buf, n); InBufN += n; x += n; OMax -= n; return(x); } oreadchar() { if (!OMax || Infd < 0) return(-1); if (InBufN == InBufI) { if (InBufN < 0) return(EOF); InBufI = read(Infd, InBuf, InBufSize); InBufN = 0; if (InBufI == 0) { InBufN = InBufI = -1; return(-1); } } return(InBuf[InBufN++]); } rollbackinput() { if (Infd >= 0) lseek(Infd, 0L, 0); InBufI = InBufN = 0; } mputc(v) char v; { register SCOMP *sc = CWrite; ++CLen; if (sc->N == sc->Bytes) { sc = GetSucc(sc); if (sc == NULL); sc = NewSComp(); if (sc == NULL) { puts("SCOMP FAILED"); return(0); } sc->N = 0; CWrite = sc; } ((char *)(sc + 1))[sc->N++] = v; } mwrite(buf, n) char *buf; long n; { register SCOMP *sc = CWrite; register long avail; CLen += n; while ((avail = sc->Bytes - sc->N) < n) { bmov(buf, (char *)(sc + 1) + sc->N, avail); buf += avail; n -= avail; sc->N = sc->Bytes; sc = GetSucc(sc); if (sc == NULL) sc = NewSComp(); if (sc == NULL) { puts("SCOMP FAILED"); return(0); } sc->N = 0; } bmov(buf, (char *)(sc + 1) + sc->N, n); sc->N += n; CWrite = sc; } SCOMP * NewSComp() { register SCOMP *sc = malloc(sizeof(SCOMP) + 8192); if (sc) { sc->Bytes = 8192; sc->N = 0; AddTail(&CList, sc); } return(sc); } transfer0(n) long n; { register long len; if (Outfd < 0) return(n); for (len = BufSize - BufI; n; len = BufSize - BufI) { if (len == 0) { dumpoutput(); len = BufSize; } if (n < len) len = n; oread(Buf + BufI, len); BufI += len; n -= len; OutBytes += len; } } /* * Compression Routines * * transfer1(n) : Backup: copy compression buffer to output file */ transfer1(a) { register long len; register SCOMP *sc = GetHead(&CList); register ubyte *ptr; long n = CLen; if (Outfd < 0) return(n); for (sc = GetHead(&CList); sc && n; sc = GetSucc(sc)) { len = sc->Bytes; ptr = (ubyte *)(sc + 1); if (n < len) len = n; n -= len; while (len > BufSize - BufI) { bmov(ptr, Buf + BufI, BufSize - BufI); ptr += BufSize - BufI; len -= BufSize - BufI; OutBytes += BufSize - BufI; BufI = BufSize; dumpoutput(); } bmov(ptr, Buf + BufI, len); BufI += len; OutBytes += len; } if (n) puts("Unexpected EOF in compression file"); } #asm ; Taken from my DRES.LIBRARY so we don't have to open the ; library. public _GetHead public _GetTail public _GetSucc public _GetPred _GetSucc: _GetHead: move.l 4(sp),A0 move.l (A0),A0 tst.l (A0) bne .gh1 .ghz sub.l A0,A0 .gh1 move.l A0,D0 rts _GetTail: move.l 4(sp),A0 move.l 8(A0),A0 tst.l 4(A0) beq .ghz move.l A0,D0 rts _GetPred: move.l 4(sp),A0 move.l 4(A0),A0 tst.l 4(A0) beq .ghz move.l A0,D0 rts #endasm #define ngetchar() oreadchar() #define nputchar(n) mputc(n) #ifndef min #define min(a,b) ((a>b) ? b : a) #endif #define BITS 13 #if BITS == 16 #define HSIZE 69001 /* 95% occupancy */ #endif #if BITS == 15 #define HSIZE 35023 /* 94% occupancy */ #endif #if BITS == 14 #define HSIZE 18013 /* 91% occupancy */ #endif #if BITS == 13 #define HSIZE 9001 /* 91% occupancy */ #endif #if BITS <= 12 #define HSIZE 5003 /* 80% occupancy */ #endif typedef long code_int; typedef long count_int; typedef unsigned char char_type; #define MAXCODE(n_bits) ((1 << (n_bits)) - 1) #define INIT_BITS 9 /* initial number of bits/code */ int n_bits; /* number of bits/code */ int maxbits; /* user settable max # bits/code */ code_int maxcode; /* maximum code, given n_bits */ code_int maxmaxcode; /* should NEVER generate this code */ count_int htab[HSIZE]; uword codetab[HSIZE]; #define htabof(i) htab[i] #define codetabof(i) codetab[i] code_int hsize = HSIZE; /* for dynamic table sizing */ #define tab_prefixof(i) codetabof(i) #define tab_suffixof(i) ((char_type *)(htab))[i] #define de_stack ((char_type *)&tab_suffixof(1<<BITS)) code_int free_ent; /* first unused entry */ code_int getcode(); #define CHECK_GAP 10000 /* ratio check interval */ int block_compress = 1; int clear_flg; long ratio; count_int checkpoint; /* * the next two codes should not be changed lightly, as they must not * lie within the contiguous general code space. */ #define FIRST 257 /* first free entry */ #define CLEAR 256 /* table clear output code */ static int offset; long int in_count = 1; /* length of input */ /* * Compress a file to memory-buffers and return TRUE if the compressed * size is smaller than the actual size. */ CompressFile(name, fsize) { long fcode; code_int i = 0; int c; code_int ent; int disp; code_int hsize_reg; int hshift; if (wildcmp("*.Z", name) || wildcmp("*.ARC", name) || wildcmp("*.ZOO", name)) { printf(" Will not compress %s\n", name); return(0); } CLen = 0; CWrite = GetHead(&CList); if (CWrite == NULL) CWrite = NewSComp(); CWrite->N = 0; bzero(htab, sizeof(htab)); bzero(codetab, sizeof(codetab)); hsize = HSIZE; if ( fsize < (1 << 12) ) hsize = min ( 5003, HSIZE ); else if ( fsize < (1 << 13) ) hsize = min ( 9001, HSIZE ); else if ( fsize < (1 << 14) ) hsize = min ( 18013, HSIZE ); else if ( fsize < (1 << 15) ) hsize = min ( 35023, HSIZE ); else if ( fsize < 47000 ) hsize = min ( 50021, HSIZE ); offset = clear_flg = ratio = 0; in_count = 1; checkpoint = CHECK_GAP; n_bits = INIT_BITS; /* number of bits/code */ maxbits = BITS; /* user settable max # bits/code */ maxcode = MAXCODE(INIT_BITS); /* maximum code, given n_bits */ maxmaxcode = 1 << BITS; /* should NEVER generate this code */ free_ent = ((block_compress) ? FIRST : 256 ); ent = ngetchar(); hshift = 0; for ( fcode = (long) hsize; fcode < 65536L; fcode *= 2L ) hshift++; hshift = 8 - hshift; /* set hash code range bound */ hsize_reg = hsize; cl_hash((count_int)hsize_reg); /* clear hash table */ while ((c = ngetchar()) != EOF) { in_count++; fcode = (long) (((long) c << maxbits) + ent); i = ((c << hshift) ^ ent); /* xor hashing */ if (htabof (i) == fcode) { ent = codetabof(i); continue; } else if ((long)htabof (i) < 0) /* empty slot */ goto nomatch; disp = hsize_reg - i; /* secondary hash (after G. Knott) */ if (i == 0) disp = 1; probe: if ((i -= disp) < 0) i += hsize_reg; if (htabof (i) == fcode) { ent = codetabof(i); continue; } if ((long)htabof (i) > 0) goto probe; nomatch: output ((code_int) ent); ent = c; if (free_ent < maxmaxcode) { codetabof(i) = free_ent++; /* code -> hashtable */ htabof(i) = fcode; } else if ((count_int)in_count >= checkpoint && block_compress) cl_block (); } /* * Put out the final code. */ output((code_int)ent); output((code_int)-1); return(CLen < fsize); } static char buf[BITS]; char_type lmask[9] = {0xff, 0xfe, 0xfc, 0xf8, 0xf0, 0xe0, 0xc0, 0x80, 0x00}; char_type rmask[9] = {0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff}; output( code ) code_int code; { register int r_off = offset, bits= n_bits; register char * bp = buf; if ( code >= 0 ) { /* * Get to the first byte. */ bp += (r_off >> 3); r_off &= 7; /* * Since code is always >= 8 bits, only need to mask the first * hunk on the left. */ *bp = (*bp & rmask[r_off]) | (code << r_off) & lmask[r_off]; bp++; bits -= (8 - r_off); code >>= 8 - r_off; /* Get any 8 bit parts in the middle (<=1 for up to 16 bits). */ if ( bits >= 8 ) { *bp++ = code; code >>= 8; bits -= 8; } /* Last bits. */ if(bits) *bp = code; offset += n_bits; if (offset == (n_bits << 3)) { bp = buf; bits = n_bits; mwrite(bp, bits); bp += bits; bits = 0; offset = 0; } /* * If the next entry is going to be too big for the code size, * then increase it, if possible. */ if (free_ent > maxcode || (clear_flg > 0)) { /* * Write the whole buffer, because the input side won't * discover the size increase until after it has read it. */ if (offset > 0) mwrite(buf, n_bits); offset = 0; if (clear_flg) { n_bits = INIT_BITS; maxcode = MAXCODE(INIT_BITS); clear_flg = 0; } else { n_bits++; if (n_bits == maxbits) maxcode = maxmaxcode; else maxcode = MAXCODE(n_bits); } } } else { /* * At EOF, write the rest of the buffer. */ if (offset > 0) mwrite(buf, (offset + 7) / 8); offset = 0; } } char * xrindex(s, c) /* For those who don't have it in libc.a */ register char *s, c; { char *p; for (p = NULL; *s; s++) { if (*s == c) p = s; } return(p); } cl_block() /* table clear for block compress */ { register long int rat; checkpoint = in_count + CHECK_GAP; if (in_count > 0x007fffff) { /* shift will overflow */ rat = CLen >> 8; if (rat == 0) { /* Don't divide by zero */ rat = 0x7fffffff; } else { rat = in_count / rat; } } else { rat = (in_count << 8) / CLen; /* 8 fractional bits */ } if (rat > ratio) { ratio = rat; } else { ratio = 0; cl_hash ( (count_int) hsize ); free_ent = FIRST; clear_flg = 1; output ( (code_int) CLEAR ); } } cl_hash(hsize) /* reset code table */ register count_int hsize; { register count_int *htab_p = htab+hsize; register long i; register long m1 = -1; i = hsize - 16; do { /* might use Sys V memset(3) here */ *(htab_p-16) = m1; *(htab_p-15) = m1; *(htab_p-14) = m1; *(htab_p-13) = m1; *(htab_p-12) = m1; *(htab_p-11) = m1; *(htab_p-10) = m1; *(htab_p-9) = m1; *(htab_p-8) = m1; *(htab_p-7) = m1; *(htab_p-6) = m1; *(htab_p-5) = m1; *(htab_p-4) = m1; *(htab_p-3) = m1; *(htab_p-2) = m1; *(htab_p-1) = m1; htab_p -= 16; } while ((i -= 16) >= 0); for ( i += 16; i > 0; i-- ) *--htab_p = m1; } UnCompressFile(insize) { register char_type *stackp; register int finchar; register code_int code, oldcode, incode; /* * As above, initialize the first 256 entries in the table. */ bzero(htab, sizeof(htab)); bzero(codetab, sizeof(codetab)); offset = clear_flg = ratio = 0; in_count = 1; checkpoint = CHECK_GAP; n_bits = INIT_BITS; /* number of bits/code */ maxbits = BITS; /* user settable max # bits/code */ maxcode = MAXCODE(INIT_BITS); /* maximum code, given n_bits */ maxmaxcode = 1 << BITS; /* should NEVER generate this code */ for ( code = 255; code >= 0; code-- ) { tab_prefixof(code) = 0; tab_suffixof(code) = (char_type)code; } free_ent = ((block_compress) ? FIRST : 256 ); finchar = oldcode = getcode(); if (oldcode == -1) /* EOF already? */ return; /* Get out of here */ oputc((char)finchar); /* first code must be 8 bits = char */ stackp = de_stack; while ((code = getcode()) > -1) { if ((code == CLEAR) && block_compress) { for (code = 255; code >= 0; code--) tab_prefixof(code) = 0; clear_flg = 1; free_ent = FIRST - 1; if ((code = getcode()) == -1) /* O, untimely death! */ break; } incode = code; /* * Special case for KwKwK string. */ if (code >= free_ent) { *stackp++ = finchar; code = oldcode; } /* * Generate output characters in reverse order */ while ( code >= 256 ) { *stackp++ = tab_suffixof(code); code = tab_prefixof(code); } *stackp++ = finchar = tab_suffixof(code); /* * And put them out in forward order */ do oputc (*--stackp); while (stackp > de_stack); /* * Generate the new entry. */ if ((code=free_ent) < maxmaxcode) { tab_prefixof(code) = (unsigned short)oldcode; tab_suffixof(code) = finchar; free_ent = code+1; } /* * Remember previous code. */ oldcode = incode; } } code_int getcode() { /* * On the VAX, it is important to have the register declarations * in exactly the order given, or the asm will break. */ register code_int code; static int offset = 0, size = 0; static char_type buf[BITS]; register int r_off, bits; register char_type *bp = buf; if (clear_flg > 0 || offset >= size || free_ent > maxcode) { /* * If the next entry will be too big for the current code * size, then we must increase the size. This implies reading * a new buffer full, too. */ if ( free_ent > maxcode ) { n_bits++; if ( n_bits == maxbits ) maxcode = maxmaxcode; /* won't get any bigger now */ else maxcode = MAXCODE(n_bits); } if ( clear_flg > 0) { maxcode = MAXCODE (n_bits = INIT_BITS); clear_flg = 0; } size = oread(buf, n_bits); if (size <= 0) return -1; /* end of file */ offset = 0; size = (size << 3) - (n_bits - 1); } r_off = offset; bits = n_bits; /* * Get to the first byte. */ bp += (r_off >> 3); r_off &= 7; /* Get first part (low order bits) */ code = (*bp++ >> r_off); bits -= (8 - r_off); r_off = 8 - r_off; /* now, offset into code word */ /* Get any 8 bit parts in the middle (<=1 for up to 16 bits). */ if ( bits >= 8 ) { code |= *bp++ << r_off; r_off += 8; bits -= 8; } /* high order bits. */ code |= (*bp & rmask[bits]) << r_off; offset += n_bits; return code; } SHAR_EOF cat << \SHAR_EOF > Makefile # Makefile for backup SYMS= include:symbols.m SYMC= include:local/makesymbols.c CFLAGS= +L +I$(SYMS) LFLAGS= +Q DD= srcc: OD= T: $(DD)backup: backup.c cc $(CFLAGS) backup.c -o $(OD)backup.o ln +Q $(OD)backup.o -lsup32 -lc32 -o $(DD)backup delete $(OD)backup.o $(SYMS): $(SYMC) make -f include:local/Makefile SHAR_EOF cat << \SHAR_EOF > backup.doc BACKUP.DOC V2.01 3 November 1988 (c)Copyright 1988, Matthew Dillon, All Rights Reserved Freely Distributable for non-profit only (this is the same document as backup.doc v2.00) (I) OVERVIEW Backup and Restore (same executable, just renamed) allow you to backup any directory tree with optional compression, and later extract all or part of the tree. The protection, date, and file comment is saved with each file. SEE SECTION (6), FLOPICAL BACKUP, for options pertaining to backing up directly to floppies. LIMITATIONS: When using -c (compress), the entire result of the compressed file must fit into memory. Any files (after compression, if any) larger than a floppy when backing up to floppies cannot be handled yet. (I) INSTALLATION Copy the (1) executable to your C: directory twice, naming one 'Backup' and the other 'Restore'. The program looks at the first character of its name to determine whether the default is to backup or restore. If you don't have the space, all is not lost ... you simply need to specify backup or restore explicitly with the options (-b or -r). (2) EXOTIC FEATURES Backup does not write directory trees which contain no files. Thus, when doing incremental backups on a large hard disk, the structure of directory sub-trees which have no new files in them will not be written. So if you only modified a couple of files since the last backup, there won't be 50K of structure-overhead. You can specify compression (-c) in the backup. All files are compressed (there must be enough RAM to hold the largest file in its compressed state). Those files which would turn out larger after compression than before are NOT compressed. Certain file suffixes cause the compression attempt to be skipped entire: .Z, .ARC, and .ZOO files IT IS SUGGESTED YOU USE THE COMPRESSION FEATURE EVEN THOUGH IT SLOWS DOWN THE BACKUP. The resulting backup file will be significantly reduced in size. Even though a full backup will take a long time, you only do that every once in a while and as incremental backups are shorter, they take much less time. 'Pick' patterns are supported (backup only paths which match the specified patterns), and 'Discard' patterns are supported (do not backup paths which match the specified patterns). Discard patterns override Pick patterns. (3) OPTIONS Here is a list of Backup/Restore options. -A ARCHIVE. (Backup) Causes the archive bit to be cleared on files being backed up. (Protection bit -> 1) (Restore): If specified, the archive bit is cleared on restored files. Otherwise the archive bit will be set, causing the files to be backed up again on the next incremental backup. -U UPDATE. (Backup) Restricts the backup to only those files which have the archive bit set. (Protection bit == 0) -b BACKUP (this is the default if the first character in the executable is a 'b') -r RESTORE (this is the default if the first character in the executable is an 'r') -a APPEND (Backup) Cause the backup to be appended to the specified (-o) file rather than overwriting it. Useful for incremental backups. -c COMPRESS (Backup) Cause all files to be compressed, if possible. Restore automatically decompresses files which have been compressed. This is the same algorithm used in the UNIX compress command. NO COMPRESSION WILL EVEN BE ATTEMPED ON FILES WITH THE FOLLOWING SUFFIXES: .Z .ARC .ZOO -f[#kb] Turn on backup fragmentation and specify the fragment size, default 800K (for backing up to floppies). NOTE: Currently, files larger than the fragment size cannot be backed up! Note: a suffix is added to the file name w/ a sequence number, but each fragment is a complete backup file unto itself and may be restored out of order. Any numerical value specified is in K, i.e. -f800 == 800K -Fvol: Add the volume (df0:, df1:, etc..) to the drive sequencing list. The idea is to specify several -F options which are cycled through and prefix each fragment. This also enables prompting... the program prompts whenever it switches to the next fragment. If -F is not specified at all and -f is, no prompting occurs. -s Only display directories as we go along -S Silent... don't display files or directories -v Verbose... display those files which will NOT be backed up as well as those that will. -l/-t (both do the same thing). (Restore) LIST the archive only, do not do an actual restore. -T (Restore) Restore ONLY the timestamp, comment, and protection fields. This is incredibly useful to restore timestamp information to files that already exist. NO files are overwritten or created on the destination volume(s). -n# (Backup) Sets the output buffer size, in KILOBYTES. Default is 64. -pPAT (Backup) Restrict the backup to files which match the specified pattern. Up to 32 patterns may be specified. (Restore) Extract only those files which match the specified pattern. NOTE: The pattern is matched with the COMPLETE PATH as shown in the -t option. '*' and '?' wildcarding is supported -dPAT (Backup) Do not backup files which match the specified pattern. Up to 32 patterns may be specified. (Restore) Do not extract files which match the specified pattern. NOTE: The pattern is matched with the COMPLETE PATH as shown in the -t option. '*' and '?' wildcarding is supported. -oFILE (Backup) Specify the OUTPUT FILE for a backup. IF NO OUTPUT FILE IS SPECIFIED, NO OUTPUT FILE IS CREATED. I.E. you can take a second pass just to clear the archive bit. (Restore) Specify the directory to place the restored directory tree. (This overrides the volume the backup was saved from). If no directory specified, the original volume used in the backup will be restored to. If no volume was specified in the backup but rather a relative path was, the restore will be relative to the current directory. (4) STANDARD METHOD OF BACKING UP Note that with combinations of -o, -A, and -U, you have many choices available to you when backing up files. For hard disks, backups are normally done in two phases: -Once a month do a complete backup of your hard disk -Every day do a much smaller 'incremental' backup which backs up only those files which have been modified/created since the last backup. The master backup, plus the N incremental backups together reconstruct your disk up to the moment. After a while the number of incremental backups will be so great (and messy), that you will want to do another complete backup and start again. IN THIS WAY, the COMPRESS option can be put to good use. The incremental backups are usually small, and can be compressed relatively quickly. The complete backup should also be compressed (-c in case you forgot) though this will take a much longer time... but then again the complete backup is not done every day. UPDATING THE ARCHIVE BIT ON THE FLY VERSES TAKING A SECOND PASS. Updating the archive bit involves writing to the disk. For complete safety you probably do NOT want to update the archive bit while you are backing up the files in question. The following two-pass method works well: (COMPLETE BACKUP) backup -c -d"*.o" volume: -ooutput backup -A volume: (INCREMENTAL BACKUP) backup -U -c -d"*.o" volume: -ooutput backup -A volume: The -d option is saying "Don't backup all those object modules I left laying around". The first command backs up only those files which have be modified or created but does NOT update the archive bit. The second command updates the archive bit without doing anything else (no output file is specified and thus none is created). The second command goes extremely quickly since files are not actually read. For your INCREMENTAL backups, risking modifying the archive bit while backing the files up is feasible, Since very few files will be backed up (comparitively): backup -a -UA -c -d"*.o" volume: -ooutput In this case I use the -a option (append). You might NOT want to do this and save the incremental backups as separate files. (5) STANDARD METHOD OF RESTORING Restoring a backup to the original volume: (Restore or Backup -r) NOTE: If the -A option is specified, restored files will have their Archive bit cleared. If -A is NOT specified, files will have their archive bit set and will thus be re-backed up on the next incremental backup. NOTE: When restoring files, remember that the incremental backups will have the newest modifications and should be specified LAST: Restore completebkfile incr1 incr2 incr3.... Partial Restore using -p: Restore -pstuffIWant bkfile1 bkfile2 bkfile3... ARCHIVE LISTING: Restore -t bkfile1 bkfile2.... NOTE: Two 'sizes' are given in the listing. The first is the # bytes the file takes up in the archive, the second is the actual size of the file. (6) FLOPICAL BACKUP AND RESTORE There are two problems associated with floppy backup and restore: (1) A single file on the HD may be two big for a single floppy (2) It may take a lot of floppies to backup an HD Additionaly, one wants the following conveniences: (1) Be able to backup to a logical progression of (floppy) drives, inserted blank disks in while the system is working on other drives. (2) Be able to restore the same way (3) If one or more floppies is destroyed be able to restore from the remaining floppies. Currently, you can specify which devices to cycle through with the -F option (-Fdf0: -Fdf1: -Fdf2:), but BACKUP will always prompt you before continuing. Since it uses the console device, you can specify the yes response as many times as you have floppies in ready drives. Currently, files larger than the specified -f size (800K default) cannot be handled if using -f/-F ... However, by enabling compression larger files can still fit. Currently, the entire compressed result of a file must be able to fit into memory during the backup. *** BACKUP. Specify the -f option and then -Fvol: for each floppy drive (in the order you want) to backup to. Example: -f -Fdf0: -Fdf1: (Also keep in mind all the options listed in sections (3), (4), & (5)) Currently, you must have PRE-FORMATTED all your backup disks since the backup/restore works through normal files. The system will prompt for readyness every time it completes a section. The backup process never deletes files, only creates or overwrites them. Thus, accidently sticking in a non-blank or incorrect backup disk will probably result in a disk-full requester.... not Fatal, though inconvenient. You must still specify the -o option to name the output file, which will be prefixed with the next -F volume in sequence and suffixed with the sequence number. (.nn) The disk labels for your floppies may all be the same. EXAMPLE: backup -c -d"*.o" -f -Fdf0: -Fdf1: -Fdf2: volume: -oxxbak backup -A volume: NOTE: You do NOT specifiy a volume prefix in the -o option .. those specified by -F are automatically cycled through as the prefix. * If your hard disk has enough space on it I suggest you backup to an alternate partition using only -f (not -F which forces user prompts), then copy the chunks to floppies after the entire backup has completed. *** RESTORE. Restore works the same way. EACH DISK IS INDEPENDANT! You may insert disks in any order to be restored. Simply execute the standard restore command in section (5) for each floppy. Destroyed disks may be skipped. Currently, no support for partially restored disks exists... i.e. the archive has be clean. SHAR_EOF # End of shell archive exit 0 -- Bob Page, U of Lowell CS Dept. page@swan.ulowell.edu ulowell!page Have five nice days.