# backup.icn - book, pp. 147-150. # Revision date 7/18/2001 # # backup.icn - incremental filesystem backups # # Usage: # ./backup [-nlevel] [-ooutput] [dir] # # Save all files that have changed since the last backup of higher # level (a level 0 backup is the highest level and saves all files; # it is the default) # The files are all saved to the directory "output", which is # probably a mounted backup device like a floppy or Zip drive. # # # Suggestions by ptho: There are two stop's so # this could probably be better placed in a called procedure. # A polished production model would have a series of error codes and proably a # report on how many files were copied and a reminder of where they were # copied to. # $include "posix.icn" link options global dbase, exclude, levels global output, last procedure main(args) dbase := "/var/run/backups.db" exclude := ["/mnt", "/tmp", "/dev", "/proc"] # Process arguments opt := options(args, "-n+ -o:") level := integer(\opt["n"]) | 0 output := opt["o"] dir := args[1] | "/" \output | stop("An output directory (option -o) must be specified.\n", "Usage: ./backup [-nlevel] -ooutput [dir]") if level < 0 | level > 9 then stop("Only levels 0..9 can be used.") # Get the time of the previous lower-numbered backup last := get_time(level) # Now look for files newer than "last" traverse(dir) # Write the database and exit. save_time(level) exit(0) end # main() procedure traverse(dir) # Skip excluded directories. if dir == !exclude then return # Read all the files; for any non-special files, copy them # over to the output dir, creating directories as necessary. d := open(dir) | { write(&errout, "Couldn't stat ", dir, " ", sys_errstr(&errno)) return } if dir[-1] ~== "/" then dir ||:= "/" while fname := read(d) do { if fname == ("." | "..") then next s := stat(dir || fname) | { write(&errout, "Couldn't stat ", dir || fname, " ", sys_errstr(&errno)) next } if s.mode[1] == "d" then traverse(dir || fname) else { # Only save plain files. if s.mode[1] == "-" & s.ctime > last then copy_file(dir, fname) } } end procedure copy_file(dir, fname) # First, make sure the directory exists. mkdir_p(output, dir) system("cp " || dir || "/" || fname || " " || destination(output || "/", dir)) end procedure mkdir_p(prefix, dir) local st # The name is supposed to be reminiscent of "mkdir -p". # Start at the first component and keep going down it, copying # mode and owner. # dir ||:= "/" d := "" dir ? while comp := tab(upto('/')) do { tab(many('/')) if d=="" then d := comp else d ||:= "/" || comp if prefix[-1] ~== "/" & d[1] ~=="/" then prefix ||:="/" if not (st := stat(destination(prefix, d))) then { # The directory doesn't exist; create it. d is the # directory being copied over; get its uid and mode. if s := stat(d) then { mkdir(destination(prefix, d), s.mode[2:11]) chown(destination(prefix, d), s.uid, s.gid) } } } return end procedure destination(prefix, d) d ?:= if =(("."|"..")||"/") then tab(0) return prefix || d end procedure get_time(n) # Get the date of earlier backup levels := table() f := open(dbase) while line := read(\f) do line ? { lev := integer(tab(upto(' '))) move(1) date := tab(0) levels[lev] := date } close(\f) every i := integer(!&digits) do if i < n then prev := \levels[i] /prev := 0 # default: the epoch return prev end procedure save_time(n) levels[n] := &now f := open(dbase, "w") | stop("Couldn't open table ", dbase) every i := integer(!&digits) do if i <= n then write(f, i, " ", \levels[i]) else break close(f) end