flexbackup-1.2.1/0040775000076400007640000000000007741574735013216 5ustar edwinhedwinhflexbackup-1.2.1/Makefile0100664000076400007640000000207707711524027014643 0ustar edwinhedwinh# Hey emacs, use -*-Makefile-*- mode ###################################################################### # $Id: Makefile.dist,v 1.8 2003/07/29 17:00:07 edwinh Exp $ # $Name: v1_2_1 $ ###################################################################### PREFIX=/usr # Where the script binary should go BINPATH = $(PREFIX)/bin # Where the manpage should go MANPATH = $(PREFIX)/share/man # Where flexbackup.conf should be stored CONFFILE = /etc/flexbackup.conf # Where perl lives PERLPATH = /usr/bin/perl ###################################################################### all: fb.install install: all install -m 0644 flexbackup.conf $(CONFFILE) install -m 0755 fb.install $(BINPATH)/flexbackup install -m 0644 flexbackup.1 $(MANPATH)/man1/flexbackup.1 install -m 0644 flexbackup.conf.5 $(MANPATH)/man5/flexbackup.conf.5 fb.install: cp flexbackup fb.install cp fb.install fb.tmp; sed -e 's%/etc/flexbackup.conf%$(CONFFILE)%g;' fb.tmp > fb.install cp fb.install fb.tmp; sed -e 's%/usr/bin/perl%$(PERLPATH)%g' fb.tmp > fb.install rm -f fb.tmp clean: rm -f fb.install flexbackup-1.2.1/CHANGES0100664000076400007640000007355207741551561014212 0ustar edwinhedwinh$Id: CHANGES,v 1.159 2003/10/10 15:42:41 edwinh Exp $ $Name: v1_2_1 $ ---------------------------------------------------------------------- Version 1.2.1 (20031010) Changes: - Added "-ignore-errors" flag to allow backups to continue even if commands return non-zero exit status. Use at your own risk. - Added contributed manpages - Add 'rsync' type that acts like 'copy' but using rsync instead of piped cpio's. You need rsync >= 2.5.6 for this to work! - Added lzop compression support - Spinning bar indicator for long-running pkg delta operations if output is a terminal - Recognize .deb files as ar archives - Skip proc/devpts/devfs/tmpfs for traverse_fs=local or all - Add hooks for mbuffer's multivolume support (experimental) Bug fixes/cleanup: - FreeBSD pkgdelta support fixed up - Cleanup better if error detected - Fix quoting in check_remote_progs if your remote shell is csh - Make sure we don't generate conflicting index keys (as long as -newtape is not run in parallel) - Cleanup a bit regarding block devices ---------------------------------------------------------------------- Version 1.2.0 (20030707) Changes: - Merge devel -> stable - Added -pipe option to archive single directory to stdout, or list/extract/compare archive from stdin - No null padding necessary for reads, even with $pad_blocks true - only needed for writes. Should take harmless warnings way from gzip,tar, and others when reading - Added -nodefaults option for debug - Allow compression for filelist type Bugfixes/cleanup: - Fix FreeBSD find w/ prune regex - Truncate tar label to 99 chars - Error if -num flag with backup write mode or using file - Error if -erase with archive read mode or using file - Fix afio volume header print filehandle - Fix if on-disk backups, and "-rmfile all", the index for the keyfile was removed but not the keyfile itself - use $:: rather than $main:: for globals, its shorter... ---------------------------------------------------------------------- Version 1.1.8 (20030621) Changes: - Detect old config file - Config file parsing not so rigid - if values not found, it now uses defaults (w/ message printed so you know it) - Can configure buffer percent full and write sleep time options - Compare works for copy type (just diff -r -q) Bugfixes/cleanup: - Doh! index key creation broken for to-disk backups as a result of 1.1.7 changes. Fixed. - Relative links for $device get chased correctly - $pkgdelta_archive_changed = false didn't work. - README updated; example for filesystem setup & cron. - find -depth w/ -prune isn't supposed to work, no bug. (see single unix spec and various manpages). ---------------------------------------------------------------------- Version 1.1.7 (20030618) Changes: - Added "copy" type that just mirrors the files into a directory tree if archiving to disk - Initial support of "-pkgdelta" with FreeBSD packages - On FreeBSD, use mt to set 1-filemark behavior globally - Globally ignore sockets - Use "find -prune" for subtree pruning. (When this happens, we have to turn off "-depth" switch, as with GNU find 4.1.7 they don't work together. Need to check into findutils more.) Bugfixes/cleanup: - Don't use --same-order for tar. -extract -flist would fail if you had more than one file and they weren't in the expected order - Use --ignore-failed-read for tar - Remote tape drive device didn't create index key correctly - Typo/update star flags setup - Tweak quiet flags ---------------------------------------------------------------------- Version 1.1.6 (20030313) Changes beyond stable 1.0.3 release: - Different way to spec the filesystems. This lets us have arbitrarily named backup "sets". Config file: $set{'tag1'} = "/dir1 /dir2 ..."; $set{'tag2'} = "/dir3 host:/dir4 ..."; Flags: "-dir " backs up one directory tree (old "-fs ") "-set " backs up a named set. Useful new feature. "-set all" acts like old "-fs all". If using tapes & level 0, each set is a different tape (acts like the old numeric index to @filesystems) - Per-filesystem subtree pruning $prune{'/'} = "tmp proc mnt"; $prune{'host:/'} = "disk.?"; - Added packaging system delta capability. If you give "-pkgdelta rpm", after the level/timestamp checks, we further cut down the backup file list by only archiving files that are either (a) not owned by a package, or (b) owned by a package but modified from the distributed version. New config variables to tweak behavior: $pkgdelta_archive_list $pkgdelta_archive_unowned $pkgdelta_archive_changed - Added 'mbuffer' program as alternative to 'buffer' - Added 'filelist' type that doesn't back up anything, but prints/saves the list of files that would have been archived. Good for debugging timestamps, levels, and file exclusion. - Added "-extract -onefile " option; if you are extracting a single file just spec it on the command line, no list file needed - Changed boolean mt_var_blksize into mt_blocksize integer (in bytes). Set to 0 for variable blocksize (old "true" value), comment out or set to "$blksize * 1024" for old "false" value. - If staticfiles or staticlogs true, don't timestamp list/extract/compare log - Added -wday option (helps with once-a-month, on a certain day of the week only cron setups) - test-tape-drive now diffs 3 files, one in the middle is different (will catch people using rewind-on-close device) - "-extract -files" -> "-extract -flist" to hopefully be more clear - db/file rm flags tweaked: Redid rmindex semantics again, see help -rmfile can be specified multiple times -rmindex can be specified multiple times - Added support for lha, shar, and ar archives just to be stupid Cleanup: - Allow dirs/prune paths with spaces in them - to use, enclose all items in the lists in quotes. - When level > 9 and using files, -toc sorts better now - Config file with spaces in path works - Don't check for writeable stampdir for non-backup - Don't check for writeable logdir for non-backup - Don't tie to db for non-backup ---------------------------------------------------------------------- Version 1.0.3 (20030313) Bug fixes: - Only look for mt if we are using tape device - If using files, "-fs all", and and multiple array entries for $filesystems, index was getting hosed up. - newtape/erase when using dirs was deleting keyfile entry from index db - remove bogus OS_ERROR string from failed tie ---------------------------------------------------------------------- Version 1.0.2 (20030219) Changes: - Added '-test-tape-drive' switch that just makes sure blocks, filemarks, and padding work as expected and we can read,write,compare. If people can't get that running, then the problem is somewhere else... - Allow mt command override in the config file: mt{'old'} = 'new'. Useful if there is a new platform-specific combination, you want 'status' vs. 'tell' output in the log, etc... Bug fixes: - If remote tape device, don't check for writable local device - Fix pattern-match so "sudo -u username command" works (Tweak sudo comment in config file and faq as well) Cleanup: - Update mt op used for hardware compression (if anyone uses it) - Cleanup more uses of $_ ---------------------------------------------------------------------- Version 1.0.1 (20030202) Bug fixes: - Quote the directory when doing initial "cd" (logfiles, stamps, filenames too). Also needed to fix a few regexps that matched against the backup dir. - afio + null fix in pre6 didn't work in FreeBSD due to a problem with its printf(1). Switch back to newline-separated for afio types for now. Linux, HPUX, Solaris, AIX printf(1) utilities did do nulls. ---------------------------------------------------------------------- Version 1.0pre6 (20030126) Bug fixes: - Fix problem with afio volume headers and null-terminated file lists. The first file in afio archives was being missed (actually showed up in the label instead of the archive) Cleanup: - "-rmfile" yells if no argument - Move shell/remote/buffer check to its own function after option checking. (So commandline/config errors and these pre-tests are displayed separately) ---------------------------------------------------------------------- Version 1.0pre5 (20030124) Bug fixes: - Fix sh status collector for the front of remote commands - Fix it for commands in ( .. ) subshells also (echo + find for afio volume header input for example) Cleanup: - Don't do status collection work for non-pipelined remote commands ---------------------------------------------------------------------- Version 1.0pre4 (20030121) Bug Fixes: - Try harder to catch pipeline errors in different environments: o zsh has a pipestatus array variable similar to bash, use it o Tweaks to shell detection to detect remote vanilla csh o Add exit-status recording shell trick for local sh - If we get an error, rm index & botched archive file if using files - If we get an error, mark problem in the index if using tapes Cleanup: - Replaced manual line-wrapper with Text::Wrap (other was buggy, missed single-char "words") ---------------------------------------------------------------------- Version 1.0pre3 (20030119) Bug Fixes: - Fix PIPESTATUS trick to not require extra 'seq' utility, just a while loop - Using "-toc" gave uninitialized var warning, obvious problem, fixed. Eradicated some other spots of code that used $_ too. - "-rmindex" yells if you don't give an argument Cleanup: - Cosmetic - fix missing line in output if no remote and buffer=false - Remove duplicated index delete code ---------------------------------------------------------------------- Version 1.0pre2 (20030118) Bug fixes: - atime preserve option to tar/star/afio modifies the ctime of the files. Using ctime checks then says everything is newer, no matter what the level. Make atime preserve a global option defaulting to false; if true turn off ctime checks - When reading from given filename, set device to dir so that we don't pad blocks by accident if default device is tape - Detect shell type for local and any remote hosts Only use bash PIPESTATUS trick if we find bash 2.x ---------------------------------------------------------------------- Version 1.0pre1 (20030118) Changes: - Allow non-root execution along with "sudo". Set path{'command'} = "sudo command" or "sudo -u username command" in the config file. Idea from Alex Aiminoff's patch to 0.9.8 - Added $remoteuser to config for rsh/ssh - Check that 'buffer' can run with set $megs. From John Reynolds, brought forward from 0.9.8. Remote as well. - Check for existence of remote executables before doing remote backups Based on John Reynolds' modifications - A "no blocking at all" option. Set $blksize=0 in the config file. - Added support for "zip" as valid archive compression type - Added support for "zip" as valid log compression type - "-device to override config file (tired of "-d device=/path/to/dir" during testing...) - "-rmfile all"' when using files will rm all files/db entries for that dir Bug Fixes: - $erase_rewind_only wasn't working right if we didn't call &mt('rewind') right before or after &mt('erase') (I think all spots are covered now, but anyway...). Now we might call rewind twice if this is set, but it won't hurt. (from e-smith patches) Cleanup: - Redid the way buffer/read/write strings were constructed to make more sense - Don't set tape blocksize with -toc unless we want current tape (from John) - Interactive restore creates log file (was in e-smith stuff) - Extract/list/compare/restore use date-stamped logfiles (e-smith) - Simplified rmfile() to call existing rmindex() routine - Symlink latest log even if staticlogs = true ---------------------------------------------------------------------- Version 0.9.9.7 (20030115) Changes: (just so we can say it supports *everything* for archive formats) - Initial support for "star" archiver. Like tar but does acl's, has its own fifo buffering mechanism, and more. see: http://www.fokus.gmd.de/research/cc/glone/employees/joerg.schilling/private/star.html - Initial support for "pax" archiver. Really just tar or cpio, but might be good for portability across unices... - Put .zip back in, same way as in old releases (kinda hokey with the tmpfiles, but there is no cleaner way to do it with info-zip utils like I wanted) Bug fixes: - Fixed problem with other releases trying to use find w/ -cnewer. Desired so we get files that change perms, not content. ctime can't be set backward with touch -t (or any mechanism), so we can't back-date a reference file. Now fixed so at least for local filesystems this works. Remote backups still won't catch that type of change. Code changes: -don't use generated timestamp files for local fs's; use the original stampfile -refactor remote fs variable -get_last_date function changes -no stamps at all for level 0 Cleanup: - '-rmindex all' says it will wait 5 seconds - '-incremental|full|differential' same as '-level ' - Fix warning with no args - touch -t the same for everyone. buggy touch in pre-4.0 fileutils is old by now.. - Clean up date routines ---------------------------------------------------------------------- Version 0.9.9.6 (20030112) Changes: - Can now backup to / read from remote tape drives. Set $device = "host:/dev/tape" Uses ssh pipelines like our remote fs backups - not rmt. Old mail idea from Caleb Crome , reworked. Bug fixes: - Some commands still weren't using path overrides (dd,mt) Cleanup: - Refactored some code due to the above. - afio volume header had extra newline - File list routine to nuke duplicated code - Echo list/extract log filename. - Ffind -depth is recommended. - Add .tbz auto file extension. - Fix incorrect tapedevice test in read setup function. (cosmetic result) ---------------------------------------------------------------------- Version 0.9.9.5 (20030111) Changes: - Static filenames option when using disk (same fs, same level overwrite) based on 0.9.8 patch from Michael Matsumura split into $staticlogs / $staticfiles config options Bug fixes: - Old bug: stamp files from the various parts of 'all' were not getting marked as 'old' correctly for removal (we were looking for "all.") - Fixed '-level incremental' with fs=all. Had to refactor some code and not use a global level variable. logs filenames a little different for 'all' in this case. Cleanup: - Make path overrides also search $PATH (say if we just changed the name tar == gtar but still want to do normal path-search) - $nocompress_types -> $afio_nocompress_types since that's the only one that uses it - A few cosmetic fixes: fix double dash-line on last line of logfile make logging routines suable for screen/log/both fix so -n won't nuke old files either ---------------------------------------------------------------------- Version 0.9.9.4 (20030110) Bug fixes: - DOH! we *were* doing levels slightly wrong. use < rather than <= for comparing levels. popped out of investigating incremental and differential requests Now we act like dumplevels should - This also changes what we regard as "old" logs/stamps as a result Changes: - Several requests were in old email for differential/incremental... Just a misunderstanding I think. Check these links: http://docs.sun.com/db/doc/805-7228/6j6q7uf18?a=view http://www.arkeia.com/archives_indexed/2000/10/msg00006.html http://www.sans.org/rr/backup/central.php To sum up, no changes are *needed* if dump behavior mimiced (we had a bug here BTW). We can do what people call "incremental" as long as we increase the backup level each day. We can do what people call "differential" just by keeping the number the same for consecutive backups - Implemented support for -level [full|differential|incremental] as aliased dump levels (for people not as familiar with dump-style levels) full = 0 differential = 1 incremental = + 1 [broken for fs=all at this stage] - Support for > 9 levels (for all types but 'dump') ---------------------------------------------------------------------- Version 0.9.9.3 (20030109) Bug fixes: - BSD find wants "!" not "-not" ---------------------------------------------------------------------- Version 0.9.9.2 (20030107) Bug fixes: - 'Argument "200301072208|0" isn't numeric in numeric comparison' when using tapes... redid toc routine to use two hashes for disk/tape files, sort them differently - Existing tape indexes weren't found to a missed edit during the past few days' cleanup ---------------------------------------------------------------------- Version 0.9.9.1 (20030107) Changes: - use -print0 with find and null-terminated list flags for the archive programs. Now any weird filenames should be valid. This necessitated using find -regex rather than an egrep pipeline. (FreeBSD find now has "-regex" so the old argument is gone). - also took out the sed of the leading "./" since the null-terminated file list messed it up too. Was just for looks anyway. - cpio flags use the short options so more compatible with other cpio's - cleaned out tape-only messages when using files Bug fixes: - Pipeline error status - if something went wrong with a shell pipeline, and it was not the _last_ command, you can't catch it! I never knew this was impossible... bash has a PIPESTATUS array variable for this, so I took care of it for systems where SHELL is bash... tcsh seems to set $? if anything fails, so it's ok too. I don't know if there is a way to fix it generically. - Actually log output. doh! (related to above, actually) - afio compress programs honor the path overrides ---------------------------------------------------------------------- Version 0.9.9 (20030106) Wonder of wonders... I'm actually using this again. The guy who registered flexbackup.sourceforge.net never picked up the files or updates his page so I'm flipping pointers back to sourceforge.net I guess. I'm mainly just using on-disk files now - will try to keep tape operation tested. Changes: - changed the index delete flags worked, it was too confusing overloading the -toc flag (WTF was I smoking 2 years ago?). Now toc only lists stuff, and db-fixing can be done with: flexbackup -rmindex all flexbackup -rmindex flexbackup -rmindex -f - added "-rmfile " option if we are backing up to disk. nukes archive and its related index entry - take the seconds field out of the archive + log names... it was just too long to interact with, and who really does more than 1 backup in a minute? - removed zip archive support. They just don't like to work with stdout/stdin like other unix tools and are a pain to special case. - "-newtape" when using files will rm the files in the dir - support non $PATH or hardcoded locations to commands in config file: $path{'program'} = /path/to/program; Bug Fixes: - doh - can't use find -cnewer & a reference file with touch -d together since the change time is still "now". I remembered the suggestion but the long lapse in devel time made me forget to see if it worked. REMOVED cnewer mistake. - default config uses /var/lib rather than /var/state (FHS spec on this went away long ago) - don't pad blocks when archiving to files on disk - was using numeric sort when listing alphanumeric files - "-num" invalid when using files - newer cpio didn't like sparse in --create mode - errors in command chain weren't detected. now separate some of the steps; remote still hosed up ---------------------------------------------------------------------- Version 0.9.8.3 (20020824) Revive the tree Move support info/web site so this isn't stagnant anymore Check in what I had pending New Features: - works for block devices (like floppies). Turns the indexing off since you can't do multiple files, but otherwise it works fine Bug fixes: - use "&&" rather than ";" in between commands - use "-cnewer -or -newer" rather than "-newer" - set locale so touch works - tar - take off "! -type d" since we have --no recursion - log/backup files now use yyyymmddhhmm.ss times in the filenames. Previously you could get messed up if you ran two backups on the same minute; files would clobber themselves - OpenBSD didn't have "mt rdhpos". Split the BSD flavors up. - add a rewind before erase when deleting old index - "-n" was touching the stamp file, now it won't ---------------------------------------------------------------------- Version 0.9.8 (19991102) New Features: - add -newtape option to initialize index, but no backup - add -num option for reads that will position the tape at the specified file number before reading - added config file option for whether "erase" actually calls "mt erase" or just rewinds - for some drives, mt erase takes hours... (variable is $erase_rewind_only) - tried to add intelligence for various mt flavors on different Unix variants - made block padding a config file option ($pad_blocks). conv=sync was causing problems for Clemmit on OSF1. Bug fixes: - sort the toc output correctly - rewind after printing toc - fix bug where tape indexes always started at 10 after awhile (similar sorting problem with setting $main::nextfile) Cleanup: - web site moved - other minor cleanups to tape key handling - "-n" bypasses index operations as well - fix some messages/formatting ---------------------------------------------------------------------- Version 0.9.7 (19991019) New Features: - table-of-contents support - tells you which backups are on which tapes. uses a simple "key" on the tape plus on-disk database files - added "traverse mountpoints" config option for non-dump types can skip nfs mounts or really do everything - added config option to turn off assumption that level zero "all" should erase the tape Bug fixes: - add "conv=sync" to dd flags so it will pad blocks some tape drivers complain if writes are not padded "buffer" already did this since I used the B flag by default - don't use `date ..`, but POSIX::strftime() instead incremental backups on FreeBSD were busted (no date -r) - using $ in exclude expressions broke remote backups if the user's login shell was csh/tcsh due to it's insane syntax quirks. Worked around it putting a trailing $ outside the second level quotes - works on OSF/1 boxes Cleanup: - remove ftape table of contents now that I have a generic one - autodetect Linux ftape device, eliminates configuration - change time format slightly for consistency - cleaned up handling of reten/erase/rewind options - tape position display under FreeBSD is just "mt rdhpos", under others is "mt tell" ("mt status" cluttered things w/ lots of useless info) ---------------------------------------------------------------------- Version 0.9.6 (19991003) New Features: - buffing is optional - log files for list/extract/compare (flexbackup..log in cwd) - can now have more than one exclude expression defined - add -norewind flag - make symlink to latest log file - use our own timestamps for "dump", with -T config file variable to use /etc/dumpdates - flag in configuration file for mt variable block size - do buffering on archive read if enabled - add "-extract -files " to extract a list of files from an archive (will need this later for interactive restore shell) - minimal Makefile that lets you set config path, bin path, etc. for install - can use "compress" now in addition to gzip/bzip2 - added zip type - var in configuration file for afio compression cache size - flag in configuration file for tar atime preserve - flag in configuration file for tar record number printing - flag in configuration file for sparse file handling - add -compress flag to override config file setting Bug fixes/cleanup: - afio nocompress types bug - have to list them all if we give the -E flag.. (I added mp3 and it ignored the builtin defaults) made generic \$nocompress_types since zip uses it too - find -regex doesn't exist on BSD, redo with egrep to be more generic - GPL clause in program itself not readme - redid remote commands to use only 1-way rsh/ssh archive command building done differently now as a result - redid command echo wrapping - dates back to string format (easier to read), use numeric only on BSD will work with pre-4.0 fileutils that have touch -t bug - tar exclude mess is fixed, using --files-from again as well. problem before was I was not stripping directory names - make better afio control file handling for volume labels - use -B (--read-full-blocks/--read-full-records) for tar - use $verbose to set restore -v for list/extract - log files and archives to disk use same filename pattern now - do obsolete timestamp removal at end of job not beginning - forgot to remove tar_verbose and cpio_verbose from conf file - debug shows real mt command - clean up flag handling some more, use long flags - sleep at end of backup not done if file or debug - if -noerase, move to eod before starting ---------------------------------------------------------------------- Version 0.9.5 (19990927) New features: - buffering for all backup types now requires the "buffer" utility, everything piped through it if remote backup, buffers on both sides - every backup type now has an embedded "title" (level,date,host,etc) dump has had this info. by default for afio we make a tiny control script for tar we use -V for a volume label for cpio we touch a filename wih the info before backing up - config file regular expression for excluding files from backup doesn't work for dump, of course - config option for afio file size compression threshold - config option for afio nocompress file extension list - compression for all backup types - if reading from a file, auto-set compression & archive type whee we can also list & extract rpm files :) Bug fixes/cleanup: - nuke trailing slashes on given dirs - typo! mt defblksize, not mt defsetblk. also, that whole routine was just broken. fixed. - tar was archiving things twice. "find . | tar -c -T -" gives double result. revert back to non-find mode for tar, works fine - remote tar didn't quite work (needed full path for ssh/rsh) - remote broken if using filenames (and they were long) - error if remote host and this host are the same - error if device is dir and its relative - stderr output from new pipeline commands wasn't in log file - fix dirs going on top of each other when mult. dumps. + files - log file removal at end of job; undef logfile after compressing - compress, compr_level, verbose, buffer are generic options - commands are an array; command build cleanup; mt takes multiple commands --------------------------------------------------------------- Version 0.9.4 (19990924) New features: - bzip2 option for afio and tar archives, as well as log files - configurable compression level for afio - multiple tapes for an "all" level 0. This just lets you split up backups onto different tapes, not end-of-media type of multivolume problems (that's on the todo list) - support for remote dump over ssh - use dd over an ssh pipe - support ssh for cpio remote backups - use dd over an ssh pipe - add -reten switch to retension before backup or any other operation - print "date of this level" sort of like dump does Bug fixes/cleanup: - test if mt setblksize or mt defblksize is the right command to use - dump -a (autosize) works now. Found my problem was a known bug. >>> NOTE for Linux w/dump, you MUST install dump-0.4b4-10 (redhat lorax) or newer, or equivalent patches if you want dump -a to work - afio doublebuffer only if local backup - dump length bug for "-fs all" - get last timestamp was off-by-one - output of archive commands didn't go to log file - with label "host:path-dir"; didn't work w/ remote + files labels are now "host-path-dir" - sanity checks in the setup, flag variables - check for programs in your path --------------------------------------------------------------- Version 0.9.3 (for testing only) New features: - can now archive to files instead of devices just set $device to a directory for backup compare/extract/list/restore can now take a filename argument - add -n option that doesn't run mt or the actual backup - timestamp changes: use "touch -t" not "touch -d" so FreeBSD works all dates now in YYYYMMDDhhmm.SS format use timestamps of the epoch for level 0 (simplifies code) - add "-d var=val" for overriding config values - tar mode uses "find" just like afio & cpio - afio doublebuffering option Bug fixes/cleanup: - FreeBSD: if $dump_length set to 0 use dump -a - FreeBSD: fix for mt setblk - error if level > 0 and no level 0 stamp exists - fix label string for remote paths --------------------------------------------------------------- Version 0.9.2 (19990920) - initial release to the public - gzip log files option - fix afio buffer to be in units of k in cfg file Version 0.9.1 (1999019) - incrementals w/ afio cpio tar done (remote fs too!) - timestamp only after sucessful backup, but use the pre-backup time - noerase option Version 0.8 - redone with "use strict" - incremental for non-dump types almost done - automated pacakging - clean some duplicated code - rpm build, lsm file - previously just a local hack I had done for myself Local Variables: mode: flyspell end: flexbackup-1.2.1/COPYING0100664000076400007640000004311006770462121014227 0ustar edwinhedwinh GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. flexbackup-1.2.1/CREDITS0100664000076400007640000000253107734054354014224 0ustar edwinhedwinh Mainly written by Edwin Huffstutler Thanks to Paul Holcomb for trying to keep the project alive while I was ignoring it... Other people who have helped me debug/fix things, send patches, or report bugs: John Reynolds Paul Holcomb Max Kalika Alex Aminoff Michael Matsumura Caleb Crome Perry Gilfillan Ben Chapman Jochen Erwied George A. Theall E-smith / Mitel folks - Charlie Brady et al. Andrew McRory Markus Schaub Mike Packard Douglas Bollinger raphael deimel Nicholas Paufler Simon Matter Many people in the long development time lapse, I'm sorry if your suggestions/fixes got dropped... older releases: Clemmitt Sigler George Young sethalx Rick Gaudette Rafal Maszkowski Michael Bartosh flexbackup-1.2.1/INSTALL0100664000076400007640000000072507701071417014231 0ustar edwinhedwinh Installation instructions 1. If you don't like the default paths for the config file, the bin dir for programs, or where perl lives, edit the Makefile, and set them. Defaults are CONFFILE=/etc/flexbackup.conf, BINPATH=/usr/bin, PERLPATH=/usr/bin/perl 2. Run "make", then "make install" 3. Edit /etc/flexbackup.conf (or wherever you put it) as desired. See the comments for details. $Id: INSTALL,v 1.3 2003/07/03 18:16:47 edwinh Exp $ $Name: v1_2_1 $ flexbackup-1.2.1/contrib/0040775000076400007640000000000007741574734014655 5ustar edwinhedwinhflexbackup-1.2.1/contrib/flexbackup.cgi0100775000076400007640000003236707614615154017467 0ustar edwinhedwinh#!/usr/bin/perl -w # # flexbackup.cgi # cgi frontend for the flexbackup tape # archival program # Copyright 2002, "Linux Systems Engineers / PC Doctor SE, Inc" # Released under GPL 2.0 By Andrew McRory # Written by Brad Andrews # version 0.9a # Wed Oct 9 2002 ###################################################################### use strict; use CGI; use CGI::Carp "fatalsToBrowser"; use POSIX; use NDBM_File; my( $CGI ) = new CGI; my( $home_directory ) = "/home/flexbackupweb"; my( $base ); my( $script ) = $CGI -> script_name; my( $directory ); my( $tape_id ); my( %dir_hash ); my( $passed_file ); my( $tape_device ) = "/dev/st0"; my( $non_rw_tape_device ) = "/dev/nst0"; my( $flexbackup_conf ) = "/etc/flexbackup.conf"; if ( $ARGV[ 0 ] eq 'blind' ) { # this is so you can run a backup from cron with # the advanced database features, just run # 'flexbackup.cgi blind' &run_backup; exit 0; } # This is the dispatch table to determine program state my( %dispatch_table ) = ( 'file' => \&extract_file, 'tape_id' => \&pre_db_crawler, 'Backup' => \&run_backup, 'Change Schedule' => \&change_schedule, 'edit_config_page' => \&edit_config_page, 'filesystems' => \&change_conf, 'add_filesystem' => \&change_conf, 'Change Options' => \&change_conf, ); my( $param_test ) = 0; foreach my $keys ( keys( %dispatch_table ) ) { if ( $CGI -> param( "$keys" ) ) { $param_test++; $dispatch_table{ "$keys" } -> ( $CGI -> param( "$keys" ) ); } } if ( $param_test < 1) { &browse_archives; } sub pre_db_crawler { $tape_id = $_[ 0 ]; &db_crawler; } sub browse_archives { # Main screen and backup initiator &html_header; my( %temp_archive ); opendir( DBDIR , "/var/lib/flexbackup" ); my( @archives_list ) = readdir( DBDIR ); print qq{

Select a backup to browse

}; foreach ( @archives_list ) { if ( m/^([\d\.]+\.master)\.db$/ ) { tie( %temp_archive, "NDBM_File", "/var/lib/flexbackup/$1", O_RDONLY , 0600 ) || die "can't tie $1, $!"; print qq{
}; foreach ( sort( keys( %temp_archive ) ) ) { print qq{Friendly name: } , $temp_archive{ $_ } , qq{
\n}; } print qq{
}; untie( %temp_archive ); } } print qq{

Or...

Enter a name to backup to or leave blank for date format.
}, $CGI -> start_form( -action => $script ), $CGI -> textfield( -name => "friendly_name" ), $CGI -> submit( -name => "Backup" ), $CGI -> endform, qq{

Or...

click here to edit configuration for backups}; } sub db_crawler { # open the db file and read in directories/files from it &html_header; my( %hash ); tie( %hash , "NDBM_File" , $CGI -> param( 'tape_id' ) , O_RDONLY , 0600 ) || die "can't tie, $!"; my( $i , $full_rootlink ); my( %dir_hash, %file_hash ); if ( $CGI -> param( 'directory' ) ) { $passed_file = $CGI -> param( 'directory' ); } else { $passed_file = $hash{ "" }; } my( @rootlink ) = split( m/\// , $passed_file ); print qq{

Files:

}; #$passed_file =~ s/(.*)\/$/$1/; foreach ( sort( keys( %hash ) ) ) { if ( $hash{ $_ } =~ m/^$passed_file([^\/]+\/)$/ ) { $dir_hash{ $hash{ $_ } } = $1; } #elsif ( $hash{ $_ } =~ m/^$passed_file$/ ) { elsif ( $hash{ $_ } =~ m/^$passed_file$/ ) { unless ( $_ eq "" ) { my( $tempfile ) = $_; $tempfile =~ s/.*\/(.*)/$1/; $file_hash{ $_ } = $tempfile; } } } foreach( sort( keys( %file_hash ) ) ) { print qq{} , $file_hash{ $_ } , qq{
\n}; } print qq{

Click here to download directory as a .tar file

Subdirectories of } , ( $CGI -> param( 'directory' ) || $passed_file ) , qq{:

}; foreach ( sort( keys( %dir_hash ) ) ) { print "[$dir_hash{ $_ }]
\n"; } print qq{

Current Directory:

}; for ( $i = 0 ; $i < ( scalar( @rootlink ) - 1 ) ; $i++ ) { $full_rootlink .= "/$rootlink[ $i ]/"; $full_rootlink =~ s/\/\/*/\//; print qq{} , $rootlink[ $i ] , qq{/}; } print pop( @rootlink ), qq{
\nReturn to archive selector
}; } sub run_backup { # run the actual backup (calling flexbackup) &html_header; print qq{Your backup has begun. It may take up to several hourse to complete, at which time access will be available.
}; my( $start_read , $base_directory , $tape_key , $file_num ); my( %hash_file , %master_file ); my( $friendly_name ) = $CGI -> param( 'friendly_name' ) || scalar( localtime() ); $ENV{'PATH'} = "/usr/bin:/bin"; $ENV{'BASH_ENV'} = undef; $< = $>; my( @old_key ) = `flexbackup -toc`; my( $old_key ) = ''; foreach ( @old_key ) { if ( $_ =~ m/(\d{12}\.\d\d)/ ) { $old_key = $1; } } if ( $old_key ne '' ) { print "$old_key
"; opendir( KILL_DIRECTORY , "/var/lib/flexbackup" ); my( @kill_files ) = readdir( KILL_DIRECTORY ); close( KILL_DIRECTORY ); foreach ( @kill_files ) { m/(.*)/; $_ = $1; print "$_
"; unless ( index( $_ , $old_key ) == -1 ) { unlink( "/var/lib/flexbackup/$_" ) || die "can't unlink $_"; } } `mt -f $tape_device erase`; `flexbackup -rmindex $old_key`; } open( FILE , "flexbackup -fs all|" ) || die "Can't open flexbackup, $!"; while ( ) { if ( m/File number (\d)+, index key ([\d\.]+)/ ){ $tape_key = $2; $file_num = $1; tie( %master_file, "NDBM_File", "/var/lib/flexbackup/$tape_key.master", O_RDWR|O_CREAT, 0660 ) || die "Can't tie, $!"; print "backing up $tape_key.$file_num"; $master_file{ "/var/lib/flexbackup/$tape_key.$file_num" } = "$friendly_name"; untie( %master_file ); tie( %hash_file, "NDBM_File", "/var/lib/flexbackup/$tape_key.$file_num", O_RDWR|O_CREAT, 0660 ) || die "Can't tie, $!"; } if ( m/Backup of\: (.*)\s*$/ ) { $base_directory = $1; } if ( m/([\w\.].*) \-\- / ) { my( $full_name ) = "$base_directory/$1"; my( $file_name ) = $1; $full_name =~ m/(.*\/)(.*)/; my( $directory_name ) = $1; #$hash_file{ $full_name } = $directory_name; print "$file_name stored in $directory_name
\n"; $hash_file{ $file_name } = $directory_name; } } close( FILE ); $hash_file{ "" } = "$base_directory/"; untie( %hash_file ); exit( 0 ); } sub extract_file { # extract file from tape and send (single file restore) $ENV{'PATH'} = "/usr/bin:/bin"; $ENV{'BASH_ENV'} = undef; $< = $>; $_[ 0 ] =~ m/(.*)/; chdir( "$home_directory" ); `mt -t $tape_device rewind`; `mt -t $non_rw_tape_device fsf 1`; `buffer -m 3m -s 64k -u 100 -t -p 75 -B -i $tape_device | afio -i -y $1 -z -x -D /usr/bin/flexbackup -P gzip -Q -d -Q -q -Z -v -b 64k -`; print $CGI -> header( $CGI -> meta( { -http_equiv => 'Content-Type', -content => 'application/x-Binary' } ) ); open( SEND_FILE , "$home_directory/$1" ); while ( ) { print; } exit 0; } sub html_header { print $CGI -> header, $CGI -> start_html,"HTML interface to flexbackup

\n"; } sub edit_config_page { # mess with the flexbackup config file in etc &html_header; my( $type , $compress , $compr_level , $blksize , $fs_ref ) = &read_flexbackup_conf; my( @filesystems ) = @{ $fs_ref }; my( $hour , $m_tens , $m_ones , $day , $half_day ) = &read_cron; print $CGI -> startform( -action => $script ), qq{
Scheduled backup time

}, $CGI -> popup_menu( -name => 'schedule_hour', -values => [1..12], -default => $hour ), qq{:}, $CGI -> popup_menu( -name => 'schedule_minutes_tens', -values => [0..5], -default => $m_tens ), $CGI -> popup_menu( -name => 'schedule_minutes_ones', -values => [0..9], -default => $m_ones ), $CGI -> popup_menu( -name => 'schedule_halfday', -values => [ "am" , "pm" ], -default => $half_day ), qq{

every
}, $CGI -> popup_menu( -name => 'schedule_day_of_week', -values => [ "Sunday" , "Monday" , "Tuesday", "Wednesday" , "Thursday" , "Friday", "Saturday" , "Weekday" , "Day" ], -default => $day ), qq{
}, $CGI -> submit( -name => 'Change Schedule' ), qq{

}, qq{}, qq{
Filesystems to backup
\n}, $CGI -> scrolling_list( -name => 'filesystems', -values => @filesystems, -size=> 5 ), $CGI -> textfield( -name => 'add_filesystem' ), qq{
}, $CGI -> submit( -name => 'Remove Filesystem' ), $CGI -> submit( -name => 'Add Filesystem' ), qq{

}, qq{WARNING: changing these options is not recommended}, qq{}, qq{}, qq{}, qq{}, qq{}, qq{
Backup type
\n}, $CGI -> popup_menu( -name =>'type', -values => [ "afio" , "dump" , "tar", "cpio" , "zip" ], -default => $type ), qq{
Compress type
\n}, $CGI -> popup_menu( -name => 'compress', -values => [ "false" , "gzip" , "bzip2", "compress" , "hardware" ], -default => $compress ), qq{
Block size
\n}, $CGI -> textfield( -name => 'blksize', -default => $blksize ), qq{
Compress level
\n}, $CGI -> popup_menu( -name => 'compress', -values => [ 1..9 ], -default => $compr_level ), qq{
}, $CGI -> submit( -name => 'Change Options' ), qq{
}, $CGI -> endform; } sub change_schedule { # edit flexbackup cron jobs my( $hour ) = $CGI -> param( 'schedule_hour' ); my( $minutes ) = $CGI -> param( 'schedule_minutes_tens' ) . $CGI -> param( 'schedule_minutes_ones' ); my( $ampm ) = $CGI -> param( 'schedule_halfday' ), my( $day )= $CGI -> param( 'schedule_day_of_week' ); if ( $day eq 'Day' ) { $day = '*' }; if ( $day eq 'Weekday' ) { $day = '1-5' }; $hour += 12; if ( $ampm eq 'am' ) { $hour %= 12; } my( $cron_line ) = "$minutes $hour * * $day /var/www/cgi-bin/flexbackup.cgi ?Backup=Backup\n"; open( CRON , "/var/spool/cron/root" ) || die "Can't open cron, $!"; my( $found ) = 0; my( @cron_file ); while ( ) { unless ( m/^\#/ ) { if ( m/flexbackup/ ) { $_ = $cron_line; $found++; } } push( @cron_file , $_ ); } if ( $found == 0 ) { push( @cron_file , $cron_line ); } close( CRON ); open( CRON , ">/var/spool/cron/root" ) || die "Can't open cron, $!"; foreach ( @cron_file ) { print CRON $_; } close( CRON ); &edit_config_page; } sub read_flexbackup_conf { # read in config file for flexbackup my( $rtype , @rfilesystems , $rcompress , $rcompr_level , $rblksize ); open( CONF_FILE , "$flexbackup_conf" ) || die "Can't open $flexbackup_conf, $!"; while ( ) { if ( $_ =~ m/^[^\#]*\$type\s*\=\s*[\'\"](.*)[\'\"]/ ) { $rtype = $1; } elsif ( $_ =~ m/^[^\#]*\$filesystems\[\s*(\d+)\s*\]\s*\=\s*[\'\"](.*)[\'\"]/ ) { $rfilesystems[ $1 ] = $2; } elsif ( $_ =~ m/^[^\#]*\$compress\s*\=\s*[\'\"](.*)[\'\"]/ ) { $rcompress = $1; } elsif ( $_ =~ m/^[^\#]*\$compr_level\s*\=\s*[\'\"](.*)[\'\"]/ ) { $rcompr_level = $1; } elsif ( $_ =~ m/^[^\#]*\$blksize\s*\=\s*[\'\"](.*)[\'\"]/ ) { $rblksize = $1; } } close( CONF_FILE ); return( $rtype , $rcompress , $rcompr_level , $rblksize , \@rfilesystems ); } sub read_cron { # read in cron to determine flexbackup's current schedule my( $fb_line , @fb_components , @return_vals ); open( CRON , "/var/spool/cron/root" ); while ( ) { unless ( m/^#/ ) { if ( index( $_ , 'flexbackup' ) != -1 ) { $fb_line = $_; } } } @fb_components = split( m/\s+/ , $fb_line , 6 ); $return_vals[ 0 ] = $fb_components[ 1 ] % 12; $return_vals[ 1 ] = int( $fb_components[ 0 ] / 10 ); $return_vals[ 2 ] = $fb_components[ 0 ] % 10; if ( $fb_components[ 4 ] eq '*' ) { $return_vals[ 3 ] = 'Day'; } elsif ( $fb_components[ 4 ] eq '1-5' ) { $return_vals[ 3 ] = 'Weekday'; } else { $return_vals[ 3 ] = $fb_components[ 4 ]; } if ( $fb_components[ 0 ] > 11 ) { $return_vals[ 4 ] = 'pm'; } else { $return_vals[ 4 ] = 'am'; } return( @return_vals ); } sub change_conf { if ( $CGI -> param( 'add_filesystem' ) ) { add_fs( $_[ 0 ] ); } elsif ( $CGI -> param( 'filesystems' ) ) { remove_fs( $_[ 0 ] ); } } sub add_fs { # take on new directory for backup open( FB_CONF , "$flexbackup_conf" ); my( $started , $fs_counter , @filesystems_list , @conf_file , $i ); while ( ) { push( @conf_file , $_ ); } for ( $i = 0 ; $i < scalar( @conf_file ) ; $i++ ) { if ( $conf_file[ $i ] =~ m/^[^\#]*\$filesystems\[\s*(\d+)\s*\]\s*\=\s*[\'\"](.*)[\'\"]/ ) { $rfilesystems[ $1 ] = $2; } } } flexbackup-1.2.1/contrib/flexbackup.cgi.README0100664000076400007640000000054607614615101020402 0ustar edwinhedwinh [ Sent in by Andrew McRory meant as a web-frontend for hosting/ISP/appliance/firewall use ] flexbackup.cgi is in a "mostly finished" state It will run backups manually, and will schedule backups in cron but I wouldn't reccommend using it to change flexbackup's configuration. I don't think that functionality is fully functional. flexbackup-1.2.1/flexbackup.spec0100664000076400007640000000577707741574735016233 0ustar edwinhedwinh# Hey emacs, use -*-rpm-spec-*- mode... %define version 1.2.1 Summary: Flexible backup script Name: flexbackup Version: %{version} Release: 1 Epoch: 0 License: GPL Group: Applications/Archiving URL: http://www.flexbackup.org Source0: flexbackup-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot BuildArch: noarch Packager: Edwin Huffstutler Requires: gzip Requires: fileutils Requires: findutils #--------------------------------------------------------------------- %description A flexible backup tool Features: o Easy to configure o Uses dump, afio, GNU tar, cpio, star, pax, or zip archivers o Full and numbered levels of incremental backup (acts like "dump") o Compression and buffering options for all backup types o Does remote filesystems (over rsh/ssh; no special service) o Can backup only files not owned by rpm, or changed from rpm version o Writes to tapes, on-disk archive files, or on-disk directory trees o Keeps a table of contents so you know archives are on each tape o Nice log files #--------------------------------------------------------------------- %prep %setup -q #--------------------------------------------------------------------- %build make all PREFIX=/usr CONFFILE=/etc/flexbackup.conf #--------------------------------------------------------------------- %install rm -rf %{buildroot} mkdir -p $RPM_BUILD_ROOT/etc mkdir -p $RPM_BUILD_ROOT%{_bindir} mkdir -p $RPM_BUILD_ROOT%{_mandir}/man1 mkdir -p $RPM_BUILD_ROOT%{_mandir}/man5 mkdir -p $RPM_BUILD_ROOT/var/lib/flexbackup mkdir -p $RPM_BUILD_ROOT/var/log/flexbackup make install PREFIX=$RPM_BUILD_ROOT/usr CONFFILE=$RPM_BUILD_ROOT/etc/flexbackup.conf #--------------------------------------------------------------------- %clean rm -rf %{buildroot} #--------------------------------------------------------------------- %files %defattr(-,root,root,-) %config(noreplace) /etc/flexbackup.conf %{_bindir}/* %{_mandir}/*/* %dir /var/lib/flexbackup %dir /var/log/flexbackup %doc CHANGES COPYING TODO README CREDITS INSTALL faq.html #--------------------------------------------------------------------- %changelog * Tue Sep 23 2003 Edwin Huffstutler - cleanup spec a bit * Tue Jul 29 2003 Edwin Huffstutler - add manpages, tweak a bit * Thu Jul 3 2003 Edwin Huffstutler - update description * Tue Feb 18 2003 Edwin Huffstutler - config file is noreplace * Wed Jan 15 2003 Edwin Huffstutler - defattr in right spot * Sun Jan 12 2003 Edwin Huffstutler - updated * Sat Sep 25 1999 Edwin Huffstutler - add more requires, update description, email address. - really goes in /usr/bin since it needs perl anyway -- if you only have your root fs, run restore or tar by hand :) * Sat Sep 18 1999 Edwin Huffstutler - initial rpm package flexbackup-1.2.1/README0100664000076400007640000002133007737566240014067 0ustar edwinhedwinh _____ _ _ _ | ___| | _____ _| |__ __ _ ___| | ___ _ _ __ | |_ | |/ _ \ \/ / '_ \ / _` |/ __| |/ / | | | '_ \ | _| | | __/> <| |_) | (_| | (__| <| |_| | |_) | |_| |_|\___/_/\_\_.__/ \__,_|\___|_|\_\\__,_| .__/ |_| http://www.flexbackup.org ---------------------------------------------------------------------- A flexible backup tool Features: o Easy to configure o Uses dump, afio, GNU tar, cpio, star, pax, or zip archivers o Full and numbered levels of incremental backup (acts like "dump") o Compression and buffering options for all backup types o Does remote filesystems (over rsh/ssh; no special service) o Package delta mode can backup files not owned by rpm, or owned+changed files. o Writes to tapes, on-disk archive files, or on-disk directory trees o Keeps a table of contents so you know what archives are on each tape o Nice log files Software needed: o perl o fileutils, findutils (a "find" capable of using the -regex switch) o one or more of dump/restore, afio, GNU tar, star, pax, cpio, zip o mt (if using tapes) o gzip, bzip2, lzop, or compress (optional) o buffer (optional) [needs patches or recompile for 2+GB file support] See the INSTALL file for installation instructions. I looked at many other packages and didn't really find anything that worked the way I wanted it to. For my needs I felt the existing packages were any combination of: o Too complex to setup/use o Lacked documentation o Nonstandard archiving formats o Overkill for a small network of 2 or 3 machines o Too simplistic, or hard-coded assumptions o No scripts helped drive "dump/restore" o Didn't have features I wanted I also wanted to take the standard backup tools and make a generic interface to them. ---------------------------------------------------------------------- USAGE EXAMPLES --> Please see "flexbackup -help" and the config file comments, or the flexbackup(1) and flexbackup.conf(5) manpages. There are many useful options that aren't appropriate to describe here. To do a full backup of /home: flexbackup -dir /home For nightly incremental backups, run something like this via cron: flexbackup -set all -level 9 To do a full backup of /usr from machine "otherhost", forcing cpio type: flexbackup -dir otherhost:/usr -type cpio To do an level 5 incremental backup of a named backup set: flexbackup -set backupset2 -level 5 Extract all files from file number 1 on the tape into current directory: flexbackup -extract -num 1 Compare afio archive at current tape position with the current directory: flexbackup -compare -type afio Extract files listed in "restorelist" from an on-disk file: flexbackup -extract home.0.19990930.afio -flist restorelist List what backups are on this tape: flexbackup -toc ---------------------------------------------------------------------- NOTES - GENERAL Remote: rsh or ssh is used for remote backups. Backups of remote machines are to be run from the machine with the tape drive or archive disk space. Archiving to files - just set $device to a directory. Backups will be called .... If you use type "copy" or "rsync", instead of an archive file you'll get a directory tree with the files mirrored in it. Log files are of the format .. Lower-level backups will wipe out all higher-level log files in the log directory for that filesystem. A symlink is made to the "latest" log for a certain filesystem. Extracting only lists of files: dump/restore and tar will recurse into subdirectories if they are in the list. afio and cpio will not (you must list all the files you want to extract them in that case) When comparing "dump" archives, and if you only backed up a subdirectory of a filesystem, you will get errors for the "other" parts of the filesystem. Similarly, if you extract a dump archive, remember that it keeps the subdirectory path, the "top" of the archive is not necessarily the directory you told it to dump, but rather the mount point of the filesystem ---------------------------------------------------------------------- NOTES - WHEN USING TAPES The first file on a tape is the "index key" (see "flexbackup -toc"). Therefore, use the -num switch to read specific archives. e.g, to read the first backup archive on a tape: "flexbackup -list -num 1". Otherwise you can position it yourself with "mt fsf 1 ; flexbackup -list ... " Level 0 backups of "all" assume a new tape - will trigger tape retension and erasure. Other backups move tape to current end of data and start the backup there. Backups rewind the tape when done. Other operations leave it where it is. If you split the "all" level 0 backup into sets, tape numbers start at 0. If you set device to "host:/dev/tapedevice", you can use remote tape drives (via rsh/ssh) I suggest using "afio" for tapes, especially if you want compression. It compresses one file at a time, so an error will not corrupt your entire archive like with other types. If you are using on-disk backups, the archive type doesn't matter as much. ---------------------------------------------------------------------- SAMPLE FILESYSTEM SETUP Here's a snippet of a config file that I'm using, so you can see how the filesystem "sets" and pruning options work. # Configure backup "sets". # Not needed if you use "-dir

" to backup one tree at a time. # Each set is a simple space-separated list of filesystems # Remote filesystems should denoted as 'host:dir' # You can use anything (other than 'all') as set names # # Example: # $set{'set1'} = "/home /usr"; # $set{'set2'} = "/dir3 machine2:/dir4 machine3:/dir5"; # # "-set all" will back up all defined sets. If you are doing a full backup # using tapes, each "set" will go onto a different tape and you will be # prompted for tape change in between. # $set{'homes'} = "/home/user1 /home/user2 /home/user3"; $set{'alletc'} = "/etc hercules:/etc thor:/etc gazelle:/etc"; $set{'other'} = "/var/named /net/local /net/www"; $set{'os'} = "/ hercules:/ thor:/ gazelle:/"; $set{'laptop'} = "laptop:/etc laptop:/home/edwinh"; # Subtree pruning # A space-separated list of directories to prune from each backup. # Key is a filesystem or host:dir spec as outlined above # regular expressions allowed (not shell-type wildcards!) $prune{'/'} = "tmp proc var/spool/news var/spool/squid disk. mnt.? misc net"; $prune{'hercules:/'} = "tmp proc disk. mnt.? misc net"; $prune{'thor:/'} = "tmp proc disk. mnt.? misc net"; $prune{'gazelle:/'} = "tmp proc disk. mnt.? misc net"; $prune{'/net/local'} = "games"; ---------------------------------------------------------------------- SAMPLE CRONTAB This goes with the above. Shows how the sets are arranged and how the -wday flag could be used. (You will not want the "root" field for a normal user crontab... this is in /etc/crontab type format.) # Home directories at 3:31am # Daily incremental backups weekdays (catch day-to-day changes) # Weekly differential backups Saturday (catch all changes since full) # Full backup - once a month on Sunday only 31 3 * * 1-5 root flexbackup -set homes -incremental 31 3 * * 6 root flexbackup -set homes -differential 31 3 1-7 * * root flexbackup -wday 7 -set homes -full -type afio # All /etc dirs at 3:01 # Daily incremental backups weekdays (catch day-to-day changes) # Weekly differential backups Saturday (catch all changes since full) # Full backup - once a month on Sunday only 1 3 * * 1-5 root flexbackup -set alletc -incremental 1 3 * * 6 root flexbackup -set alletc -differential 1 3 1-7 * * root flexbackup -wday 7 -set alletc -full -type afio # Other filesystems at 2:31am # Daily incremental backups weekdays (catch day-to-day changes) # Weekly differential backups Saturday (catch all changes since full) # Full backup - once a month on Sunday only 31 2 * * 1-5 root flexbackup -set other -incremental 31 2 * * 6 root flexbackup -set other -differential 31 2 1-7 * * root flexbackup -wday 7 -set other -full -type afio # Unowned/changed package stuff on each machine at 1:01am # Only once a month 1 1 1-7 * * root flexbackup -wday 7 -set os -pkgdelta rpm -full ---------------------------------------------------------------------- MISCELLANEOUS I originally did this to to help automate my life. See the TODO list for what we might be planning to do next. Send problems/suggestions to: flexbackup-help@lists.sourceforge.net Please check the FAQ! See http://www.flexbackup.org for contact information. Thanks, Edwin Huffstutler Local Variables: mode: flyspell end: flexbackup-1.2.1/TODO0100664000076400007640000001342707741443244013700 0ustar edwinhedwinh$Id: TODO,v 1.171 2003/10/10 05:41:56 edwinh Exp $ $Name: v1_2_1 $ Flexbackup to-do list Some of these might not happen Cleanup/fix: - Consolidate use_file, use_pipe, etc to single string var. - Fix remote tape drive + mt uname conditionals are local - Clean up more $main:: vars, split into types or refactor code - When level > 9 and rm old logs/stamps, order is printed wrong (cosmetic nit) - If comp_log is changed, we don't nuke old logs that normally would be overwritten with ourselves if comp_log stayed the same. (cosmetic nit) - Fix afio w/null (FreeBSD printf can't spit out null char) - Multiple backups launched within the same second clobber each other's logs/filenames - rpm-delta mode - prelink messages clutter stderr - Whole class of cross-platform command switch stuff: - Use dump combined flags (for older dumps that want abc not -a -b -c) - Not all finds have the -fstype flag (from $traverse_fs) - Use lowest common denominator flags? Sort of what's tried now w/ cpio (This is why pax exists...) Misc: - Do .debs? - Try against torture-test list: http://ftp.berlios.de/pub/star/testscripts/zwicky/testdump.doc.html - Update mt manpages collection I keep in cvs - Pondering moving everything to savannah. Sourceforge is annoying me. mail list archives hardly work, and I hate the file release system thing being totally un-automatable. Features to add: - Remote directores for to-disk backups - Remote device via Net::FTP ? - Handle multi-volume backups (original idea w/ 'multibuf' never panned out) Found "multivol" program. Ack. still not workable. need edits to multivol... mbuffer can do this? Bah. Won't work over remote, needs terminal attached. Testing hooks in now for local. - Make package delta work with debian pkgs or others - A shorter summary status that's better than doing egrep "(error|of set|Backup of|written)" of logfile. Needs to handle all configurations.... - CD-ROM burning (or just .iso type?) for full backups? any level? just act like to-file archives, and take those files & write to CD? "iso" type with a dirtree copy? wasted space this way. be able to split archives into 650MB chunks? lots of questions - If we detect an error and are using tapes, do something intelligent to reposition tape, add another filemark, etc to get tape to known state and avoid the maybe-error in the index & what 'nextfile' is on the tape - Log tape sizes (output of buffer or dd or mt tell can be used?) - Use AppConfig module or something for config file, so it doesn't have to be in perl syntax (I don't mind perl syntax at all, not sure how joe user feels) - smbtar? doable - use fs spec of something like user:pass@host/share:dir smbtar has "files newer than" option, test don't want password echoed no exclude from find top level of shares only? - Make -extract -files for afio/cpio/zip recurse dirs dump/tar already to this. Actually just check that all act the same. - Interactive restore-like shell for types besides dump see notes below - Destdir option for compare / extract - Umask spec for on-disk archives/logs/stamps/index. check tmpfiles too. - Spiff up the contributed CGI program? - Autoloader/mtx support? Need somonewho has one to do this - Do we want to count on perl (or flexbackup for that matter) being installed on remote machines? We could do some built-in replacements for the 'find' for instance and make things more standardized. although keeping it simply built out of standard commandline utils is a big plus as well... - Make per-host path overrides possible - Make -set or -dir able to be given multiple times on the commandline (then what do we call the log file? - Stor lists of files backed up in a on-disk db (to help find what to restore from?). Actually now you can just 'zgrep filename /var/log/flexbackup/*' and that works pretty well. - Extract options to not overwrite existing files - Store the index db on the tapes? But we don't know the size ahead of time unless hard-limited resperved size. Updating db at the beginning of the tape will add tape-travel overhead for every backup. rewind->write loses the rest of the contents past the first file? - gnuplot graphs of backup size/duration vs time (parse logfiles) - Encryption with gnupg? - Make file-based backups able to use split for smaller archives? - Along the same lines of "-toc all" there should be facilities in flexbackup to query when the last level N backup of a filesystem was done and what tape it was on. -showindex /u -level 0 would print you out when the last level 0 backup was done and what the key index was. ------------------------------------------------------------- Notes for cloning restore shell for afio/tar/cpio types) Use perl readline module (Maybe a seperate program or module that runs off a list fed to it) 1) do a listing 2) parse & put into data structures 3) mt bsf 1 4) then give a shell w/ all commands from regular dump 5) mark things for extraction 6) reopen archive & extract that list Help from restore to jog my brain... ls [arg] - list directory cd arg - change directory pwd - print current directory add [arg] - add `arg' to list of files to be extracted delete [arg] - delete `arg' from list of files to be extracted extract - extract requested files setmodes - set modes of requested directories quit - immediately exit program what - list dump header information verbose - toggle verbose flag (useful with ``ls'') help or `?' - print this list If no `arg' is supplied, the current directory is used Local Variables: mode: flyspell end: flexbackup-1.2.1/flexbackup0100775000076400007640000045644707741537071015301 0ustar edwinhedwinh#!/usr/bin/perl -w ###################################################################### # # Edwin Huffstutler, # $Id: flexbackup,v 1.185 2003/10/10 14:12:09 edwinh Exp $ # $Name: v1_2_1 $ # # >>>> Also see the config file, README, manpages, & FAQ <<<< # # USAGE: # flexbackup -help : this message # # BACKUP: # flexbackup -dir : backup directory tree, level 0 # flexbackup -set : backup set "tag" (def. in config file), level 0 # flexbackup -set all : backup all sets, level 0 # flexbackup [...] -level : backup level, can be integer or # full/differential/incremental # flexbackup [...] -pkgdelta : prune backup to files not part of a package # or changed from distributed version # can be "rpm" or "freebsd" package systems # flexbackup [...] -wday : backup only if the week day matches # the input number. Sunday is 0 or 7. # flexbackup [...] -pipe : write to stdout rather than file/device # flexbackup [...] -ignore-errors : continue backups even if commands return error # status # READING ARCHIVES: # flexbackup -list : list files in archive # flexbackup -extract : extract all files from archive into your # current working directory # flexbackup -extract -flist : restore the files listed in text file # into your current working directory # flexbackup -extract -onefile : restore the single file specified by # into your current working directory # flexbackup -compare : compare archive with the files in your # current directory # flexbackup -restore : interactive restore (dump type only for now) # flexbackup [...] -num : read file number n from tape; if not given # uses current tape position # flexbackup [...] : if archiving to files rather than a device, # list/extract/compare/restore options take # flexbackup [...] -pipe : read archive from stdin # flexbackup [...] -volumes : # of volumes in input # (EXPERIMENTAL mbuffer multivolume support) # INDEX RELATED: # flexbackup -toc : list current device's table of contents # flexbackup -toc all : list all known table of contents # flexbackup -toc : list table of contents for specific key # flexbackup -rmindex all : force db delete of all index info # flexbackup -rmindex : force db delete of specified tape/dir index # flexbackup -rmindex : : force db delete of specified tape:file # # TESTING/DEBUG: # flexbackup -test-tape-drive : tries writing/reading files to make sure you # have tape driver & parameters set up right # flexbackup [...] -n : don't run actual dump or mt commands, just echo # flexbackup [...] -type filelist : special backup type that just saves list of # files that would have been archived # MISC: # flexbackup -newtape : erase & create new index key (but no backup) # flexbackup -rmfile : if backups to disk, rm file & index info # flexbackup -rmfile all : if backups to disk, rm all files/index for dir # flexbackup [...] -c : use instead of /etc/flexbackup.conf # for configuration # flexbackup [...] -type : override $type from config file # flexbackup [...] -compress : override $compress from config file # flexbackup [...] -device : override $device from config file # flexbackup [...] -d 'var=val' : override config file setting of $var # flexbackup -dir -erase : force a rewind/erase before backup # flexbackup -dir -norewind : don't rewind tape after a single backup # flexbackup -set -noreten : don't retension for level 0 set backups # flexbackup -set -noerase : don't rewind/erase for level 0 set backups # flexbackup [...] -reten : force a retension before read # flexbackup [...] -nodefaults : don't use any default values for config variables # flexbackup -version : show version # ###################################################################### # # flexbackup is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # flexbackup is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flexbackup; see the file COPYING. If not, write to # the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. # ###################################################################### use POSIX; use AnyDBM_File; use Getopt::Long; use Text::Wrap; use File::Basename; use English; use strict; # No output buffering $OUTPUT_AUTOFLUSH = 1; # Set the traditional UNIX system locale behavior (touch doesn't read LANG) my $loc = POSIX::setlocale( &POSIX::LC_ALL, "C" ); # See if afio is calling us as a control script if (defined($ARGV[0]) and ($ARGV[0] =~ /flexbackup.volume_header_info/)) { &print_afio_volume_header(); } # This is changed during "make install" $::CONFFILE="/etc/flexbackup.conf"; # This took awhile to figure out. if the shell is capable of it, we use # this on the end of any pipelines to see if any of the commands in the # pipeline failed, rather than just the last one. # # If /bin/sh is really bash2 in disguise, or remote shell is bash2/zsh, # we can use their status array variables # # With plain sh, we don't know if the non-last command in the pipe fails # See exit-status collecting trick in the code. # # With tcsh/csh as a remote shell, you don't know which command, but # $? is still set if anything in the pipeline failed # $::bash_pipe_exit = '; x=(${PIPESTATUS[@]}); i=0; while [ $i -lt ${#x[@]} ]; do [ ${x[$i]} -eq 0 ] || exit ${x[$i]}; i=$(($i+1)); done'; $::zsh_pipe_exit = '; x=(${pipestatus[@]}); i=1; while [ $i -le ${#x[@]} ]; do [ ${x[$i]} -eq 0 ] || exit ${x[$i]}; i=$(($i+1)); done'; # tar has a limit of this many chars in its volume label $::tar_max_label = 99; # Get commandline flags %::opt = (); if (! &::GetOptions(\%::opt, "c=s", "compare:s", "compress=s", "d=s%", "dir=s", "pipe", "pkgdelta=s", "device=s", "differential", "erase!", "extract:s", "flist=s", "full", "help", "incremental", "ignore-errors", "level=s", "list:s", "onefile=s", "n", "newtape", "nodefaults", "num:i", "restore:s", "reten!", "rewind!", "rmfile:s@", "rmindex:s@", "set=s", "test-tape-drive", "toc:s", "type=s", "version", "volumes:i", "wday=i" )) { exit(0); } # Default fd for messages (we might have stdout as archive output) if (defined($::opt{'pipe'})) { $::msg = *STDERR; } else { $::msg = *STDOUT; } # Give usage message if (defined($::opt{'help'})) { &usage(); exit(0); } # Version if (defined($::opt{'version'})) { print $::msg "flexbackup version " . &versionstring() . "\n"; print $::msg '$Id: flexbackup,v 1.185 2003/10/10 14:12:09 edwinh Exp $ ' . "\n"; exit(0); } # Exit if -wday given and it isn't that day of the week (see FAQ) &check_wday(); # Get/read config file print $::msg "\nflexbackup version " . &versionstring() . "\n"; &readconfigfile(); print $::msg "\n"; # Set OS type chomp($::uname = `uname -s`); # Sanity check commandline flags and config file options &optioncheck(); &line('screen'); # Check shells, buffer is runnable, remote progs... &test_before_run(); # See about rewind/erase/reten flags &set_tape_operation_defaults(); # Get current date string $::date = ¤t_time('numeric'); # Decide what to do if (defined($::opt{'restore'})) { &restore_routine(); } elsif (defined($::opt{'extract'})) { &extract_routine(); } elsif (defined($::opt{'compare'})) { &compare_routine(); } elsif (defined($::opt{'list'})) { &list_routine(); } elsif (defined($::opt{'dir'}) or defined($::opt{'set'})) { &backup_routine(); } elsif (defined($::opt{'toc'})) { &line(); # Only do this if we're going to grab current tape index if ($::opt{'toc'} eq '') { &mt("generic-blocksize $::mt_blksize"); } &toc_routine(); } elsif (defined($::opt{'rmindex'})) { &line(); foreach my $arg (@{$::opt{'rmindex'}}) { &rmindex($arg); } } elsif (defined($::opt{'newtape'})) { &line(); &mt("generic-blocksize $::mt_blksize"); &newtape(); } elsif (defined($::opt{'rmfile'})) { &line(); &rmfile(); } elsif (defined($::opt{'test-tape-drive'})) { &line(); &test_tape_drive(); } if (($::mode !~ m/^(list|extract|restore|compare|test-tape-drive)$/) and ($cfg::indexes eq "true")) { untie(%::index); } exit(0); ###################################################################### # Backup ###################################################################### sub backup_routine { my @files; my $label; my $tapecounter = 0; my %oldlogs; my $fs; my $logfile; my $symlink = '';; my $logext = ''; my $comp_cmd; my $tape_key; my $logsuffix = ''; my $error = 0; # Figure out log file name & empty log file if (defined($::opt{'set'})) { $label = &get_label($::opt{'set'}); } else { $label = &get_label($::opt{'dir'}); } if ($cfg::staticlogs eq 'false' ) { $logsuffix = ".$::date"; } if (!defined($::set_incremental)) { $logfile = "$cfg::prefix$label.$::level" . $logsuffix; } else { $logfile = "$cfg::prefix$label.incremental" . $logsuffix; } $symlink = "$cfg::prefix$label.latest"; $::log = "$cfg::logdir/$logfile"; if (! open(LOG,">$::log")) { die "Can't write to $::log: $OS_ERROR"; } close(LOG); &line(); &mt("generic-blocksize $::mt_blksize"); # Remember old log files (will remove at end of job) # ("old" = any higher- or equal-numbered logs for this label) if (!defined($::set_incremental)) { opendir(DIR,"$cfg::logdir") or die("Can't open cfg::logdir: $OS_ERROR"); @files = readdir(DIR); foreach my $lf (reverse sort @files) { # Skip our own log next if ($lf =~ m/^$logfile(\.gz|\.bz2|\.lzo|\.Z|\.zip)?$/); # Find normal old logs if ($lf =~ m/^$cfg::prefix$label\.(\d+)(\.(\d+))?(\.gz|\.bz2|\.lzo|\.Z|\.zip)?$/) { if ($1 >= $::level) { # Might be from $staticlogs=true or false if (defined($3)) { $oldlogs{"$cfg::logdir/$lf"} = $1 . "|" . $3; } else { $oldlogs{"$cfg::logdir/$lf"} = $1; } } } # If this is a level 0, we can nuke incremental logs if (($::level == 0) and ($lf =~ m/^$cfg::prefix$label\.(incremental)(\.(\d+))?(\.gz|\.bz2|\.lzo|\.Z|\.zip)?$/)) { # Might be from $staticlogs=true or false if (defined($3)) { $oldlogs{"$cfg::logdir/$lf"} = $1 . "|" . $3; } else { $oldlogs{"$cfg::logdir/$lf"} = $1; } } } close(DIR); } # Possibly populate package-file hashes if we are using # -pkgdelta. This is so we only have to run through these operations # once per machine if multiple fs's are being run if (defined($::pkgdelta)) { if (defined($::local)) { &list_packages('localhost'); &find_packaged_files('localhost'); &find_changed_files('localhost'); } foreach my $host (keys %::remotehosts) { &list_packages($host); &find_packaged_files($host); &find_changed_files($host); } $::pkgdelta_filelist = "$cfg::tmpdir/pkgdelta.$PROCESS_ID"; &line(); } ########################## # # Main backup routine # ########################## if (defined($::opt{'set'})) { if (!defined($::set_incremental)) { &log("| Doing level $::level backup of set $::opt{set} using $cfg::type"); } else { &log("| Doing incremental backup of $::opt{set} using $cfg::type"); } # All sets or just one? my @do_sets; if ($::opt{'set'} eq 'all') { @do_sets = keys(%cfg::set); if (defined($::tapedevice)) { $_ = scalar(@do_sets); $_ = join(" ", @do_sets) . " ($_ tapes)"; } else { $_ = join(" ", @do_sets); } &log("| All sets = $_"); } else { @do_sets = ($::opt{'set'}); } my $num_tapes = scalar(@do_sets) - 1; foreach my $this_set (@do_sets) { # Maybe retension if (($::do_reten == 1) and defined($::tapedevice)) { &log('| Retensioning tape...'); &mt('retension'); } # Maybe rewind/erase if ($::do_erase == 1) { $tape_key = &newtape(); } else { &mt('rewind'); $tape_key = &get_tape_key(); if(defined($::tapedevice)) { &log('| Making sure tape is at end of data...'); } &mt('generic-eod'); } # Print what this set contains &log("| Backup set \"$this_set\" ($cfg::set{$this_set})"); # Show tape position if (defined($::tapedevice)) { # Multiple tapes are only for level 0 if (!defined($::set_incremental) and ($::level == 0)) { &log("| Tape \#$tapecounter"); } &line(); &mt('generic-query'); } # Iterate over the filesystems in the set and back 'em up foreach my $dir (&split_list($cfg::set{$this_set})) { my $level; # Get rid of trailing / $dir = &nuke_trailing_slash($dir); # If level is icremental for the set, each dir might # have a different numeric level if (!defined($::set_incremental)) { $level = $::level; } else { $level = &get_incremental_level($dir); } $error = &backup($dir, $tape_key, $level); last if ($error != 0); if ($cfg::indexes eq "true") { $::nextfile++; } } # Prompt for new tape if more than one set in list & level 0 if (!defined($::set_incremental) and ($::level == 0)) { if ($tapecounter < $num_tapes) { # Maybe rewind (usually true) if ($::do_rewind_after == 1) { if(defined($::tapedevice)) { &log("| Rewinding..."); } &mt('rewind'); &line(); } if (defined($::tapedevice)) { &toc_routine($tape_key); } $tapecounter++; if (defined($::tapedevice)) { print $::msg "\n"; while(1) { print $::msg "---> Insert tape \#$tapecounter (enter y to continue) "; chomp($_ = ); last if ($_ =~ m/^y/i); } print $::msg "\n"; &line(); } } # end not at last tape } # end if level == 0 } # end foreach set } else { # Just one filesystem, -dir given &log("| Doing level $::level backup of $::opt{dir} using $cfg::type"); # Maybe retension if ($::do_reten == 1) { if (defined($::tapedevice)) { &log('| Retensioning tape...'); } &mt('retension'); } # Maybe rewind/erase if ($::do_erase == 1) { $tape_key = &newtape(); } else { &mt('rewind'); $tape_key = &get_tape_key(); if (defined($::tapedevice)) { &log('| Making sure tape is at end of data...'); } &mt('generic-eod'); } if (defined($::tapedevice)) { &line(); &mt('generic-query'); } $error = &backup($::opt{'dir'}, $tape_key, $::level); } # end set or single fs if (defined($::tapedevice)) { &line(); } # Maybe rewind (usually true) if (($::do_rewind_after == 1) and defined($::tapedevice)) { &log("| Rewinding..."); &mt('rewind'); } # Remove old log files now that we are done if ($error == 0) { my $rmlogs = 0; foreach my $lf (sort keys %oldlogs) { $rmlogs++; my ($lev,$d) = split(/\|/,$oldlogs{$lf}); if (defined($d)) { &log("| Removing old level $lev log of $label (dated $d)"); } else{ &log("| Removing old level $lev log of $label"); } if (!defined($::debug)) { unlink("$lf") or warn("Can't remove $lf: $OS_ERROR\n"); } } &line('log') if ($rmlogs > 0); } # Compress log file if ($cfg::comp_log ne 'false') { if ($cfg::comp_log eq "gzip") { $logext = ".gz"; $comp_cmd = "$::path{gzip} -f \"$::log\""; } elsif ($cfg::comp_log eq "bzip2") { $logext = ".bz2"; $comp_cmd = "$::path{bzip2} -f \"$::log\""; } elsif ($cfg::comp_log eq "lzop") { $logext = ".lzo"; $comp_cmd = "$::path{lzop} -U -f \"$::log\""; } elsif ($cfg::comp_log eq "zip") { $logext = ".zip"; $comp_cmd = "$::path{cat} \"$::log\" | $::path{zip} -q - - > \"$::log" . $logext . "\"; $::path{rm} -f \"$::log\""; } elsif ($cfg::comp_log eq "compress") { $logext = ".Z"; $comp_cmd = "$::path{compress} -f \"$::log\""; } undef $::log; &log("| Compressing log ($logfile" . "$logext)", 'screen'); system("$comp_cmd"); if ($CHILD_ERROR) { warn("Error compressing log file\n"); } } # Symlink the "latest" log file for this level unlink("$cfg::logdir/$symlink" . $logext); &log("| Linking $symlink" . "$logext -> $logfile" . $logext, 'screen'); symlink("$logfile" . $logext,"$cfg::logdir/$symlink" . $logext); &line('screen'); if ($error == 0) { &toc_routine($tape_key); } exit($error); } ###################################################################### # Backup a filesystem ###################################################################### sub backup { my $dir = shift(@_); my $tape_key = shift(@_); my $level = shift(@_); my $title; my $title_without_type; my @cmds; my @echo_cmds; my $cmd; my $localdir = $dir; my $label = &get_label($dir); my $host; my @files; my %oldstamps; my $remote; my $tapehost; my $indexkey; my $catchexit; my $exitscript = "$cfg::tmpdir/collectexit.$PROCESS_ID.sh"; my $result = "$cfg::tmpdir/exitstatus.$PROCESS_ID"; my $pkglist; my $error = 0; &line(); if ($localdir =~ s/^(.+)://) { $remote = $1; chomp($tapehost = `hostname`); if (($tapehost eq $remote) or ($remote =~ /^localhost/)) { die("Remote host and this host are the same! No scooby snack for you!"); } } else { undef $remote; } # Remember old stamp files (will remove at end of job) # "old" = any higher-numbered stamps for this label # (we will be touching the one of equal level, so don't mark for removal) opendir(DIR,"$cfg::stampdir") or die("Can't open $cfg::stampdir: $OS_ERROR"); @files = readdir(DIR); foreach my $f (reverse sort @files) { next if ($f !~ m/^$cfg::sprefix$label\.(\d+)$/); if ($1 > $level) { $oldstamps{"$cfg::stampdir/$f"} = $1 } } close(DIR); # Create file name if writing to a file # (config file's $device points to a dir in this case) if (defined($::use_file)) { my $filename = $level; if (defined($::pkgdelta)) { $filename .= $::pkgdelta; } if ($cfg::staticfiles eq 'true') { $filename .= "." . $cfg::type; } else { $filename .= "." . $::date . "." . $cfg::type; } # Some types need the filename modified if ($cfg::type eq 'ar') { $filename =~ s/ar$/a/; } elsif ($cfg::type eq 'copy') { $filename =~ s/\.copy$//; } elsif ($cfg::type eq 'rsync') { $filename =~ s/\.rsync$//; } # Note compression setting in filename if ($cfg::type =~ m/^(tar|dump|cpio|star|pax|ar|shar|filelist)$/) { if ($cfg::compress eq "gzip") { $filename .= ".gz"; } elsif ($cfg::compress eq "bzip2") { $filename .= ".bz2"; } elsif ($cfg::compress eq "lzop") { $filename .= ".lzo"; } elsif ($cfg::compress eq "zip") { $filename .= ".zip"; } elsif ($cfg::compress eq "compress") { $filename .= ".Z"; } } elsif ($cfg::type eq "afio") { # tag these a little different, the archive file itself isn't a # .gz or .bz2, but the files in it are.... if ($cfg::compress eq "gzip") { $filename .= "-gz"; } elsif ($cfg::compress eq "bzip2") { $filename .= "-bz2"; } elsif ($cfg::compress eq "lzop") { $filename .= "-lzo"; } elsif ($cfg::compress eq "zip") { $filename .= "-zip"; } elsif ($cfg::compress eq "compress") { $filename .= "-Z"; } } # Overwrite device var to be the archive filename $::device = $cfg::device . "/" . $label . "." . $filename; } # Just get the date for now; don't write the timestamp # Until after the backup has run $::date_at_start = ¤t_time('ctime'); $::stamp_at_start = ¤t_time('numeric'); # Label for this archive chomp($host = `hostname`); $title = $cfg::type . "+" . $cfg::compress; $title =~ s/\+false//; if (!defined($::pkgdelta)) { $title = "level $level $dir $::date_at_start $title from $host"; $title_without_type = "level $level $dir $::date_at_start from $host"; } else { $pkglist = "flexbackup.$::pkgdelta.packagelist"; $title = "level $level+$::pkgdelta $dir $::date_at_start $title from $host"; $title_without_type = "level $level+$::pkgdelta $dir $::date_at_start from $host"; } # Modify table of contents if (($tape_key ne '') and ($cfg::indexes eq "true")) { # If writing to files, store the filename if (defined($::use_file)) { @_ = split(/\//,$::device); $_ = pop(@_); $indexkey = "$tape_key|$_"; if (defined($::debug)) { &log("(debug) \$::index{$indexkey} = $title_without_type"); } else { $::index{$indexkey} = "$title_without_type"; } } elsif (defined($::use_blockdevice)) { # no indexes anyway } else { $indexkey = "$tape_key|$::nextfile"; if (defined($::debug)) { &log("(debug) \$::index{$indexkey} = $title"); } else { $::index{$indexkey} = $title; } &log("| File number $::nextfile, tape index $tape_key"); } } # Write list of packages if (defined($::pkgdelta) and ( ($cfg::pkgdelta_archive_list eq 'true') or (($cfg::pkgdelta_archive_list eq 'rootonly') and ($localdir eq '/')) ) ) { $pkglist = "$localdir/$pkglist"; my $write = "> $pkglist"; my $h; if(defined($remote)) { $write = &maybe_remote_cmd("$::path{cat} $write", $remote); $write = "| $write"; $h = $remote; } else { $h = 'localhost'; } if (!defined($::debug)) { open(LIST,"$write") || die; foreach my $pkg (sort keys %{$::package_list{$h}}) { print LIST "$pkg\n"; } close(LIST); } } &log("| Backup of: $dir"); my $remove = ''; if ($cfg::type eq 'dump') { ($remove, @cmds) = &backup_dump($label, $localdir, $level, $remote); } elsif ($cfg::type eq 'afio') { ($remove, @cmds) = &backup_afio($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'cpio') { ($remove, @cmds) = &backup_cpio($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'tar') { ($remove, @cmds) = &backup_tar($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'star') { ($remove, @cmds) = &backup_star($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'pax') { ($remove, @cmds) = &backup_pax($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'zip') { ($remove, @cmds) = &backup_zip($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'ar') { ($remove, @cmds) = &backup_ar($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'shar') { ($remove, @cmds) = &backup_shar($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'lha') { ($remove, @cmds) = &backup_lha($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'copy') { ($remove, @cmds) = &backup_copy_cpio($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'rsync') { ($remove, @cmds) = &backup_copy_rsync($label, $localdir, $title, $level, $remote); } elsif ($cfg::type eq 'filelist') { ($remove, @cmds) = &backup_filelist($label, $localdir, $title, $level, $remote); } # Nuke any tmp files used in the above routines if ($remove ne '') { push(@cmds, &maybe_remote_cmd("$::path{rm} -f $remove", $remote)); } # Create/nuke tmp file list if we did local package delta if (defined($::pkgdelta)) { if ( ($cfg::pkgdelta_archive_list eq 'true') or (($cfg::pkgdelta_archive_list eq 'rootonly') and ($localdir eq '/')) ) { push(@cmds, &maybe_remote_cmd("$::path{rm} -f $::pkgdelta_filelist $pkglist", $remote)); } else { push(@cmds, &maybe_remote_cmd("$::path{rm} -f $pkglist", $remote)); } } # Strip multiple spaces foreach my $cmd (@cmds) { $cmd =~ s/\s+/ /g; } # Use pipeline exitcode hook if /bin/sh can't report pipeline status if ($::shelltype{'localhost'} =~ m/^(unknown|bash1|ksh)$/) { $catchexit = 1; unlink($result); open(SCR, "> $exitscript") || die; print SCR '#!/bin/sh' . "\n"; print SCR '"$@"' . "\n";; print SCR '[ $? = 0 ] || echo $@ >> ' . $result . "\n"; close(SCR); chmod(0755, $exitscript); push(@cmds, "[ ! -e $result ]"); } # Replace piped commands with exit status collector if we need to foreach my $cmd (@cmds) { if (defined($catchexit)) { # Save ssh commands temporarily so we don't replace pipes inside them my $saveremote; if ($cmd =~ s/($cfg::remoteshell .* \'.*\')/XXXflexbackupXXX/) { $saveremote = $1; } # Replace piped or anded commands with catch-script # -Not if the command started a subshell ( .. ) if ($cmd =~ s:\s+(\||&&)\s+([^\(]): $1 $exitscript $2:g) { # You would think we'd put it on the front of the pipe as # well. Can't do this globally because the "cd &&" # at the front makes the cd happen in a subshell. If # its not "cd , do it. if ($cmd !~ m:^\s*cd\s+\"[^\"]+\"\s*(;|&&):) { $cmd = "$exitscript $cmd"; } # Take care of subshell $cmd =~ s:\s+(\||&&)\s+(\()\s*: $1 \( $exitscript :g; } # Put any ssh stuff back $cmd =~ s:XXXflexbackupXXX:$saveremote:; } } # Format commands for nice printing @echo_cmds = @cmds; foreach my $line (@echo_cmds) { &split_and_echo($line); } &line(); # Enough fooling around... run it. if (!defined($::debug)) { foreach $cmd (@cmds) { if (defined($::use_pipe)) { system("$cmd"); } else { if ($::shelltype{'localhost'} eq 'bash2') { # /bin/sh is really bash2 on this system open(CMD,"($cmd " . $::bash_pipe_exit . ") 2>&1 |") || die; } elsif ($::shelltype{'localhost'} eq 'zsh') { # Does anybody make /bin/sh be zsh? probably not... open(CMD,"($cmd " . $::zsh_pipe_exit . ") 2>&1 |") || die; } else { open(CMD,"($cmd) 2>&1 |") || die; } open(LOG,">>$::log") || die; while() { print $::msg $_; print LOG $_; } close(LOG); close(CMD); } if ($CHILD_ERROR) { &log(''); # If using exit trick, cat the result file; otherwise use normal output if (defined($catchexit)) { my $out = `cat $result`; &log("ERROR: non-zero exit from:\n$out"); } else { &log("ERROR: non-zero exit from:\n$cmd"); } if (defined($::opt{'ignore-errors'})) { $error = 0; &log(''); &log("ERROR: will continue anyway"); } else { $error++; &log(''); &log("ERROR: exiting"); # Put ERROR in the index if tapedevice, or nuke index if file if (defined($indexkey)) { if (defined($::use_file)) { delete $::index{$indexkey}; } elsif (defined($::use_blockdevice)) { # no indexes anyway } else { $::index{$indexkey} .= "\n\t---> ERROR during write, above may not be valid"; } } # If file, rm botched file regardless of index if (defined($::use_file)) { if ($cfg::type =~ m/^(copy|rsync)$/) { system("rm -rf $::device"); } else { unlink($::device); } } } # ignore error defined } # CHILD_ERROR } # foreach cmd } else { &log("(debug) command output would be here"); } &line(); # Actually remove the old stamp files now that we are done if ($error == 0) { foreach my $ts (sort keys %oldstamps) { print $::msg "| Removing out of date level $oldstamps{$ts} timestamp for $dir\n"; if (!defined($::debug)) { unlink("$ts") or warn("Can't remove $ts: $OS_ERROR\n"); } } } # Create timestamp file, but use date from before the backup started # so next time we will catch files that might have been touched during the run my $t = ¤t_time('ctime'); &log("| Backup start: $::date_at_start"); &log("| Backup end: $t"); if (($error == 0) and !defined($::debug)) { system("$::path{touch} -t \"$::stamp_at_start\" \"$cfg::stampdir/$cfg::sprefix$label.$level\""); } &line(); # Got errors unless I paused before trying to access the tape right way... if ((!defined($::debug)) and defined($::tapedevice)) { sleep 10; } # Show where we are on the tape &mt('generic-query'); if (defined($catchexit)) { unlink($result); unlink($exitscript); } return($error); } ###################################################################### # Return command to backup a directory using dump ###################################################################### sub backup_dump { my $label = shift(@_); my $dir = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $date_flag; my $remove = ''; # Need this check here in case fs=all, level=incremental, and we go beyond 9 if ($level > 9) { die("Can't use level > 9 and type=dump"); } # Warnings about stuff dump can't do if (defined($cfg::exclude_expr[0])) { &log("| NOTE: \$exclude_expr is ignored for type=dump"); } my $prunekey; if (defined($remote)) { $prunekey = "$remote:$dir"; } else { $prunekey = $dir; } if (defined(%{$::prune{$prunekey}})) { &log("| NOTE: \$prune is ignored for type=dump"); } if ($cfg::traverse_fs ne 'false') { &log("| NOTE: \$traverse_fs is always false for type=dump"); } if (defined($::pkgdelta)) { &log("| NOTE: packaging system delta ignored for for type=dump"); } # With this one we don't have to put a stampfile on the remote system # since we only need the date string my $time = &get_last_date($label, $level, 'ctime'); if ($level == 0) { $date_flag = ""; } else { $date_flag = "-T \"$time\" "; } $cmd = ''; $cmd .= "dump -$level "; $cmd .= "$::dump_blk_flag "; if ($cfg::dump_use_dumpdates eq "true") { $cmd .= "-u "; } else { $cmd .= $date_flag; } $cmd .= "$::dump_len_flag "; $cmd .= "-f - "; $cmd .= "$dir $::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using afio ###################################################################### sub backup_afio { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $tmplabel = "$cfg::tmpdir/label.$PROCESS_ID"; my $tmpnocompress = "$cfg::tmpdir/nocompress.$PROCESS_ID"; my $remove = ''; my $no_compress = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } # list of file exenstions to not compress if (($cfg::compress !~ /^(false|hardware)$/) and ($cfg::afio_nocompress_types ne "")) { $cmd = "$::path{printf} \"$cfg::afio_nocompress_types\" > $tmpnocompress"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $no_compress = "-E $tmpnocompress"; $remove .= " $tmpnocompress"; } if ($cfg::label ne 'false') { $cmd = "$::path{printf} \"Volume Label:\\n$title\\n\\n\" > $tmplabel"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $tmplabel"; } $cmd = "cd \"$dir\" && "; if ($cfg::label ne 'false') { $cmd .= "($::path{printf} \"//--$tmplabel flexbackup.volume_header_info\\n\" && "; } $cmd .= &file_list_cmd($dir, $stamp, 'newline', $level, $remote); if ($cfg::label ne 'false') { $cmd .= ")"; } $cmd .= " | "; $cmd .= "$::path{afio} -o "; $cmd .= "$no_compress "; $cmd .= "-z "; $cmd .= "-1 m "; $cmd .= "$::afio_z_flag "; $cmd .= "$::afio_verb_flag "; $cmd .= "$::afio_sparse_flag "; $cmd .= "$::afio_atime_flag "; $cmd .= "$::afio_bnum_flag "; $cmd .= "$::afio_blk_flag "; $cmd .= "-"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using cpio ###################################################################### sub backup_cpio { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } if ($cfg::label ne 'false') { # Kludge a title by replacing / with - in the title # then touch a file in the dir we are going to back up. $title =~ s%/%-%g; $cmd = "$::path{touch} \"$dir/$title\""; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " \"$dir/$title\""; } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd($dir, $stamp, 'null', $level, $remote); $cmd .= "| "; $cmd .= "$::path{cpio} -o "; $cmd .= "-0 "; $cmd .= "-H $cfg::cpio_format "; $cmd .= "$::cpio_verb_flag "; $cmd .= "$::cpio_blk_flag "; $cmd .= "$::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to copy directory tree ###################################################################### sub backup_copy_cpio { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd($dir, $stamp, 'null', $level, $remote); $cmd .= "| "; $cmd .= "$::path{cpio} -o "; $cmd .= "-0 "; $cmd .= "-H $cfg::cpio_format "; $cmd .= "$::cpio_verb_flag "; $cmd .= "$::cpio_blk_flag "; # Buffer both sides / compress if remote if (defined($remote)) { $cmd .= "$::z"; $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Yell if destination exists if (-d "$::device") { &log("| Existing destination directory $::device found!"); &log("| It will be *deleted*, unless you hit CTRL-C"); &log("| and abort within 10 seconds..."); &line(); sleep(10); system("rm -rf $::device"); } # Expand cpio archive on other side of pipe $cmd .= " | "; if (defined($remote)) { $cmd .= "$::unz"; } $cmd .= "("; $cmd .= "mkdir -p $::device ; "; $cmd .= "cd $::device ; "; $cmd .= "$::path{cpio} -i "; $cmd .= "-m "; $cmd .= "-d "; $cmd .= "$::cpio_blk_flag"; $cmd .= ")"; push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to copy directory tree via rsync ###################################################################### sub backup_copy_rsync { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if ($cfg::buffer ne 'false') { &log("| NOTE: \$buffer is ignored for type=rsync"); } if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd($dir, $stamp, 'newline', $level, $remote); # Just the find may run on the remote - rsync call will always be local $cmd = &maybe_remote_cmd($cmd, $remote); # Have to take leading './' off to make rsync's include/exclude work right $cmd .= " | $::path{sed} -e \"s/\\.\\///g\" | "; $cmd .= "$::path{rsync} "; $cmd .= "--include-from=- --exclude=* "; $cmd .= "--archive "; $cmd .= "$::rsync_verb_flag "; $cmd .= "--delete --delete-excluded "; if ($cfg::compress ne 'false') { $cmd .= "--compress "; } if (defined($remote)) { $cmd .= "--rsh=$::path{$cfg::remoteshell} "; if ($cfg::remoteuser ne '') { $cmd .= "$cfg::remoteuser" . '@' . "$remote:"; } else { $cmd .= "$remote:"; } } $cmd .= "$dir/ $::device"; push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using tar ###################################################################### sub backup_tar { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd($dir, $stamp, 'null', $level, $remote); $cmd .= "| "; $cmd .= "$::path{tar} --create "; $cmd .= "--null "; $cmd .= "--files-from=- "; $cmd .= "--ignore-failed-read "; $cmd .= "--same-permissions "; $cmd .= "--no-recursion "; $cmd .= "--totals "; if ($cfg::label ne 'false') { if (length($title) > $::tar_max_label) { &log("| NOTE: truncating tar label (> $::tar_max_label chars)"); $title = substr($title, 0, $::tar_max_label); } $cmd .= "--label \"$title\" "; } $cmd .= "$::tar_verb_flag "; $cmd .= "$::tar_sparse_flag "; $cmd .= "$::tar_atime_flag "; $cmd .= "$::tar_recnum_flag "; $cmd .= "$::tar_blk_flag "; $cmd .= "--file - "; $cmd .= "$::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using star ###################################################################### sub backup_star { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd($dir, $stamp, 'newline', $level, $remote); $cmd .= "| "; $cmd .= "$::path{star} -c "; $cmd .= "list=- "; $cmd .= "-p "; $cmd .= "-l "; $cmd .= "-D "; $cmd .= "-B "; $cmd .= "-dirmode "; if ($cfg::label ne 'false') { $cmd .= "VOLHDR=\"$title\" "; } $cmd .= "H=$cfg::star_format "; $cmd .= "$::star_fifo_flag "; $cmd .= "$::star_acl_flag "; $cmd .= "$::star_verb_flag "; $cmd .= "$::star_sparse_flag "; $cmd .= "$::star_atime_flag "; $cmd .= "$::star_blocknum_flag "; $cmd .= "$::star_blk_flag "; $cmd .= "file=- "; $cmd .= "$::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using pax ###################################################################### sub backup_pax { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } if ($cfg::label ne 'false') { # Kludge a title by replacing / with - in the title # then touch a file in the dir we are going to back up. $title =~ s%/%-%g; $cmd = "$::path{touch} \"$dir/$title\""; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " \"$dir/$title\""; } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd( $dir, $stamp, 'newline', $level, $remote); $cmd .= "| "; $cmd .= "$::path{pax} -w "; $cmd .= "-d "; $cmd .= "-s %^./%% "; $cmd .= "-x $cfg::pax_format "; $cmd .= "$::pax_verb_flag "; $cmd .= "$::pax_blk_flag "; $cmd .= "$::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using zip ###################################################################### sub backup_zip { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $tmpzip = "$cfg::tmpdir/archive.$PROCESS_ID.zip"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } if ($cfg::label ne 'false') { # Kludge a title by replacing / with - in the title # then touch a file in the dir we are going to back up. $title =~ s%/%-%g; $cmd = "$::path{touch} \"$dir/$title\""; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " \"$dir/$title\""; } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd($dir, $stamp, 'newline', $level, $remote); $cmd .= "| "; $cmd .= "$::path{zip} -@ "; $cmd .= "-b $cfg::tmpdir "; # temp file path $cmd .= "-y "; # store symlinks $cmd .= "$::zip_compr_flag "; $cmd .= "$::zip_noz_flag "; # nocompress list $cmd .= "$::zip_verb_flag "; # verbose flag $cmd .= "$tmpzip"; # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); push(@cmds,$cmd); $cmd = "$::path{cat} $tmpzip "; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); $remove .= " $tmpzip"; return($remove, @cmds); } ###################################################################### # Return command to backup a directory using ar ###################################################################### sub backup_ar { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $filelist = "$cfg::tmpdir/arlist.$PROCESS_ID"; my $tmpfile = "$cfg::tmpdir/ar.$PROCESS_ID"; my $remove = ''; &log("| NOTE: ar archives will not descend directories"); if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } if ($cfg::label ne 'false') { # Kludge a title by replacing / with - in the title # then touch a file in the dir we are going to back up. $title =~ s%/%-%g; $title =~ s% %_%g; $cmd = "$::path{touch} \"$dir/$title\""; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " \"$dir/$title\""; } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd( $dir, $stamp, 'newline', $level, $remote, '-maxdepth 1 ! -type d'); $cmd .= "> $filelist; "; $cmd .= "$::path{ar} rc"; $cmd .= "$::ar_verb_flag "; $cmd .= "$tmpfile "; $cmd .= "`$::path{cat} $filelist`"; $cmd .= "; $::path{cat} $tmpfile $::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); $remove .= " $filelist $tmpfile"; return($remove, @cmds); } ###################################################################### # Return command to backup a directory using shar ###################################################################### sub backup_shar { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd( $dir, $stamp, 'newline', $level, $remote, '! -type d'); $cmd .= " | "; $cmd .= "$::path{shar} "; $cmd .= "$::shar_verb_flag "; if ($cfg::label ne 'false') { $cmd .= "-n \"$title\" "; } $cmd .= "-S "; $cmd .= "$::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); return($remove, @cmds); } ###################################################################### # Return command to backup a directory using lha ###################################################################### sub backup_lha { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $filelist = "$cfg::tmpdir/lhalist.$PROCESS_ID"; my $tmpfile = "$cfg::tmpdir/lha.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } if ($cfg::label ne 'false') { # Kludge a title by replacing / with - in the title # then touch a file in the dir we are going to back up. $title =~ s%/%-%g; $title =~ s% %_%g; $cmd = "echo \"$title\" > \"$dir/$title\""; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " \"$dir/$title\""; } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd( $dir, $stamp, 'newline', $level, $remote); $cmd .= "> $filelist; "; $cmd .= "$::path{lha} a"; $cmd .= "$::lha_verb_flag "; $cmd .= "$tmpfile "; $cmd .= "`$::path{cat} $filelist`"; $cmd .= "; $::path{cat} $tmpfile $::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); $remove .= " $filelist $tmpfile"; return($remove, @cmds); } ###################################################################### # Just back up the file listing (useful for debugging) ###################################################################### sub backup_filelist { my $label = shift(@_); my $dir = shift(@_); my $title = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $cmd = ''; my @cmds; my $stamp = "$cfg::tmpdir/refdate.$PROCESS_ID"; my $filelist = "$cfg::tmpdir/filelist.$PROCESS_ID"; my $remove = ''; if (defined($remote) and ($level != 0)) { my $time = &get_last_date($label, $level, 'numeric'); $cmd = "$::path{touch} -t \"$time\" $stamp"; push(@cmds, &maybe_remote_cmd($cmd, $remote)); $remove .= " $stamp"; } else { $stamp = &get_last_date($label, $level, 'filename'); } if (defined $::use_pipe) { &log("| NOTE: Writing list of files that would have been backed up to stdout"); } else { &log("| NOTE: Writing list of files that would have been backed up to current directory"); } $cmd = "cd \"$dir\" && "; $cmd .= &file_list_cmd( $dir, $stamp, 'newline', $level, $remote); $cmd .= "> $filelist; $::path{cat} $filelist 1>&2; $::path{cat} $filelist "; $cmd .= "$::z"; # Buffer both sides if remote if (defined($remote)) { $cmd .= $::buffer_cmd; } # Wrap all that together $cmd = &maybe_remote_cmd($cmd, $remote); # Append writer stuff $cmd = &append_writer_cmd($cmd); push(@cmds, $cmd); $remove .= " $filelist"; return($remove, @cmds); } ###################################################################### # List the files in an archive ###################################################################### sub list_routine { my $cmd = &setup_before_read('list'); if ($cfg::type eq 'dump') { $cmd .= "$::path{restore} -t "; $cmd .= "$::dump_verb_flag "; $cmd .= "$::dump_blk_flag "; $cmd .= "-f -"; } elsif ($cfg::type eq 'afio') { $cmd .= "$::path{afio} -t "; $cmd .= "-z "; # Don't use label reader if reading from pipe (needs stdin) if (!defined($::use_pipe)) { $cmd .= "-D $0 "; } $cmd .= "$::afio_unz_flag "; $cmd .= "$::afio_verb_flag "; $cmd .= "$::afio_sparse_flag "; $cmd .= "$::afio_bnum_flag "; $cmd .= "$::afio_blk_flag "; $cmd .= "-"; } elsif ($cfg::type eq 'cpio') { $cmd .= "$::path{cpio} -t "; $cmd .= "$::cpio_verb_flag "; $cmd .= "$::cpio_blk_flag"; } elsif ($cfg::type eq 'tar') { $cmd .= "$::path{tar} --list "; $cmd .= "--totals "; $cmd .= "$::tar_verb_flag "; $cmd .= "$::tar_sparse_flag "; $cmd .= "$::tar_recnum_flag "; $cmd .= "$::tar_blk_flag "; $cmd .= "-B "; $cmd .= "--file -"; } elsif ($cfg::type eq 'star') { $cmd .= "$::path{star} -t "; $cmd .= "$::star_fifo_flag "; $cmd .= "$::star_verb_flag "; $cmd .= "$::star_sparse_flag "; $cmd .= "$::star_blocknum_flag "; $cmd .= "$::star_blk_flag "; $cmd .= "-B "; $cmd .= "file=-"; } elsif ($cfg::type eq 'pax') { $cmd .= "$::path{pax} "; $cmd .= "$::pax_verb_flag "; } elsif ($cfg::type eq 'zip') { my $tmpfile = "$cfg::tmpdir/zip.$PROCESS_ID"; $cmd .= "$::path{cat} > $tmpfile ; "; $cmd .= "$::path{unzip} -l "; $cmd .= "$::zip_verb_flag "; $cmd .= "$tmpfile ; "; $cmd .= "$::path{rm} -f $tmpfile"; } elsif ($cfg::type eq 'ar') { my $tmpfile = "$cfg::tmpdir/ar.$PROCESS_ID"; $cmd .= "$::path{cat} > $tmpfile; "; $cmd .= "$::path{ar} t"; $cmd .= "$::ar_verb_flag "; $cmd .= "$tmpfile; "; $cmd .= "$::path{rm} -f $tmpfile"; } elsif ($cfg::type eq 'shar') { $cmd .= "perl -pe 'last if (! m/^#/)'"; } elsif ($cfg::type =~ m/^(copy|rsync)$/) { if ($cfg::verbose eq "true") { $cmd = "ls -laR $::device"; } else { $cmd = "ls -aR $::device"; } } elsif ($cfg::type eq 'lha') { my $tmpfile = "$cfg::tmpdir/lha.$PROCESS_ID"; $cmd .= "$::path{cat} > $tmpfile ; "; $cmd .= "$::path{lha} l"; $cmd .= "$::lha_verb_flag "; $cmd .= "$tmpfile ; "; $cmd .= "$::path{rm} -f $tmpfile"; } elsif ($cfg::type eq 'filelist') { $cmd .= "$::path{cat}"; } &run_or_echo_then_query($cmd); } ###################################################################### # Extract files (maybe a list) to current directory ###################################################################### sub extract_routine { my $restore_files = ''; my $newlist = "$cfg::tmpdir/extract.$PROCESS_ID"; my $cmd = &setup_before_read('extract'); if (defined($::opt{'flist'})) { # Have to get a list of the files for restore to use open(LIST,"$::opt{flist}") or die ("Can't open $::opt{flist}: $OS_ERROR"); open(NEWLIST,">$newlist") or die ("Can't open $newlist: $OS_ERROR"); while() { chomp; $_ =~ s%^/%%; $_ =~ s%^\./%%; # Some types need the leading ./ to extract the file list, # since its stored that way if ($cfg::type =~ m/^(tar|lha)$/) { $_ = './' . $_; } print NEWLIST "$_\n"; $restore_files .= " $_"; } close(LIST); close(NEWLIST); &log("| Extracting files listed in $::opt{flist}"); } if (defined($::opt{'onefile'})) { open(NEWLIST,">$newlist") or die ("Can't open $newlist: $OS_ERROR"); $_ = $::opt{'onefile'}; $_ =~ s%^/%%; $_ =~ s%^\./%%; # Some types need the leading ./ to extract the file list, # since its stored that way if ($cfg::type =~ m/^(tar|lha)$/) { $_ = './' . $_; } print NEWLIST "$_\n"; $restore_files .= " $_"; close(NEWLIST); &log("| Extracting single file" . $restore_files); } if ($cfg::type eq 'dump') { $cmd .= "$::path{restore} -x "; $cmd .= "$::dump_verb_flag "; $cmd .= "$::dump_blk_flag "; $cmd .= "-f -"; $cmd .= $restore_files; } elsif ($cfg::type eq 'afio') { $cmd .= "$::path{afio} -i "; if ($restore_files ne '') { $cmd .= "-w $newlist "; } $cmd .= "-z "; $cmd .= "-x "; # Don't use label reader if reading from pipe (needs stdin) if (!defined($::use_pipe)) { $cmd .= "-D $0 "; } $cmd .= "$::afio_unz_flag "; $cmd .= "$::afio_verb_flag "; $cmd .= "$::afio_sparse_flag "; $cmd .= "$::afio_bnum_flag "; $cmd .= "$::afio_blk_flag "; $cmd .= "-"; } elsif ($cfg::type eq 'cpio') { $cmd .= "$::path{cpio} -i "; if ($restore_files ne '') { $cmd .= "-E $newlist "; } $cmd .= "-m "; $cmd .= "-d "; $cmd .= "$::cpio_verb_flag "; $cmd .= "$::cpio_blk_flag"; } elsif ($cfg::type eq 'tar') { $cmd .= "$::path{tar} --extract "; if ($restore_files ne '') { $cmd .= "--files-from $newlist "; } $cmd .= "--totals "; $cmd .= "--same-permissions "; $cmd .= "$::tar_verb_flag "; $cmd .= "$::tar_sparse_flag "; $cmd .= "$::tar_recnum_flag "; $cmd .= "$::tar_blk_flag "; $cmd .= "-B "; $cmd .= "--file -"; } elsif ($cfg::type eq 'star') { $cmd .= "$::path{star} -x "; if ($restore_files ne '') { $cmd .= "list=$newlist "; } $cmd .= "-p "; $cmd .= "$::star_fifo_flag "; $cmd .= "$::star_verb_flag "; $cmd .= "$::star_sparse_flag "; $cmd .= "$::star_blocknum_flag "; $cmd .= "$::star_blk_flag "; $cmd .= "-B "; $cmd .= "file=-"; } elsif ($cfg::type eq 'pax') { $cmd .= "$::path{pax} -r "; $cmd .= "$::pax_verb_flag "; $cmd .= $restore_files; } elsif ($cfg::type eq 'zip') { my $tmpfile = "$cfg::tmpdir/zip.$PROCESS_ID"; $cmd .= "$::path{cat} > $tmpfile ; "; $cmd .= "$::path{unzip} "; $cmd .= "$tmpfile "; $cmd .= $restore_files; $cmd .= "; "; $cmd .= "$::path{rm} -f $tmpfile"; } elsif ($cfg::type eq 'ar') { my $tmpfile = "$cfg::tmpdir/ar.$PROCESS_ID"; $cmd .= "$::path{cat} > $tmpfile; "; $cmd .= "$::path{ar} xo"; $cmd .= "$::ar_verb_flag "; $cmd .= "$tmpfile "; $cmd .= $restore_files; $cmd .= "; "; $cmd .= "$::path{rm} -f $tmpfile"; } elsif ($cfg::type eq 'shar') { $cmd .= "sh "; if ($restore_files ne '') { &log("| NOTE: \"-flist/-onefile\" ignored for shar"); } } elsif ($cfg::type =~ m/^(copy|rsync)$/) { die("Ummm... just copy your files, you have the whole tree..."); } elsif ($cfg::type eq 'filelist') { die("You can't extract the 'filelist' type, it's just for testing..."); } elsif ($cfg::type eq 'lha') { my $tmpfile = "$cfg::tmpdir/lha.$PROCESS_ID"; $cmd .= "$::path{cat} > $tmpfile ; "; $cmd .= "$::path{lha} x"; $cmd .= "$::lha_verb_flag "; $cmd .= "$tmpfile "; $cmd .= $restore_files; $cmd .= "; "; $cmd .= "$::path{rm} -f $tmpfile"; } &run_or_echo_then_query($cmd); if (defined($::opt{'flist'})) { unlink("$newlist") or die ("Can't remove $newlist: $OS_ERROR"); } } ###################################################################### # Compare an archive to current directory ###################################################################### sub compare_routine { my $cmd = &setup_before_read('compare'); if ($cfg::type eq 'dump') { $cmd .= "$::path{restore} -C "; $cmd .= "$::dump_blk_flag "; $cmd .= "-f -"; } elsif ($cfg::type eq 'afio') { $cmd .= "$::path{afio} -r "; $cmd .= "-z "; # Don't use label reader if reading from pipe (needs stdin) if (!defined($::use_pipe)) { $cmd .= "-D $0 "; } $cmd .= "$::afio_unz_flag "; $cmd .= "$::afio_sparse_flag "; $cmd .= "$::afio_blk_flag "; $cmd .= "-"; } elsif ($cfg::type eq 'tar') { $cmd .= "$::path{tar} --diff "; $cmd .= "--totals "; $cmd .= "$::tar_blk_flag "; $cmd .= "$::tar_sparse_flag "; $cmd .= "$::tar_recnum_flag "; $cmd .= "-B "; $cmd .= "--file -"; } elsif ($cfg::type eq 'star') { $cmd .= "$::path{star} -diff "; $cmd .= "$::star_fifo_flag "; $cmd .= "$::star_blk_flag "; $cmd .= "$::star_sparse_flag "; $cmd .= "$::star_blocknum_flag "; $cmd .= "-B "; $cmd .= "file=-"; } elsif ($cfg::type =~ m/^(copy|rsync)$/) { $::path{'diff'} = &checkinpath('diff'); $cmd = "$::path{diff} -r -q "; $cmd .= ". $::device"; } else { die("$cfg::type not capable of comparing files"); } &run_or_echo_then_query($cmd); } ###################################################################### # Interactive restore ###################################################################### sub restore_routine { my $cmd = &setup_before_read('restore'); if ($cfg::type eq 'dump') { $cmd .= "$::path{restore} -i "; $cmd .= "$::dump_verb_flag "; $cmd .= "$::dump_blk_flag "; $cmd .= "-f -"; } else { die("Interactive restore for $cfg::type not implemented"); } &run_or_echo_then_query($cmd); } ###################################################################### # Return the "label" name of the filesystem/dir ###################################################################### sub get_label { my $path = shift(@_); my $host = ''; my $label; if ($path =~ s/(\S+)://) { $host = $1 . "-"; $label = $path; } else { $label = $path; } $label =~ s%^/%%; # nuke leading slash $label =~ s%/%-%g; # turn / into - $label = 'root' if ($label eq ''); return($host . $label); } ###################################################################### # Return a date string of the timestamp file # from the last dump of lower level # in YYYYMMDDhhmm.ss format if arg 'numeric' # in ctime format if if arg 'ctime' # timestamp reference file if arg 'filename' ###################################################################### sub get_last_date { my $label = shift(@_); my $thislevel = shift(@_); my $format = shift(@_); my $lastlevel; my $targetfile = ''; my $numeric_val; my $string_val; my $mtime; # use the epoch for level 0 if ($thislevel == 0) { $numeric_val = '197001010000.00'; $string_val = "Thu Jan 01 00:00:00 1970"; } else { # Find last stamp file opendir(DIR,"$cfg::stampdir") or die("Can't open $cfg::stampdir: $OS_ERROR"); close(DIR); my $tmp = $thislevel - 1; foreach my $lev (reverse (0..$tmp)) { my $file = "$cfg::stampdir/$cfg::sprefix" . "$label.$lev"; if (-e "$file") { $lastlevel = $lev; $targetfile = $file; last; } } # get date from targetfile # or complain if no timestamp if ($targetfile ne '') { $mtime = (stat($targetfile))[9]; $string_val = strftime("%a %b %d %H:%M:%S %Y", localtime($mtime)); $numeric_val = strftime("%Y%m%d%H%M.%S", localtime($mtime)); } else { die("Can't do a level $thislevel backup - no level 0 timestamp found"); } } &log("| Date of this level $thislevel backup: $::date_at_start"); if ($thislevel == 0) { &log("| Date of last level $thislevel backup: the epoch"); } else { &log("| Date of last level $lastlevel backup: $string_val"); } &line(); if (!defined($format)) { $format = 'ctime'; } if ($format eq 'numeric') { return($numeric_val); } elsif ($format eq 'ctime') { return($string_val); } elsif ($format eq 'filename') { return($targetfile); } else { return($string_val); } } ###################################################################### # Echo message to screen and log # optionally just one or the other ###################################################################### sub log { my $msg = shift(@_); my $only = shift(@_); my $do_screen = 1; my $do_log = 1; if (!defined($only)) { $do_screen = 1; $do_log = 1; } elsif ($only eq 'screen') { $do_screen = 1; $do_log = 0; } elsif ($only eq 'log') { $do_screen = 0; $do_log = 1; } if ($do_screen == 1) { print $::msg "$msg\n"; } if (($do_log == 1) and defined($::log)) { open(LOG,">>$::log") || warn("can't open logfile"); print LOG "$msg\n"; close(LOG); } } ###################################################################### # Echo a line to both screen and log # optionally just one or the other ###################################################################### sub line { my $only = shift(@_); my $do_screen = 1; my $do_log = 1; my $length = 60; if (!defined($only)) { $do_screen = 1; $do_log = 1; } elsif ($only eq 'screen') { $do_screen = 1; $do_log = 0; } elsif ($only eq 'log') { $do_screen = 0; $do_log = 1; } if ($do_screen == 1) { print $::msg '|'; print $::msg '-' x $length; print $::msg "\n"; } if (($do_log == 1) and defined($::log)) { open(LOG,">>$::log") || warn("can't open logfile"); print LOG '|'; print LOG '-' x $length; print LOG "\n"; close(LOG); } } ###################################################################### # Read configuration file ###################################################################### sub readconfigfile { my $configfile; my $var; my $value; my $defines = $::opt{'d'}; if (defined($::opt{'c'})) { $configfile = $::opt{'c'}; } else { $configfile = $::CONFFILE; } if (! -r "$configfile") { die("config file $configfile: $OS_ERROR"); } system("perl -c \"$configfile\""); if ($CHILD_ERROR) { die("syntax error in config file $configfile"); } package cfg; require "$configfile"; package main; # Overrides foreach $var (keys %$defines) { $value = $$defines{$var}; &log("(override) $var = $value"); eval("\$cfg::$var=\"$value\""); } } ###################################################################### # Do a tape operation ###################################################################### sub mt { my (@operations) = (@_); # Set hardware compression when we do the blocksize if ($cfg::compress eq "hardware") { foreach my $operation (@operations) { if ($operation =~ m/generic-blocksize/) { if ($::uname =~ /Linux/) { push(@operations,'compression 1'); } elsif ($::uname =~ /FreeBSD/) { push(@operations,'comp on'); } else { push(@operations,'compression 1'); } } } } # We want 1-filemark behavior always # Set if currently doing blocksize command foreach my $operation (@operations) { if ($operation =~ m/generic-blocksize/) { if ($::uname =~ /FreeBSD/) { push(@operations,'seteotmodel 1'); } } } foreach my $operation (@operations) { # mt flavors for block number if ($operation eq 'generic-query') { if ($::uname =~ /Linux/) { $operation = 'tell'; if ($::ftape == 1) { $operation = 'getsize'; } } elsif ($::uname =~ /OpenBSD/) { $operation = 'status'; } elsif ($::uname =~ /FreeBSD/) { $operation = 'rdhpos'; } elsif ($::uname =~ /OSF1/) { $operation = 'status'; } elsif ($::uname =~ /AIX/) { $operation = 'status'; } elsif ($::uname =~ /HP-UX/) { $operation = 'status'; } elsif ($::uname =~ /SunOS/) { $operation = 'status'; } elsif ($::uname =~ /IRIX/) { $operation = 'status'; } else { $operation = 'status'; } } # mt flavors for eod if ($operation eq 'generic-eod') { if ($::uname =~ /Linux/) { $operation = 'eod'; if ($::ftape == 1) { $operation = 'eom'; } } elsif ($::uname =~ /OpenBSD/) { $operation = 'eod'; } elsif ($::uname =~ /FreeBSD/) { $operation = 'eod'; } elsif ($::uname =~ /OSF1/) { $operation = 'seod'; } elsif ($::uname =~ /AIX/) { $operation = 'fsf 1000'; } elsif ($::uname =~ /HP-UX/) { $operation = 'eod'; } elsif ($::uname =~ /SunOS/) { $operation = 'eom'; } elsif ($::uname =~ /IRIX/) { $operation = 'eod'; } else { $operation = 'eod'; } } # mt flavors for erase # (some mt's have no "erase", just rewind before starting...) if ($operation eq 'generic-erase') { if ($cfg::erase_rewind_only eq "true") { $operation = 'rewind'; } elsif ($::uname =~ /Linux/) { $operation = 'erase'; } elsif ($::uname =~ /OpenBSD/) { $operation = 'erase'; } elsif ($::uname =~ /FreeBSD/) { $operation = 'erase'; } elsif ($::uname =~ /OSF1/) { $operation = 'erase'; } elsif ($::uname =~ /AIX/) { $operation = 'erase'; } elsif ($::uname =~ /HP-UX/) { $operation = 'erase'; } elsif ($::uname =~ /SunOS/) { $operation = 'erase'; } elsif ($::uname =~ /IRIX/) { $operation = 'erase'; } else { $operation = 'erase'; } } # mt flavors for setblk if ($operation =~ /generic-blocksize/) { if ($::uname =~ /Linux/) { $operation =~ s/generic-blocksize/setblk/; } elsif ($::uname =~ /OpenBSD/) { $operation =~ s/generic-blocksize/blocksize/; } elsif ($::uname =~ /FreeBSD/) { $operation =~ s/generic-blocksize/blocksize/; } elsif ($::uname =~ /OSF1/) { $operation =~ s/generic-blocksize/setblk/; } elsif ($::uname =~ /AIX/) { $operation =~ s/generic-blocksize/setblk/; } elsif ($::uname =~ /HP-UX/) { $operation =~ s/generic-blocksize/setblk/; } elsif ($::uname =~ /SunOS/) { $operation =~ s/generic-blocksize/setblk/; } elsif ($::uname =~ /IRIX/) { $operation =~ s/generic-blocksize/setblksz/; } else { $operation =~ s/generic-blocksize/setblk/; } } if (defined($::use_file)) { # mt ops skipped for files } elsif (defined($::use_blockdevice)) { # mt ops skipped for block device } else { my $command; # Override mt operation so user can set for unknown flavors # or for debugging info, like mt tell -> mt status if(defined($cfg::mt{$operation})) { $operation = $cfg::mt{$operation}; next if ($operation eq 'nop'); } if ($operation =~ /setblk/) { # Try and see which of setblk/defblksize will work # This is kludgy, but doable $command = "$::path{mt} -f $::device $operation > /dev/null 2>&1"; if (defined($::remotetapehost)) { $command = &maybe_remote_cmd($command, $::remotetapehost); } if (defined($::debug)) { &log("(debug) $command"); } system($command); if ($CHILD_ERROR) { &log("| Trying \"mt defblksize\" instead of \"mt setblk\""); my $oldoperation = $operation; $operation =~ s/setblk/defblksize/; $command = "$::path{mt} -f $::device $operation > /dev/null 2>&1"; if (defined($::remotetapehost)) { $command = &maybe_remote_cmd($command, $::remotetapehost); } if (defined($::debug)) { &log("(debug) $command"); } system($command); if ($CHILD_ERROR) { &log("Error setting block size"); &log("Neither of these commands worked:"); &log(" $::path{mt} -f $::device $oldoperation"); &log(" $::path{mt} -f $::device $operation"); exit(1); } # error on second guess } # error on first guess } # operation = setblk $command = "$::path{mt} -f $::device $operation 2>&1 "; if (defined($::remotetapehost)) { $command = &maybe_remote_cmd($command, $::remotetapehost); } if (!defined($::debug)) { open(CMD,"($command) 2>&1 |") || die; if (defined($::log)) { open(LOG,">>$::log") || die; } while() { print $_; if (defined($::log)) { print LOG $_; } } close(CMD); if (defined($::log)) { close(LOG); } } else { &log("(debug) $command"); } } # not a file } # foreach operation } ###################################################################### # Option error checking & init stuff ###################################################################### sub optioncheck { my $buffer_blk_flag; my $buffer_write_pad_flag; my $buffer_read_pad_flag; my $mbuffer_blk_flag; my $mbuffer_write_pad_flag; my $mbuffer_read_pad_flag; # Archive type on commandline if (defined($::opt{'type'})) { $cfg::type = $::opt{'type'}; } # Compress flag on commandline if (defined($::opt{'compress'})) { $cfg::compress = $::opt{'compress'}; } # Device flag on commandline if (defined($::opt{'device'})) { $cfg::device = $::opt{'device'}; if (defined($::opt{'stdout'})) { push(@::errors,"Can't use -device and -pipe at the same time"); } } # Debug if (defined($::opt{'n'})) { $::debug = 1; } # Flag old config file if (defined(@cfg::filesystems) or defined($cfg::mt_var_blksize)) { # so strict shuts up my $junk = @cfg::filesystems; $junk = $cfg::mt_var_blksize; push(@::errors,"You've got an old 1.0.x configuration file, please update it!"); } # Mode my (@modelist) = qw(set dir list extract compare restore toc newtape rmindex rmfile test-tape-drive); my @modes; my $modecount = 0; $::mode = ''; foreach my $mode (@modelist) { if (defined($::opt{$mode})) { $modecount++; $::mode = $mode; push(@modes,$mode); } } if ($modecount > 1) { $_ = join(" -",@modes); push(@::errors,"Can't specify more than one mode (given \"-$_\")"); } if ($modecount == 0) { push(@::errors,"Nothing to do (see -help)"); } # First check if things are defined in the config file # Checks exist, true/false, or one of options &checkvar(\$cfg::type,'type','dump afio cpio tar star pax zip ar shar lha copy rsync filelist','tar'); &checkvar(\$cfg::compress,'compress','gzip bzip2 lzop compress zip false hardware','gzip'); &checkvar(\$cfg::compr_level,'compr_level','exist','4'); &checkvar(\$cfg::verbose,'verbose','bool','true'); &checkvar(\$cfg::sparse,'sparse','bool','true'); &checkvar(\$cfg::label,'label','bool','true'); &checkvar(\$cfg::atime_preserve,'atime_preserve','bool','false'); &checkvar(\$cfg::indexes,'indexes','bool','true'); &checkvar(\$cfg::staticfiles,'staticfiles','bool','false'); &checkvar(\$cfg::buffer,'buffer','false buffer mbuffer','false'); &checkvar(\$cfg::pad_blocks,'pad_blocks','bool','true'); &checkvar(\$cfg::device,'device','exist','/dev/tape'); &checkvar(\$cfg::remoteshell,'remoteshell','ssh ssh2 ssh1 rsh','ssh'); &checkvar(\$cfg::remoteuser,'remoteuser','exist',''); &checkvar(\$cfg::erase_tape_set_level_zero,'erase_tape_set_level_zero','bool','true'); &checkvar(\$cfg::erase_rewind_only,'erase_rewind_only','bool','false'); &checkvar(\$cfg::logdir,'logdir','exist','/var/log/flexbackup'); &checkvar(\$cfg::tmpdir,'tmpdir','exist','/tmp'); &checkvar(\$cfg::comp_log,'comp_log','gzip bzip2 lzop compress zip false','gzip'); &checkvar(\$cfg::stampdir,'stampdir','exist','/var/lib/flexbackup'); &checkvar(\$cfg::index,'index','exist','/var/lib/flexbackup/index'); &checkvar(\$cfg::keyfile,'keyfile','exist','00-index-key'); &checkvar(\$cfg::staticlogs,'staticlogs','bool','false'); &checkvar(\$cfg::prefix,'prefix','exist',''); &checkvar(\$cfg::sprefix,'sprefix','exist',''); if (@::errors) { print $::msg "Errors:\n"; while(@::errors) { print $::msg " " . shift(@::errors) . "\n"; } exit(1); } # Check we can find rsh or ssh $::path{$cfg::remoteshell} = &checkinpath($cfg::remoteshell); if ($cfg::remoteuser ne '') { $::remoteshell = "$::path{$cfg::remoteshell} -l $cfg::remoteuser"; } else { $::remoteshell = $::path{$cfg::remoteshell}; } # Check we can find common stuff $::path{'touch'} = &checkinpath('touch'); $::path{'hostname'} = &checkinpath('hostname'); $::path{'cat'} = &checkinpath('cat'); $::path{'rm'} = &checkinpath('rm'); $::path{'tee'} = &checkinpath('tee'); $::path{'find'} = &checkinpath('find'); $::path{'dd'} = &checkinpath('dd'); $::path{'printf'} = &checkinpath('printf'); push(@::remoteprogs,($::path{'touch'},$::path{'rm'},$::path{'find'},$::path{'printf'})); # Check device (or dir) $::ftape = 0; if (defined($::opt{'pipe'})) { # Dump to stdout. # Disable indexing, all messages to stderr $::use_file = 1; $::use_pipe = 1; $cfg::indexes = 'false'; $cfg::device = '-'; } elsif ($cfg::type eq 'filelist') { $::use_file = 1; chomp($cfg::device = `pwd`); $cfg::device =~ s:/$::; $cfg::indexes = 'false'; # Can we write to cwd? if (! -w $cfg::device) { push(@::errors,"Can't write to $cfg::device"); } } else { # Chase device links my $realdev = $cfg::device; while (-l "$realdev") { my @pathname = split('/',$realdev); $realdev = readlink("$realdev"); # If a relative link we'll need the dir from the link if ($realdev !~ m:^/:) { pop(@pathname); $realdev = join('/',@pathname) . "/$realdev"; } } if (-c $realdev) { # Check for ftape driver if ($realdev =~ /n?z?[qr]ft(\d+)/) { $::ftape = 1; } $::tapedevice = 1; } elsif (-b $realdev) { # In case of floppy or similar. # Can't do multiple files this way; turn indexing off $::use_blockdevice = 1; $cfg::indexes = 'false'; } elsif (-d "$cfg::device") { if ($cfg::device !~ m:^/:) { push(@::errors,"Please give full path, not relative (\$device=$cfg::device)"); } else { $::use_file = 1; $cfg::device =~ s:/$::; # nuke trailing slash if any } } elsif ($cfg::device =~ m%(\S+):(/dev/.*)%) { $::remotetapehost = $1; $cfg::device = $2; $::tapedevice = 1; } else { push(@::errors,"\$device must be set to a directory, a local device, or a remote device"); } # Can we write to it? if ((! -w $cfg::device) and !defined($::remotetapehost) and ($::mode =~ m/^(set|dir|newtape)$/)) { push(@::errors,"Can't write to $cfg::device"); } } $::device = $cfg::device; # Set mt type if (defined($::tapedevice)) { if ($::ftape == 1) { $::path{'mt'} = &checkinpath('ftmt'); } else { $::path{'mt'} = &checkinpath('mt'); } } # Exclude regexp for find $::exclude_expr = ''; if (defined($cfg::exclude_expr[0])) { my @excl_array; my $expr; foreach $expr (@cfg::exclude_expr) { # People just don't grok regex's. # # If the first character is a *, they obviously got it wrong, # we can try to assume what they meant. # # If the user put "*.whatever" as an expression, turn this # "glob" into a regex for them # If the user put "*whatever" as an expression, turn this # "glob" into a regex for them if ($expr =~ m/^\*\./) { $expr =~ s/^\*\./.\*\\./; } if ($expr =~ m/^\*/) { $expr =~ s/^\*/.*/; } # AAAH! Csh should be banned from the face of the earth! # # If an expression contains $ at the end we need to be careful # and leave it out of the quotes, or csh will yack if doing a # remote backup. This happens only if the user's shell is # csh/tcsh. Then the string is doublequoted inside single # quotes and there is _no way_ for csh do deal with $ in that # situation. This took a LONG time to figure out. if ($expr =~ m/^(.+)\$$/) { $expr = '"' . $1 . '"' . '$'; #' (comment to fool emacs 20.7 } else { $expr = '"' . $expr . '"'; } $::exclude_expr .= "! -regex $expr "; } } # Traverse mountpoints? &checkvar(\$cfg::traverse_fs,'traverse_fs','false local all','false'); if ($cfg::traverse_fs eq "local") { $::mountpoint_flag = "! -fstype nfs ! -fstype smbfs ! -fstype bind ! -fstype proc ! -fstype devpts ! -fstype devfs ! -fstype tmpfs"; } elsif ($cfg::traverse_fs eq "all") { $::mountpoint_flag = "! -fstype proc ! -fstype devpts ! -fstype devfs ! -fstype tmpfs"; } else { $::mountpoint_flag = "-xdev"; } # Block size &checkvar(\$cfg::blksize,'blksize','exist','10'); # Isn't required; if commented out in config we use same as $blksize #&checkvar(\$cfg::mt_blksize,'mt_blksize','exist'); if ($cfg::blksize !~ m/^\d+$/) { push(@::errors,"\$blksize must be set to an integer"); } if ($cfg::blksize ne '0') { # buffer blocksize needs k appended $buffer_blk_flag = "-s " . $cfg::blksize . "k"; # mbuffer blocksize in bytes $mbuffer_blk_flag = "-s " . $cfg::blksize * 1024; # dd blocksize needs k appended $::dd_blk_flag = "ibs=" . $cfg::blksize . "k obs=" . $cfg::blksize . "k"; # dump blocksize just in k like the config file $::dump_blk_flag = "-b $cfg::blksize"; # afio blocksize needs k appended $::afio_blk_flag = "-b " . $cfg::blksize . "k"; # cpio blocks are in bytes $::cpio_blk_flag = "-C " . $cfg::blksize * 1024; # tar blocks are in 512-byte units # long name is really --blocking-factor but changed from --block-size # only in recent versions. just use the short flag. $::tar_blk_flag = "-b " . $cfg::blksize * 2; # star blocks are in 512-byte units $::star_blk_flag = "blocks=" . $cfg::blksize * 2; # pax blocksize needs k appended $::pax_blk_flag = "-b " . $cfg::blksize . "k"; } else { $buffer_blk_flag = ""; $mbuffer_blk_flag = ""; $::dd_blk_flag = ""; $::dump_blk_flag = ""; $::afio_blk_flag = ""; $::cpio_blk_flag = ""; $::tar_blk_flag = ""; $::star_blk_flag = ""; $::pax_blk_flag = ""; } # mt block size (in bytes not k) if (!defined($cfg::mt_blksize)) { $cfg::mt_blksize = $cfg::blksize * 1024; $::mt_blksize = $cfg::mt_blksize; } if ($cfg::mt_blksize !~ m/^\d+$/) { push(@::errors,"\$mt_blksize must be set to an integer"); } else { if ($cfg::mt_blksize != 0) { my $tmp = $cfg::blksize * 1024; if ($tmp%$cfg::mt_blksize != 0) { push(@::errors,"\$mt_blksize ($cfg::mt_blksize) should be a factor of \$blksize ($tmp)"); } } $::mt_blksize = $cfg::mt_blksize; } # Generic compression (afio archives will do their own flags) if ($cfg::compress eq "gzip") { $::path{'gzip'} = &checkinpath($cfg::compress); push(@::remoteprogs, $::path{$cfg::compress}); if ($cfg::compr_level !~ m/^[123456789]$/) { push(@::errors,"\$compr_level must be set to 1-9"); } else { $::z = " | $::path{$cfg::compress} -$cfg::compr_level"; } $::unz = "$::path{$cfg::compress} -dq | "; } elsif ($cfg::compress eq "bzip2") { $::path{'bzip2'} = &checkinpath($cfg::compress); push(@::remoteprogs, $::path{$cfg::compress}); if ($cfg::compr_level !~ m/^[123456789]$/) { push(@::errors,"\$compr_level must be set to 1-9"); } else { $::z = " | $::path{$cfg::compress} -$cfg::compr_level"; } $::unz = "$::path{$cfg::compress} -d | "; } elsif ($cfg::compress eq "lzop") { $::path{'lzop'} = &checkinpath($cfg::compress); push(@::remoteprogs, $::path{$cfg::compress}); if ($cfg::compr_level !~ m/^[123456789]$/) { push(@::errors,"\$compr_level must be set to 1-9"); } else { $::z = " | $::path{$cfg::compress} -$cfg::compr_level"; } $::unz = "$::path{$cfg::compress} -d | "; } elsif ($cfg::compress eq "compress") { $::path{'compress'} = &checkinpath($cfg::compress); push(@::remoteprogs, $::path{$cfg::compress}); $::z = " | $::path{$cfg::compress} -c"; $::unz = "$::path{$cfg::compress} -dc | "; } elsif ($cfg::compress eq "zip") { $::path{'zip'} = &checkinpath('zip'); push(@::remoteprogs, $::path{'zip'}); $::path{'funzip'} = &checkinpath('funzip'); if ($cfg::compr_level !~ m/^[123456789]$/) { push(@::errors,"\$compr_level must be set to 1-9"); } else { $::z = " | $::path{zip} -$cfg::compr_level - -"; $::unz = "$::path{funzip} | "; } } else { $::z = ""; $::unz = ""; } # Block padding if (($cfg::pad_blocks eq "true") and defined($::tapedevice)) { $::dd_write_pad_flag = "conv=noerror,sync"; $::dd_read_pad_flag = "conv=noerror"; $buffer_write_pad_flag = "-B"; $buffer_read_pad_flag = ""; $mbuffer_write_pad_flag = ""; $mbuffer_read_pad_flag = ""; } else { $::dd_write_pad_flag = "conv=noerror"; $::dd_read_pad_flag = "conv=noerror"; $buffer_write_pad_flag = ""; $buffer_read_pad_flag = ""; $mbuffer_write_pad_flag = ""; $mbuffer_read_pad_flag = ""; } # Buffer setup if ($cfg::buffer ne 'false') { &checkvar(\$cfg::buffer_megs,'buffer_megs','exist'); &checkvar(\$cfg::buffer_fill_pct,'buffer_fill_pct','exist','75'); &checkvar(\$cfg::buffer_pause_usec,'buffer_pause_usec','exist','100'); if ($cfg::buffer_megs !~ m/^\d+$/) { push(@::errors,"\$buffer_megs must be set to integer number of megabytes"); } if ($cfg::buffer_fill_pct !~ m/^\d+$/) { push(@::errors,"\$buffer_fill_pct must be set to an integer"); } if ($cfg::buffer_pause_usec !~ m/^\d+$/) { push(@::errors,"\$buffer_pause_usec must be set to an integer"); } if ($cfg::buffer eq "buffer") { $::path{'buffer'} = &checkinpath('buffer'); push(@::remoteprogs, $::path{'buffer'}); my $write_flags; my $read_flags; my $megs = $cfg::buffer_megs . "m"; my $bufcmd = "$::path{buffer} -m $megs -p $cfg::buffer_fill_pct $buffer_blk_flag -t "; if (defined($::tapedevice)) { $write_flags = "-u $cfg::buffer_pause_usec $buffer_write_pad_flag -o "; $read_flags = "-u $cfg::buffer_pause_usec $buffer_read_pad_flag -i "; } else { $write_flags = "$buffer_write_pad_flag -o "; $read_flags = "$buffer_read_pad_flag -i "; } $::buffer_cmd = " | $bufcmd"; $::write_cmd = "$bufcmd $write_flags"; $::read_cmd = "$bufcmd $read_flags"; } elsif ($cfg::buffer eq "mbuffer") { $::path{'mbuffer'} = &checkinpath('mbuffer'); push(@::remoteprogs, $::path{'mbuffer'}); my $megs = $cfg::buffer_megs . "M"; my $bufcmd = "$::path{mbuffer} -q -m $megs -p $cfg::buffer_fill_pct $mbuffer_blk_flag "; $::buffer_cmd = " | $bufcmd"; $::write_cmd = "$bufcmd -f -o "; if (defined($::opt{'volumes'})) { $::read_cmd = "$bufcmd -f -n $::opt{volumes} -i "; } else { $::read_cmd = "$bufcmd -f -i "; } } } else { # If buffering disabled, use dd or cat depending on if blocking turned off on not if ($cfg::blksize eq '0') { $::buffer_cmd = ""; $::write_cmd = "$::path{cat} > "; $::read_cmd = "$::path{cat} "; } else { $::buffer_cmd = ""; $::write_cmd = "$::path{dd} $::dd_blk_flag $::dd_write_pad_flag of="; $::read_cmd = "$::path{dd} $::dd_blk_flag $::dd_read_pad_flag if="; } } # Sets / filesystems if (defined($::opt{'dir'})) { # Single directory if ($::opt{'dir'} =~ /^(\S+):/) { $::remotehosts{$1} = 1; } else { $::local = 1; } # Get rid of trailing / $::opt{'dir'} = &nuke_trailing_slash($::opt{'dir'}); } elsif (defined($::opt{'set'})) { if (defined($::use_pipe)) { push(@::errors,"can't use -set with -pipe option"); } foreach my $set (keys %cfg::set) { if ($set eq 'all') { push(@::errors,"can't define a set named 'all'"); } } my @do_sets; if ($::opt{'set'} eq 'all') { @do_sets = keys(%cfg::set); if (scalar(@do_sets) == 0) { push(@::errors,"no backup sets defined"); } } else { @do_sets = ($::opt{'set'}); } foreach my $this_set (@do_sets) { if (!defined($cfg::set{$this_set})) { push(@::errors,"set $this_set is not defined"); } else { foreach my $dir (&split_list($cfg::set{$this_set})) { if ($dir =~ /^(\S+):/g) { $::remotehosts{$1} = 1; } else { $::local = 1; } } } } } # Subtree pruning foreach my $fs (keys %cfg::prune) { $fs = &nuke_trailing_slash($fs); foreach my $expr (&split_list($cfg::prune{$fs})) { $::prune{$fs}{$expr} = 1; } } # Verbose flag if ($cfg::verbose eq "true") { $::dump_verb_flag = "-v"; $::afio_verb_flag = "-v"; $::cpio_verb_flag = "-v"; $::tar_verb_flag = "--verbose"; $::star_verb_flag = "-v"; $::pax_verb_flag = "-v"; $::zip_verb_flag = "-v"; $::ar_verb_flag = "v"; $::shar_verb_flag = ""; $::lha_verb_flag = ""; $::rsync_verb_flag = "--verbose"; } else { $::dump_verb_flag = ""; $::afio_verb_flag = ""; $::cpio_verb_flag = ""; $::tar_verb_flag = ""; $::star_verb_flag = "-silent"; $::pax_verb_flag = ""; $::zip_verb_flag = "-q"; $::ar_verb_flag = ""; $::shar_verb_flag = "-q"; $::lha_verb_flag = "q"; $::rsync_verb_flag = ""; } # Sparse flag if ($cfg::sparse eq "true") { $::afio_sparse_flag = ""; $::cpio_sparse_flag = ""; $::tar_sparse_flag = "--sparse"; $::star_sparse_flag = "-sparse"; } else { $::afio_sparse_flag = "-j"; $::cpio_sparse_flag = ""; $::tar_sparse_flag = ""; $::star_sparse_flag = ""; } # atime preserve flag if ($cfg::atime_preserve eq "true") { $::afio_atime_flag = "-a"; $::tar_atime_flag = "--atime-preserve"; $::star_atime_flag = "-atime"; } else { $::afio_atime_flag = ""; $::tar_atime_flag = ""; $::star_atime_flag = ""; } # Type-specific setup if ($cfg::type eq 'dump') { &checkvar(\$cfg::dump_length,'dump_length','exist','0'); &checkvar(\$cfg::dump_use_dumpdates,'dump_use_dumpdates','bool','false'); $::path{'dump'} = &checkinpath('dump'); $::path{'restore'} = &checkinpath('restore'); push(@::remoteprogs, $::path{'dump'}); # Length of tape if ($cfg::dump_length !~ m/^\d+$/) { push(@::errors,"\$dump_length must be set to integer number of kilobytes"); } # If length set to 0 will will try autosize if ($cfg::dump_length == 0) { $::dump_len_flag = "-a"; } else { $::dump_len_flag = "-B $cfg::dump_length"; } } elsif ($cfg::type eq 'afio') { &checkvar(\$cfg::afio_echo_block,'afio_echo_block','bool','false'); &checkvar(\$cfg::afio_compress_cache_size,'afio_compress_cache_size','exist','2'); &checkvar(\$cfg::afio_compress_threshold,'afio_compress_threshold','exist','3'); &checkvar(\$cfg::afio_nocompress_types,'afio_nocompress_types','exist','mp3 MP3 Z z gz gif zip ZIP lha jpeg jpg JPG taz tgz deb rpm bz2 lzo'); $::path{'afio'} = &checkinpath('afio'); push(@::remoteprogs, $::path{'afio'}); # Compress flag for afio must be handled differently if ($cfg::compress =~ m/^(gzip|bzip2|lzop|compress|zip)$/) { if ($cfg::compress eq "gzip") { $::afio_z_flag = "-P $::path{$cfg::compress} -Q -$cfg::compr_level -Z"; $::afio_unz_flag = "-P $::path{$cfg::compress} -Q -d -Q -q -Z"; } elsif ($cfg::compress eq "bzip2") { $::afio_z_flag = "-P $::path{$cfg::compress} -Q -$cfg::compr_level -Z"; $::afio_unz_flag = "-P $::path{$cfg::compress} -Q -d -Z"; } elsif ($cfg::compress eq "lzop") { $::afio_z_flag = "-P $::path{$cfg::compress} -Q -$cfg::compr_level -Z"; $::afio_unz_flag = "-P $::path{$cfg::compress} -Q -d -Z"; } elsif ($cfg::compress eq "zip") { $::afio_z_flag = "-P $::path{zip} -Q -$cfg::compr_level -Q - -Q - -Z"; $::afio_unz_flag = "-P $::path{funzip} -Q \"\" -Z"; } elsif ($cfg::compress eq "compress") { $::afio_z_flag = "-P $::path{$cfg::compress} -Q -c -Z"; $::afio_unz_flag = "-P $::path{$cfg::compress} -Q -d -Q -c -Z"; } $::unz = ""; # Reset & just use this for reading the archive file. # Compression cache size if ($cfg::afio_compress_cache_size !~ m/^\d+$/) { push(@::errors,"\$afio_compress_cache_size must be set to an integer"); } else { if ($cfg::afio_compress_cache_size != 0) { $::afio_z_flag .= " -M " . $cfg::afio_compress_cache_size . "m"; } } # Compression threshold if ($cfg::afio_compress_threshold !~ m/^\d+$/) { push(@::errors,"\$afio_compress_threshold must be set to an integer"); } else { if ($cfg::afio_compress_threshold != 0) { $::afio_z_flag .= " -T " . $cfg::afio_compress_threshold . "k"; } } } else { $::afio_z_flag = ""; $::afio_unz_flag = ""; } # Echo block number $::afio_bnum_flag = ""; if ($cfg::verbose eq "true") { if ($cfg::afio_echo_block eq "true") { $::afio_bnum_flag = "-B"; } } } elsif (($cfg::type eq 'cpio') or ($cfg::type eq 'copy')) { &checkvar(\$cfg::cpio_format,'cpio_format','bin odc newc crc tar ustar hpbin hpodc','newc'); $::path{'cpio'} = &checkinpath('cpio'); push(@::remoteprogs, $::path{'cpio'}); if ($cfg::type eq 'copy') { if (!defined($::use_file)) { push(@::errors,"Can't use type \"copy\" unless archiving to disk!"); } if (defined($::use_pipe)) { push(@::errors,"Can't use type \"copy\" with -pipe!"); } } } elsif ($cfg::type eq 'rsync') { $::path{'rsync'} = &checkinpath('rsync'); $::path{'sed'} = &checkinpath('sed'); push(@::remoteprogs, $::path{'rsync'}); if (!defined($::use_file)) { push(@::errors,"Can't use type \"rsync\" unless archiving to disk!"); } if (defined($::use_pipe)) { push(@::errors,"Can't use type \"rsync\" with -pipe!"); } } elsif ($cfg::type eq 'tar') { &checkvar(\$cfg::tar_echo_record_num,'tar_echo_record_num','bool','false'); $::path{'tar'} = &checkinpath('tar'); push(@::remoteprogs, $::path{'tar'}); # Echo record number $::tar_recnum_flag = ""; if ($cfg::verbose eq "true") { if ($cfg::tar_echo_record_num eq "true") { $::tar_recnum_flag = "-R"; } } } elsif ($cfg::type eq 'star') { &checkvar(\$cfg::star_acl,'star_acl','bool','true'); &checkvar(\$cfg::star_fifo,'star_fifo','bool','true'); &checkvar(\$cfg::star_format,'star_format','tar star gnutar ustar pax xstar xustar exustar suntar','exustar'); &checkvar(\$cfg::star_echo_block_num,'star_echo_block_num','bool','false'); $::path{'star'} = &checkinpath('star'); push(@::remoteprogs, $::path{'star'}); # Echo block number $::star_blocknum_flag = ""; if ($cfg::verbose eq "true") { if ($cfg::star_echo_block_num eq "true") { $::star_blocknum_flag = "-block-number"; } } # ACL flag if ($cfg::star_acl eq "true") { $::star_acl_flag = "-acl"; } else { $::star_acl_flag = ""; } # fifo if ($cfg::star_fifo eq "true") { $::star_fifo_flag = "-fifo"; if ($cfg::verbose eq "true") { $::star_fifo_flag .= " -fifostats"; } } else { $::star_fifo_flag = ""; } } elsif ($cfg::type eq 'pax') { &checkvar(\$cfg::pax_format,'pax_format','cpio bcpio sv4cpio sv4crc tar ustar'); $::path{'pax'} = &checkinpath('pax'); push(@::remoteprogs, $::path{'pax'}); } elsif ($cfg::type eq 'zip') { &checkvar(\$cfg::zip_nocompress_types,'zip_nocompress_types','exist','mp3 MP3 Z z gz gif zip ZIP lha jpeg jpg JPG taz tgz deb rpm bz2 lzo'); $::path{'zip'} = &checkinpath('zip'); push(@::remoteprogs, $::path{'zip'}); $::path{'unzip'} = &checkinpath('unzip'); $::zip_compr_flag = "-$cfg::compr_level"; if ($cfg::compress =~ /^(gzip|bzip2|lzop|compress|zip)$/) { warn("Using type \"zip\" with compress=$cfg::compress makes no sense"); warn("Setting compression to false"); $::unz = ""; $::z = ""; $cfg::compress = "false"; } $::zip_noz_flag = ""; if (defined($cfg::zip_nocompress_types) and $cfg::zip_nocompress_types ne "") { # Add dots to file extensions, make -n flag @_ = split(" ",$cfg::zip_nocompress_types); foreach (@_) { $_ = "." . $_; } $::zip_noz_flag = " -n " . join(":",@_); } } elsif ($cfg::type eq 'ar') { $::path{'ar'} = &checkinpath('ar'); push(@::remoteprogs, $::path{'ar'}); } elsif ($cfg::type eq 'shar') { $::path{'shar'} = &checkinpath('shar'); push(@::remoteprogs, $::path{'shar'}); } elsif ($cfg::type eq 'lha') { $::path{'lha'} = &checkinpath('lha'); push(@::remoteprogs, $::path{'lha'}); if ($cfg::compress =~ /^(gzip|bzip2|lzop|compress|zip)$/) { warn("Using type \"lha\" with compress=$cfg::compress makes no sense"); warn("Setting compression to false"); $::unz = ""; $::z = ""; $cfg::compress = "false"; } } elsif ($cfg::type eq 'filelist') { # Nothing specific to check } # type-specific # Tmp dir $cfg::tmpdir = &nuke_trailing_slash($cfg::tmpdir); if ($cfg::tmpdir !~ m:^/:) { push(@::errors,"\$tmpdir must be absolute path: $cfg::tmpdir"); } if (! -d "$cfg::tmpdir") { push(@::errors,"\$tmpdir $cfg::tmpdir is not a directory"); } if (! -w "$cfg::tmpdir") { push(@::errors,"\$tmpdir $cfg::tmpdir is not writable"); } # Levels if (defined($::opt{'level'}) and (defined($::opt{'incremental'}) or defined($::opt{'differential'}) or defined($::opt{'full'}))) { push(@::errors,"Can't use -level AND -incremental/-differential/-full"); } if (!defined($::opt{'level'})) { if (defined($::opt{'incremental'})) { $::opt{'level'} = 'incremental'; } elsif (defined($::opt{'differential'})) { $::opt{'level'} = 'differential'; } elsif (defined($::opt{'full'})) { $::opt{'level'} = 'full'; } else { $::opt{'level'} = 0; } } if (($::opt{'level'} !~ m/^\d+$/) and ($::opt{'level'} !~ m/^(full|differential|incremental)$/)) { push(@::errors,"-level must be numeric, or full/differential/incremental"); } # Check for digits or change full/diff to level number # Incremental + fs=all we have to handle later since it might be # different for each fs if ($::opt{'level'} =~ m/^\d+$/) { # Make string variable numeric $::level = POSIX::strtod($::opt{'level'}); if (($cfg::type eq 'dump') and ($::level > 9)) { push(@::errors,"can't use level > 9 and type=dump"); } } elsif ($::opt{'level'} eq "full") { $::level = 0; } elsif ($::opt{'level'} eq "differential") { $::level = 1; } elsif ($::opt{'level'} eq "incremental") { # If incremental + one fs, we can find the level now. if (defined($::opt{'dir'})) { $::level = &get_incremental_level($::opt{'dir'}); if (($cfg::type eq 'dump') and ($::level > 9)) { push(@::errors,"can't use level > 9 and type=dump"); } } else { # If we are doing a set have to postpone till later; each # fs might have a different level... undef $::level; $::set_incremental = 1; } } # Package delta option if (defined($::opt{'pkgdelta'})) { &checkvar(\$cfg::pkgdelta_archive_list,'pkgdelta_archive_list','true false rootonly','rootonly'); &checkvar(\$cfg::pkgdelta_archive_unowned,'pkgdelta_archive_unowned','bool','true'); &checkvar(\$cfg::pkgdelta_archive_changed,'pkgdelta_archive_changed','bool','true'); if ($::opt{'pkgdelta'} eq 'rpm') { $::pkgdelta = 'rpm'; $::path{'rpm'} = &checkinpath('rpm'); } elsif ($::opt{'pkgdelta'} =~ /freebsd/i) { $::pkgdelta = 'freebsd'; $::path{'pkg_info'} = &checkinpath('pkg_info'); } else { push(@::errors,"$::opt{pkgdelta} not a valid option for -pkgdelta"); } } # Check toc/rmindex/rmfile flags if (defined($::opt{'toc'}) or defined($::opt{'rmindex'})) { if ($cfg::indexes eq "false") { push(@::errors,"Can't do -toc/rmindex with \$indexes set to false"); } } if (defined($::opt{'rmindex'}) and (${$::opt{'rmindex'}}[0] eq '')) { push(@::errors,"-rmindex requires 'key:filenum', 'key' or 'all'"); } if (defined($::opt{'rmfile'}) and (${$::opt{'rmfile'}}[0] eq '')) { push(@::errors,"-rmfile requires a filename or 'all'"); } # Check log/stamp dirs (only if we are in a 'write' mode) if ($::mode =~ m/^(set|dir|newtape)$/) { $::path{$cfg::comp_log} = &checkinpath($cfg::comp_log) if ($cfg::comp_log ne "false"); $cfg::logdir = &nuke_trailing_slash($cfg::logdir); $cfg::stampdir = &nuke_trailing_slash($cfg::stampdir); if ($cfg::logdir !~ m:^/:) { push(@::errors,"\$logdir must be absolute path: $cfg::logdir"); } if ($cfg::stampdir !~ m:^/:) { push(@::errors,"\$stampdir must be absolute path: $cfg::stampdir"); } if (! -d "$cfg::logdir") { mkdir("$cfg::logdir",0755) or push(@::errors,"Can't mkdir $cfg::logdir: $OS_ERROR"); } if (! -w "$cfg::logdir") { push(@::errors,"Can't write to $cfg::logdir"); } if (! -d "$cfg::stampdir") { mkdir("$cfg::stampdir",0755) or push(@::errors,"Can't mkdir $cfg::stampdir: $OS_ERROR"); } if (! -w "$cfg::stampdir") { push(@::errors,"Can't write to $cfg::stampdir: $OS_ERROR"); } } # Tie index database if (($::mode !~ m/^(list|extract|restore|compare|test-tape-drive)$/) and ($cfg::indexes eq "true")) { tie(%::index,"AnyDBM_File",$cfg::index,O_CREAT|O_RDWR,0640) or push(@::errors,"Can't tie DB $cfg::index"); } # Sanity check some accessory tape flags if (($::mode =~ m/^(list|extract|restore|compare)$/) and defined($::opt{'erase'})) { push(@::errors,"-erase can't be used in -$::mode mode"); } if (($::mode =~ m/^(set|dir|newtape)$/) and defined($::opt{'num'})) { push(@::errors,"-num Can't be used in -$::mode mode"); } if (defined($::use_file) or defined($::use_blockdevice)) { if (defined($::opt{'num'})) { push(@::errors,"Can't use -num unless reading from tape"); } if (defined($::opt{'erase'}) or defined($::opt{'rewind'}) or defined($::opt{'reten'})) { push(@::errors,"Can't use -erase/-rewind/-reten unless using a tape"); } } # Testing if (defined($::debug)) { &log('(debug) no backup or mt commands will be executed'); &log('(debug) no old stamps or old log files will be removed'); } # Check extract list if (defined($::opt{'flist'})) { if (defined($::opt{'extract'})) { if (! -r $::opt{'flist'}) { push(@::errors,"list of files $::opt{flist} not readable: $OS_ERROR"); } } else { push(@::errors,"-flist can only be used with -extract"); } } if (defined($::opt{'onefile'}) and !defined($::opt{'extract'})) { push(@::errors,"-onefile can only be used with -extract"); } # Requirements for testing if (defined($::opt{'test-tape-drive'})) { if (defined($::use_file)) { push(@::errors,"No use trying tape drive tests on directories!"); } elsif (defined($::use_blockdevice)) { push(@::errors,"No use trying tape drive tests on block devices!"); } $::path{'diff'} = &checkinpath('diff'); $::path{'tr'} = &checkinpath('tr'); } if (@::errors) { print $::msg "\nErrors:\n"; while(@::errors) { print $::msg " " . shift(@::errors) . "\n"; } exit(1); } } ###################################################################### # Check buffer, shelltype, and any remote hosts for required programs ###################################################################### sub test_before_run { if ($cfg::buffer ne 'false') { &test_bufferprog($::buffer_cmd, 'localhost'); } &check_shell('localhost'); &check_remote_progs(\%::remotehosts, \@::remoteprogs); if (@::errors) { print $::msg "\nErrors:\n"; while(@::errors) { print $::msg " " . shift(@::errors) . "\n"; } exit(1); } } ###################################################################### # Print usage summary from the header ###################################################################### sub usage { open(FILE,"$0") or die "Can't open $0: $OS_ERROR"; while() { last if (m/^\#\s+USAGE:/); } while() { last if (m/^\#\#\#\#\#\#\#/); s/^\#//; print; } close(FILE); } ###################################################################### # Return version string from CVS tag ###################################################################### sub versionstring { my $ver = ' $Name: v1_2_1 $ '; $ver =~ s/Name//g; $ver =~ s/[:\$]//g; $ver =~ s/\s+//g; $ver =~ s/^v//g; $ver =~ s/_/\./g; if ($ver eq '') { $ver = "devel"; } return($ver . " (http://flexbackup.sourceforge.net)"); } ###################################################################### # Return current time in ctime format if normal # in YYYYMMDDHHMM.SS format if 'numeric' is given ###################################################################### sub current_time { my $format = shift(@_); my $string; my $current_time = time; if (defined($format) and ($format eq 'numeric')) { $string = strftime("%Y%m%d%H%M", localtime($current_time)); } elsif (defined($format) and ($format eq 'ctime')) { $string = strftime("%a %b %d %H:%M:%S %Y", localtime($current_time)); } else { $string = strftime("%a %b %d %H:%M:%S %Y", localtime($current_time)); } return($string); } ###################################################################### # Possibly return a filename to use # if running list/extract/compare/restore ###################################################################### sub maybe_get_filename { my @modes = qw(list extract compare restore); my $arg; my $file; my $ftype; # grab filename from option argument # optionscheck already guarantees only one is set foreach my $mode (@modes) { if (defined($::opt{$mode})) { $arg = $::opt{$mode}; } } # If reading from stdin if (defined($::use_pipe)) { # -pipe and file arg doesn't make sense, yell if ($arg ne '') { print STDERR "Error: when using -pipe, don't specify file name.\n"; die(); } else { # Set file to "-" for stdin return('-'); } } # If the flag given but null, and $device was not set to a dir, just return if (($arg eq '') and (!defined($::use_file))) { return($::device); } # If the flag given but null, and $device is a dir, spew if (($arg eq '') and (defined($::use_file))) { print STDERR "Error: when extracting from a file, you must specify file name.\n"; print STDERR "(like \"-list file.tar.bz2\")\n"; die(); } # Look for file in current dir first (or full path given) # Then in $device dir (if conf file set to backup to files) if (-f "$arg") { $file = $arg; $::use_file = 1; $cfg::device = $cfg::tmpdir; # Just so optioncheck doesn't assume tape undef $::tapedevice; undef $::remotetapehost; } elsif (defined($::use_file) and (-f "$cfg::device/$arg")) { $file = $cfg::device . "/" . $arg; $cfg::device = $cfg::tmpdir; # Just so optioncheck doesn't assume tape undef $::tapedevice; undef $::remotetapehost; } elsif (-d "$arg") { $file = $arg; $::use_file = 1; $cfg::device = $cfg::tmpdir; # Just so optioncheck doesn't assume tape undef $::tapedevice; undef $::remotetapehost; } elsif (defined($::use_file) and (-d "$cfg::device/$arg")) { $file = $cfg::device . "/" . $arg; $cfg::device = $cfg::tmpdir; # Just so optioncheck doesn't assume tape undef $::tapedevice; undef $::remotetapehost; } else { if (defined($::use_file)) { print STDERR "Error: file \"$arg\" or \"$cfg::device/$arg\" not found\n"; print STDERR "(like \"-list file.tar.bz2\")\n"; die(); } else { die("Error: file \"$arg\" not found"); } } # Try and guess file types and commpression scheme # might as well since we are reading from a file in this case if ($file =~ m/\.(dump|cpio|tar|star|pax|a|shar|filelist)\.(gz|bz2|lzo|Z|zip)$/) { $cfg::type = $1; $cfg::compress = $2; $cfg::type =~ s/^a$/ar/; $cfg::compress =~ s/gz/gzip/; $cfg::compress =~ s/bz2/bzip2/; $cfg::compress =~ s/lzo/lzop/; $cfg::compress =~ s/Z/compress/; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.afio-(gz|bz2|lzo|Z|zip)$/) { $cfg::type = "afio"; $cfg::compress = $1; $cfg::compress =~ s/gz/gzip/; $cfg::compress =~ s/bz2/bzip2/; $cfg::compress =~ s/lzo/lzop/; $cfg::compress =~ s/Z/compress/; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.(dump|afio|cpio|tar|star|pax|zip|a|shar|lha|filelist)$/) { $cfg::type = $1; $cfg::type =~ s/^a$/ar/; $cfg::compress = "false"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif (-d "$file") { $cfg::type = "copy"; $cfg::compress = "false"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.tgz$/) { $cfg::type = "tar"; $cfg::compress = "gzip"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.tbz2?$/) { $cfg::type = "tar"; $cfg::compress = "bzip2"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.taz$/) { $cfg::type = "tar"; $cfg::compress = "compress"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.rpm$/) { $cfg::type = "cpio"; $cfg::compress = "false"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.deb$/) { $cfg::type = "ar"; $cfg::compress = "false"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.jar$/i) { $cfg::type = "zip"; $cfg::compress = "false"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } elsif ($file =~ m/\.lzh$/i) { $cfg::type = "lha"; $cfg::compress = "false"; &log("| Auto-set to type=$cfg::type compress=$cfg::compress"); &optioncheck(); # redo to set a few variables over } return($file); } ###################################################################### # Check validity of a config option ###################################################################### sub checkvar { my $ref = shift(@_); # ref to variable my $varname = shift(@_); # name of variable my $ok = shift(@_); # list of ok values, "bool", "exists" my $default = shift(@_); # default to use if not set my @ok; my $found = 0; if (!defined($ok)) { die("checkvar called incorrectly"); } if ($ok eq 'bool') { @ok = ('true','false'); } else { @ok = split(" ",$ok); } if (!defined($$ref)) { if (!defined($::opt{'nodefaults'}) and defined($default)) { print $::msg " \$$varname not found in config: default=$default\n"; $$ref = $default; } else { push(@::errors,"\$$varname not defined"); } } else { if ($ok[0] ne "exist") { foreach (@ok) { if ($_ eq $$ref) { $found = 1; } } if ($found == 0 ) { $_ = join(", ",@ok); push(@::errors,"\$$varname must be one of $_"); } } } } ###################################################################### # Check to see if a program is found in $PATH ###################################################################### sub checkinpath { my $file = shift(@_); if (defined($cfg::path{$file})) { # Override in config file if ($cfg::path{$file} =~ m:^/:) { # Starts with /; full path override if (-e $cfg::path{$file} && -x _) { print $::msg "path $file = $cfg::path{$file}\n"; return "$cfg::path{$file}"; } else { push(@::errors,"$cfg::path{$file} not found"); return(0); } } elsif (($cfg::path{$file} =~ m:^\s*sudo\s+-u\s+\S+\s+(\S+):) or ($cfg::path{$file} =~ m:^\s*sudo\s+(\S+):)) { # some sort of sudo... my $prog = $1; &checkinpath('sudo'); # sudo with full pathname if (($prog =~ m:^/:) and (-e $prog) and (-x _)) { print $::msg "path $file = $cfg::path{$file}\n"; return "$cfg::path{$file}"; } # sudo with just command name my @path = split(/:/,$ENV{'PATH'}); foreach my $dir (@path) { if (-e "${dir}/$prog" && -x _) { return "$cfg::path{$file}"; } } push(@::errors,"sudo $prog not found in \$PATH"); return(0); } else { # Didn't start with /; just overriding name of command # search PATH for it my @path = split(/:/,$ENV{'PATH'}); foreach my $dir (@path) { if (-e "${dir}/$cfg::path{$file}" && -x _) { return "$cfg::path{$file}"; } } push(@::errors,"$cfg::path{$file} not found in \$PATH"); return(0); } } else { # Not spec'ed as an override in config file; search PATH my @path = split(/:/,$ENV{'PATH'}); foreach my $dir (@path) { if (-e "${dir}/$file" && -x _) { return "$file"; } } push(@::errors,"$file not found in \$PATH"); return(0); } } ###################################################################### # Run a command, or echo it depending on the -n flag # Then show tape drive position ###################################################################### sub run_or_echo_then_query { my $cmd = shift(@_); &split_and_echo($cmd); &line(); if (!defined($::debug)) { system("($cmd) 2>&1 | $::path{tee} -a $::log"); } else { &log("(debug) command output would be here"); } if (!defined($::use_file)) { &line(); &mt('generic-query'); } &line(); # Maybe rewind (usually false for reads) if (($::do_rewind_after == 1) and !defined($::use_file)) { &log("| Rewinding..."); &mt('rewind'); &line(); } } ###################################################################### # Return a command possibly wrapped in ssh/rsh ###################################################################### sub maybe_remote_cmd { my $cmd = shift(@_); my $host = shift(@_); my $quote = shift(@_); my $is_pipeline = 0; if (!defined($quote)) { $quote = "'"; } if ($cmd =~ m:\s+(\||&&)\s+:) { $is_pipeline = 1; } if (defined($host) and ($host ne '')) { # If remote shell is smart enough use pipeline exit detectors if (($is_pipeline == 1) and ($::shelltype{$host} eq 'bash2')) { $cmd = "$::remoteshell $host " . $quote . $cmd . $::bash_pipe_exit . $quote; } elsif (($is_pipeline == 1) and ($::shelltype{$host} eq 'zsh')) { $cmd = "$::remoteshell $host " . $quote . $cmd . $::zsh_pipe_exit . $quote; } else { $cmd = "$::remoteshell $host " . $quote . $cmd . $quote; } } else { $cmd = "$cmd"; } return($cmd); } ###################################################################### # Append to the pipelins string appropriate commands to write archive ###################################################################### sub append_writer_cmd { my $cmd = shift(@_); my $dev = shift(@_); # Possibly override device if (!defined($dev)) { $dev = $::device; } if (defined($::use_pipe)) { $cmd .= $::buffer_cmd; } elsif (!defined($::remotetapehost)) { $cmd .= " | " . $::write_cmd . '"' . $dev . '"' ; } else { $cmd .= "$::buffer_cmd | "; $cmd .= &maybe_remote_cmd($::write_cmd . '"' . $dev . '"', $::remotetapehost); } return($cmd); } ###################################################################### # Stuff to do before list/restore/extract/compare # return command to get archive on stdout ###################################################################### sub setup_before_read { my $op = shift(@_); my $cmd; &line(); if (($cfg::staticlogs eq 'false') and ($cfg::staticfiles eq 'false')) { $::log = "flexbackup.$op." . ¤t_time('numeric') . ".log"; } else { $::log = "flexbackup.$op.log"; } if (! open(LOG,">$::log")) { $::log = "$cfg::tmpdir/$::log"; if (! open(LOG,">$::log")) { die "Can't write to $::log: $OS_ERROR"; } } close(LOG); &log("| Logging output to \"$::log\""); $::device = &maybe_get_filename(); &mt("generic-blocksize $::mt_blksize"); # Maybe retension if (($::do_reten == 1) and !defined($::use_file)) { &log('| Retensioning tape...'); &mt('retension'); } if (defined($::opt{'num'})) { &log("| Positioning tape at file number $::opt{num}"); &mt("rewind","fsf $::opt{num}"); } else { if (defined($::use_pipe)) { &log("| Reading from stdin (type=$cfg::type compress=$cfg::compress)"); } elsif (defined($::use_file)) { &log("| Reading from on-disk file $::device"); } elsif (defined($::use_blockdevice)) { &log("| Reading from block device $::device"); } else { &log("| Reading from CURRENT TAPE POSITION"); } } &line(); if (!defined($::use_file)) { &mt('generic-query'); &line(); } $cmd = &read_function($::device); if (defined($::remotetapehost)) { $cmd = &maybe_remote_cmd($cmd, $::remotetapehost); # Buffer both sides if remote $cmd .= $::buffer_cmd; } $cmd .= " | $::unz "; if ($::device =~ m/\.rpm$/) { $cmd .= "rpm2cpio | "; } $cmd =~ s/\s+/ /g; return($cmd); } ###################################################################### # Read from file/device - in future buffer cmds might need a blocking # dd read ahead of them ###################################################################### sub read_function { my $file = shift(@_); my $cmd; # If reading from stdin arg is '-' if ($file eq '-') { $cmd = $::buffer_cmd; $cmd =~ s/^\s*\|\s*//; # Nuke leading " | " we normally use } else { $cmd = $::read_cmd . '"' . $file . '"'; } return($cmd); } ###################################################################### # Get rid of trailing slash on path or host:/path specs ###################################################################### sub nuke_trailing_slash { my $spec = shift(@_); my $host; my $path; if ($spec =~ s/(\S+:)//) { $host = $1; $path = $spec; } else { $host = ''; $path = $spec; } if ($path ne "/") { $path =~ s%/$%%; } return($host . $path); } ###################################################################### # Print the volume label from an afio control file ###################################################################### sub print_afio_volume_header { # for now just echo our stdin print STDOUT "\n"; while() { print; } exit(0); } ###################################################################### # Figure out which of rewind/erase/reten we are going to assume ###################################################################### sub set_tape_operation_defaults { # Assume stuff based on how we are called first if (defined($::opt{'set'})) { if (!defined($::set_incremental) and ($::level == 0) and !defined($::use_file)) { # Set level zero, using device. Retension & erase a new tape # (config file may tell us not to erase) if ($cfg::erase_tape_set_level_zero eq "true") { $::do_reten = 1; $::do_erase = 1; } else { $::do_reten = 0; $::do_erase = 0; } $::do_rewind_after = 1; } else { # Using files, set incremental backup, or set non-zero # don't erase + go to end of tape $::do_reten = 0; $::do_erase = 0; $::do_rewind_after = 1; } } elsif (defined($::opt{'dir'})) { # Just one filesystem - assume we append to tape $::do_reten = 0; $::do_erase = 0; $::do_rewind_after = 1; } else { # We're doing a read of some sort $::do_reten = 0; $::do_erase = 0; # -erase has no effect anyway here $::do_rewind_after = 0; } # Then see if commandline flags override anything if (defined($::opt{'reten'})) { $::do_reten = $::opt{'reten'}; } if (defined($::opt{'erase'})) { $::do_erase = $::opt{'erase'}; } if (defined($::opt{'rewind'})) { $::do_rewind_after = $::opt{'rewind'}; } } ###################################################################### # Split long lines for echoing ###################################################################### sub split_and_echo { my $string = shift(@_); my $initial_tab; my $subsequent_tab; local($Text::Wrap::columns) = 76; # Older perl's don't have this var. Use twice to shut up # -w in that case. Output almost the same... local($Text::Wrap::separator) = " \\\n"; local($Text::Wrap::separator) = " \\\n"; # This make it easier to cut-n-paste for debugging commands manually if (defined($::debug)) { $initial_tab = " "; $subsequent_tab = " "; } else { $initial_tab = "| "; $subsequent_tab = "| "; } my @lines = wrap($initial_tab, $subsequent_tab, ($string)); foreach (@lines) { &log($_); } } ###################################################################### # Create new tape "key" and return it (YYYYMMDDHHMMSS) # Also sets ::nextfile ###################################################################### sub new_tape_key { my $key; my $dev = $cfg::device; my $old; my $string; return('') if $cfg::indexes eq "false"; $key = ¤t_time('numeric'); # If writing to a file see if there is already an index key and use it if (defined($::use_file)) { $dev .= "/$cfg::keyfile"; if (-r $dev) { open(KEY,$dev) or die("Can't open existing key $dev: $OS_ERROR"); chomp($key = ); close(KEY); &log("| Directory's existing key is $key"); # Make sure keyfile entry is there if (!defined($::index{"$key|$cfg::keyfile"})) { my $label = ""; if (defined($::debug)) { &log("(debug) \$::index{$key|$cfg::keyfile} = $label"); } else { $::index{"$key|$cfg::keyfile"} = $label; } } # Figure out the existing files foreach (sort keys %::index) { my ($tape,$filenum) = split(/\|/,$_); if ($tape eq $key) { $::nextfile = $filenum; } } # Set for the next file $::nextfile++; return($key); } } &log("| Creating index key $key"); $string = "$::path{printf} \'$key\\nThis is a flexbackup index key\\n\' "; $string = &append_writer_cmd($string, $dev); if (defined($::debug)) { &log("(debug) $string"); } else { `$string 2> /dev/null`; } $::nextfile = 1; if (defined($::use_file)) { my $label = ""; if (defined($::debug)) { &log("(debug) \$::index{$key|$cfg::keyfile} = $label"); } else { $::index{"$key|$cfg::keyfile"} = $label; } } else { my $label = ""; if (defined($::debug)) { &log("(debug) \$::index{$key|0} = $label"); } else { $::index{"$key|0"} = $label; } } # So that we won't generate duplicate keys... # (as long as two processes with -newtape aren't run in parallel) sleep(1); return($key); } ###################################################################### # Get existing index key # Also sets ::nextfile ###################################################################### sub get_tape_key { my $quiet = shift(@_); my $key; return('') if $cfg::indexes eq "false"; # If writing to a file see if there is already an index key and use it if (defined($::use_file)) { my $dev = "$cfg::device/$cfg::keyfile"; if (-r $dev) { open(KEY,$dev) or die("Can't open existing key $dev: $OS_ERROR"); chomp($key = ); close(KEY); } else { return(&new_tape_key()); } } else { my $string = "$::path{dd} $::dd_blk_flag $::dd_write_pad_flag count=1 if=$::device"; if (defined($::remotetapehost)) { $string = &maybe_remote_cmd($string, $::remotetapehost); } if (defined($::debug)) { &log("(debug) $string"); $key = ''; } else { $key = `$string 2> /dev/null`; @_ = split(/\n/,$key); $key = $_[0]; } if (defined($key)) { chomp($key); if ($key !~ m/^\d+$/) { if (!defined($quiet)) { &log("| ERROR: Tape doesn't have an index! (use -newtape?)"); } $::nextfile = 0; return(''); } } else { if (!defined($quiet)) { &log("| ERROR: Tape doesn't have an index! (use -newtape?)"); } $::nextfile = 0; return(''); } } # Find the number of existing files $::nextfile = 0; unless (defined($::use_file)) { foreach (sort keys %::index) { my ($tape,$filenum) = split(/\|/,$_); if ($tape eq $key) { if ($filenum > $::nextfile) { $::nextfile = $filenum; } } } # Set for the next file $::nextfile++; &log("| Found index key $key, next file is $::nextfile"); } else { &log("| Found directory index key $key"); } return($key); } ###################################################################### # Print table of contents # Can give a specific key as argument # Or uses command flag (specific key, current tape/dir, or "all") ###################################################################### sub toc_routine { my $arg = shift(@_); my %desired_keys; my $tape; my $desired; my $label; my $dir; my $file; my %tape_files; my %disk_files; return if $cfg::indexes eq "false"; if (defined($arg)) { # Print toc for current tape if given argument $desired_keys{$arg} = 1; } elsif ($::opt{'toc'} =~ m/^\d+$/) { # Print toc for a specific tape &log("| Listing specific index"); $desired_keys{"$::opt{toc}"} = 1; &line(); } elsif ($::opt{'toc'} eq '') { # Print toc for current tape/device &mt('rewind'); my $key = &get_tape_key(); &mt('rewind'); if ($key ne '') { $desired_keys{$key} = 1; } &line(); } elsif ($::opt{'toc'} eq "all") { # Print everything we know about &log("| Listing all in database"); foreach (keys %::index) { ($tape,$file) = split(/\|/,$_); $desired_keys{$tape} = 1; } &line(); } else { die("Invalid key spec $::opt{toc}"); } # Go through the index and fill hashes foreach my $key (keys %::index) { ($tape,$file) = split(/\|/,$key); if ($file =~ m/^\d+$/) { $tape_files{$tape}{$file} = $::index{$key}; } else { $disk_files{$tape}{$file} = $::index{$key}; } } # Print the toc of each tape in our desired list foreach $desired (sort bynumber keys %desired_keys) { my $found = 0; my $length = 45; foreach $tape (sort bynumber keys %tape_files) { if ($tape eq $desired) { $found = 1; &log(''); &log("File Contents (tape index $tape)"); &log("-" x $length); foreach $file (sort bynumber keys %{$tape_files{$tape}}) { $_ = sprintf("%-04s",$file); &log($_ . " " . $tape_files{$tape}{$file}); } } } foreach $dir (sort bynumber keys %disk_files) { if ($dir eq $desired) { my @array; $found = 1; foreach $file (sort keys %{$disk_files{$dir}}) { if ((! -e "$cfg::device/$file") and (!defined($::opt{'toc'}) or ($::opt{'toc'} eq ''))) { &log("| Bogus index entry - $file does not exist"); &rmindex("$dir:$file"); delete $disk_files{$dir}{$file}; } } &log(''); &log("File Contents (dir index $dir)"); &log("-" x $length); foreach $file (keys %{$disk_files{$dir}}) { push(@array, $file . " " . $disk_files{$dir}{$file}); } foreach (sort byfilename @array) { &log($_); } } } if ($found == 0) { &log("Key $desired not found in index"); } &log(''); } } ###################################################################### # Nuke stuff from DB ###################################################################### sub rmindex { my $arg = shift(@_); my $key; my $tape; my $filenum; my $file; my $found = 0; return if $cfg::indexes eq "false"; # Figure out if we delete all for one tape, single entry for one tape, # or the entire db if ($arg =~ m/^(\d+)(:all)?$/) { $key = $1; } elsif ($arg =~ m/^(\d+):(.+)$/) { $key = $1; $file = $2; } elsif ($arg eq "all") { &log("| Removing all in database!!!"); &log("| Hit CTRL-C to abort within 5 seconds.."); &line(); sleep(5); foreach (keys %::index) { delete $::index{$_}; } return; } else { die("Invalid key or key:fileno spec $arg"); } if ($key =~ m/^\d+$/) { # This section deletes a whole index record, or maybe just # individual file records foreach (sort keys %::index) { ($tape,$filenum) = split(/\|/,$_); if (defined($file)) { # One file entry if (($tape eq $key) and (defined($::use_file) or ($filenum != 0)) and ($filenum eq $file)) { &log("| Deleting record for $tape file $filenum"); $found++; if (defined($::debug)) { &log("(debug) delete \$::index{$tape|$filenum}"); } else { delete $::index{"$tape|$filenum"}; } } } else { # Whole tape/dir entry if ($tape eq $key) { &log("| Deleting record for $tape file $filenum"); $found++; if (defined($::debug)) { &log("(debug) delete \$::index{$tape|$filenum}"); } else { delete $::index{"$tape|$filenum"}; } } } } if ($found eq 0) { &log("| Record for $arg not found"); } &line(); return; } } ###################################################################### # Nuke file from on disk, and stuff from DB ###################################################################### sub rmfile { my $key; my $tape; my $filenum; return if !defined($::use_file); $key = &get_tape_key('quiet'); foreach my $arg (@{$::opt{'rmfile'}}) { my $file = "$cfg::device/$arg"; if ($arg eq 'all') { # Nuke all files in this dir opendir(DIR,$cfg::device) or die ("Can't open dir $cfg::device: $OS_ERROR"); foreach my $f (readdir(DIR)) { next if ($f =~ m:^\.\.?$:); #next if ($f =~ m%^$cfg::keyfile$%); if ( -f "$cfg::device/$f") { &log("| Erasing archive $f"); unlink("$cfg::device/$f") or die ("Can't rm $cfg::device/$f: $OS_ERROR"); } if ( -d "$cfg::device/$f") { &log("| Erasing directory $f"); system("rm -rf $cfg::device/$f") and die ("Can't rm $cfg::device/$f: $OS_ERROR"); } } closedir(DIR); # Nuke all db entries for this key if ($key ne '') { &rmindex("$key:all"); } } elsif (-f $file) { &log("| Deleting file $file"); unlink($file) or die ("Can't rm $file: $OS_ERROR"); if ($key ne '') { # Nuke db entry for this file &rmindex("$key:$arg"); } } elsif (-d $file) { &log("| Deleting directory $file"); system("rm -rf $file") and die ("Can't rm $file: $OS_ERROR"); if ($key ne '') { # Nuke db entry for this file &rmindex("$key:$arg"); } } else { warn("Error: $file doesn't exist"); } } } ###################################################################### # Remove index records for a tape we are about to erase ###################################################################### sub maybe_delete_old_index { my $key; return if $cfg::indexes eq "false"; return if (defined($::use_file)); $key = &get_tape_key('quiet'); if ($key ne '') { &rmindex("$key:all"); } } ###################################################################### # Sort by number ###################################################################### sub bynumber { $a <=> $b; } ###################################################################### # Sort by archive filename ###################################################################### sub byfilename { return 0 if ($a =~ m/^$cfg::keyfile/); return 1 if ($b =~ m/^$cfg::keyfile/); my $alabel; my $alevel; my $blabel; my $blevel; if ($a =~ m/^(.+?)\.(\d+)(\.(\d+))?\./) { $alabel = $1; $alevel = $2; if ($b =~ m/^(.+?)\.(\d+)(\.(\d+))?\./) { $blabel = $1; $blevel = $2; if ($alabel eq $blabel) { return($alevel <=> $blevel); } } } return($a cmp $b); } ###################################################################### # Figure out numeric level for '-level incremental', for a certain fs. # Try to find last the stamp file, then add one to the level ###################################################################### sub get_incremental_level { my $fs = shift(@_); my $label = &get_label($fs); my $highestlevel = 0; opendir(DIR,"$cfg::stampdir") or die("Can't open $cfg::stampdir: $OS_ERROR"); foreach my $file (readdir(DIR)) { next if ($file !~ m/^$cfg::sprefix$label\.(\d+)$/); if ($1 > $highestlevel) { $highestlevel = $1; } } close(DIR); $highestlevel++; return($highestlevel); } ###################################################################### # Common commands to invoke 'find' & get a desired file list on stdout ###################################################################### sub file_list_cmd { my $dir = shift(@_); my $timestampfile = shift(@_); my $separator = shift(@_); my $level = shift(@_); my $remote = shift(@_); my $otherarg = shift(@_); if (!defined($separator) or ($separator !~ m/^(null|newline)$/)) { $separator = 'null'; } my $cmd = ''; # FreeBSD wants -E to enable extended regex if ($::uname =~ /FreeBSD/) { $cmd .= "$::path{find} -E . "; } else { $cmd .= "$::path{find} . "; } my $prunekey; if (defined($remote)) { $prunekey = "$remote:$dir"; } else { $prunekey = $dir; } if (defined(%{$::prune{$prunekey}})) { # FreeBSD needs -E (above) and no backslashes around the (|) chars if ($::uname =~ /FreeBSD/) { $cmd .= '-regex "\./('; $cmd .= join('|', keys %{$::prune{$prunekey}}); $cmd .= ')/.*" '; } else { $cmd .= '-regex "\./\('; $cmd .= join('\|', keys %{$::prune{$prunekey}}); $cmd .= '\)/.*" '; } $cmd .= '-prune -o '; } else { # Can't use find -depth with -prune (see single unix spec etc) # (not toally required anyway, only if you are archiving dirs you # don't have permissions on and are running as non-root) $cmd .= "-depth "; } $cmd .= "$::mountpoint_flag "; $cmd .= "! -type s "; if (defined($otherarg)) { $cmd .= $otherarg . " "; } if ($level != 0) { # If local, we can use the flexbackup timetamp native and ctime # checks can be used. Remote, we'll be creating stamp with "touch # -t"... but ctime can't be touched backwards. Turn it off. # # If atime preserve is set, can't use ctime checks anyway since # preserving atime changes the ctime. if (($cfg::atime_preserve eq 'false') and !defined($remote)) { $cmd .= '\( '; } $cmd .= "-newer \"$timestampfile\" "; if (($cfg::atime_preserve eq 'false') and !defined($remote)) { $cmd .= "-or -cnewer \"$timestampfile\" " . '\) '; } } $cmd .= "$::exclude_expr "; if (!defined($::pkgdelta)) { if ($separator eq 'newline') { $cmd .= "-print "; } else { $cmd .= "-print0 "; } } else { # Use the normal level & timestamp mechanism to get a list of files # Then only keep unowned or owned+changed files my $host; my $find = &maybe_remote_cmd("cd \"$dir\"; $cmd -print", $remote); my $write = "> $::pkgdelta_filelist"; if(defined($remote)) { &log("| Listing level $level to-be-archived files for $remote:$dir"); $write = &maybe_remote_cmd("$::path{cat} $write", $remote); $write = "| $write"; $host = $remote; } else { &log("| Listing level $level to-be-archived files for $dir"); $host = 'localhost'; } &log("| Finding subset of files based on packaging system delta"); if (!defined($::debug)) { open(LIST,"$find |") || die; open(NEWLIST,"$write") || die; while() { my $key; my $archive = 0; chomp(my $file = $_); # Strip leading ./ $file =~ s:^\./::g; # Don't care about the backup dir itself next if ($file eq '.'); if ($dir eq '/') { $key = "/$file"; } else { $key = "$dir/$file"; } if (($cfg::pkgdelta_archive_unowned eq 'true') and !defined($::packaged{$host}{$key})) { $archive = 1; } if (($cfg::pkgdelta_archive_changed eq 'true') and defined($::changed{$host}{$key})) { $archive = 1; } if ($archive == 1) { if ($separator eq 'null') { print NEWLIST "./$file\0"; } else { print NEWLIST "./$file\n"; } } } close(LIST); close(NEWLIST); } &line(); $cmd = "$::path{cat} $::pkgdelta_filelist "; } return($cmd); } ###################################################################### # List installed packages, fills %package_list hash ###################################################################### sub list_packages { my $host = shift (@_); my $cnt = 0; if ($::pkgdelta eq 'rpm') { my $cmd = "$::path{rpm} -q -a --queryformat '%{name}-%{version}-%{release}.%{arch}.rpm\\n'"; if ($host ne 'localhost') { &log("| Identifying all RPM packages on host $host..."); $cmd = &maybe_remote_cmd($cmd, $host); } else { &log("| Identifying all RPM packages..."); } if (defined($::debug)) { &log("(debug) $cmd"); } else { open(LIST,"$cmd |") || die; while() { if (m:^(.*)$:) { $::package_list{$host}{$1} = 1; if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } } } close(LIST); } } elsif ($::pkgdelta eq 'freebsd') { my $cmd = "$::path{pkg_info}"; if ($host ne 'localhost') { &log("| Identifying all FreeBSD packages on host $host..."); $cmd = &maybe_remote_cmd($cmd, $host); } else { &log("| Identifying all FreeBSD packages..."); } if (defined($::debug)) { &log("(debug) $cmd"); } else { my (@junk, $pkg); open(LIST,"$cmd |") || die; while() { if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } ($pkg, @junk) = split (/\s+/, $_); $::package_list{$host}{$pkg} = 1; } close(LIST); } } } ###################################################################### # Fill %packaged with a list of files on host owned by packages ###################################################################### sub find_packaged_files { my $host = shift (@_); my $cnt = 0; return if ($cfg::pkgdelta_archive_unowned eq 'false'); if ($::pkgdelta eq 'rpm') { my $cmd = "$::path{rpm} -q -a -l"; if ($host ne 'localhost') { &log("| Finding all files owned by RPM packages on host $host..."); $cmd = &maybe_remote_cmd($cmd, $host); } else { &log("| Finding all files owned by RPM packages..."); } if (defined($::debug)) { &log("(debug) $cmd"); } else { open(LIST,"$cmd |") || die; while() { if (m:^(/.*)$:) { $::packaged{$host}{$1} = 1; if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } } } close(LIST); } } elsif ($::pkgdelta eq 'freebsd') { my $cmd = "$::path{pkg_info} -f -q -a"; my ($fullpath, $localbase, $alt_localbase); $localbase = '/usr/local'; $alt_localbase = ''; $fullpath = ''; if ($host ne 'localhost') { &log("| Finding all files owned by FreeBSD packages on host $host..."); $cmd = &maybe_remote_cmd($cmd, $host); } else { &log("| Finding all files owned by FreeBSD packages..."); } if (defined($::debug)) { &log("(debug) $cmd"); } else { open(LIST,"$cmd 2> /dev/null |") || die; while() { # If it starts with '@' then it's a pkg directive, # else it's a (relative) path # if (/^\@/) { if (/\@cwd\s+(\S+)/) { my ($name, $path, $suffix); $localbase = $1; $alt_localbase = ''; ($name,$path,$suffix) = fileparse($localbase,'\.\S+'); $path =~ s/\/$//; # In some (default) situations there are some packages which are # installed relative to a PREFIX which is actually a link in the / # filesystem. The following hack gets around that and creates an # entry in $packaged twice--once for the full path that would be seen via # pkg_info -L and one for the "unlinked" version. In this manner # no matter which FS is being dumped, the code to filter out # packaged files will always work. # if (-l $path) { my $link; $link = readlink ($path); $link = '/' . $link . '/' . $name; $alt_localbase = $link; } } if (/\@dirrm\s+(\S+)/) { $fullpath = $localbase . '/' . $1; $::packaged{$host}{$fullpath} = 1; if ($alt_localbase ne '') { $fullpath = $alt_localbase . '/' . $1; $::packaged{$host}{$fullpath} = 1; } if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } } } else { $fullpath = $localbase . '/' . $_; chomp ($fullpath); $::packaged{$host}{$fullpath} = 1; if ($alt_localbase ne '') { $fullpath = $alt_localbase . '/' . $_; chomp ($fullpath); $::packaged{$host}{$fullpath} = 1; } if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } } } close(LIST); } } } ###################################################################### # Fill %changed with a list of packaged files on host that have been # modified ###################################################################### sub find_changed_files { my $host = shift (@_); my $cnt = 0; return if ($cfg::pkgdelta_archive_changed eq 'false'); if ($::pkgdelta eq 'rpm') { my $cmd = "$::path{rpm} -V -a"; my ($num); if ($host ne 'localhost') { &log("| Finding changed package files on host $host..."); $cmd = &maybe_remote_cmd($cmd, $host); } else { &log("| Finding changed package files..."); } $num = scalar (keys %{$::package_list{$host}}); &log("| Analyzing $num packages may take quite a while, please be patient"); if (defined($::debug)) { &log("(debug) $cmd"); } else { open(LIST,"$cmd |") || die; while() { if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } # ex: if size, md5sum, and timestamp changed on a config file # S.5....T c /etc/ntp.conf if (m:^([\.S][\.M][\.5][\.D][\.L][\.U][\.G][\.T]) [dgc ] (.*)$:) { $::changed{$host}{$2} = 1; } } close(LIST); } } elsif ($::pkgdelta eq 'freebsd') { my $cmd = "$::path{pkg_info} -g -a -q"; my ($num); if ($host ne 'localhost') { &log("| Finding changed package files on host $host..."); $cmd = &maybe_remote_cmd($cmd, $host); } else { &log("| Finding changed package files..."); } $num = scalar (keys %{$::package_list{$host}}); &log("| Analyzing $num packages may take quite a while, please be patient"); if (defined($::debug)) { &log("(debug) $cmd"); } else { open(LIST,"$cmd 2> /dev/null |") || die; while() { if (&POSIX::isatty($::msg)) { print $::msg &spinner(++$cnt) . "\r"; } if (/^(\S+)\s+fails.*MD5.*checksum$/) { $::changed{$host}{$1} = 1; } } close(LIST); } } } ############################################################################# # Actually test to see if we can run buffer. In situations where SysV shared # memory is low, or buffer can't run, buffer can fail ############################################################################# sub test_bufferprog { my $buffer_cmd = shift(@_); my $host = shift(@_); my $tmp_script = "$cfg::tmpdir/buftest.$host.$PROCESS_ID.sh"; my $retval = 0; my $pipecmd; $buffer_cmd =~ s:^\s*\|\s*::; $buffer_cmd =~ s:\s*\|\s*$::; # Create a script which tests the buffer program open(SCR,"> $tmp_script") || die; print SCR "#!/bin/sh\n"; print SCR "tmp_data=/tmp/bufftest\$\$.txt\n"; print SCR "tmp_err=/tmp/bufftest\$\$.err\n"; print SCR "echo testme > \$tmp_data\n"; print SCR "$buffer_cmd > /dev/null 2> \$tmp_err < \$tmp_data\n"; print SCR "res=\$?\n"; print SCR "out=\`cat \$tmp_err\`\n"; print SCR "if [ \$res -eq 0 ]; then\n"; print SCR " echo successful\n"; print SCR "else\n"; print SCR " echo \"unsuccessful: exit code \$res: \$out\" \n"; print SCR "fi\n"; print SCR "rm -f \$tmp_data \$tmp_err\n"; close(SCR); if ($host eq 'localhost') { print $::msg "| Checking '$cfg::buffer' on this machine... "; $pipecmd = "sh $tmp_script "; } else { print $::msg "| Checking '$cfg::buffer' on host $host... "; $pipecmd = "cat $tmp_script | ($::remoteshell $host 'cat > $tmp_script; sh $tmp_script; rm -f $tmp_script')"; } if (!defined($::debug)) { open(PIPE,"$pipecmd |") || die; while () { if (/^unsuccessful: exit code (\d+): (.*)/) { $retval = $1; my $out = $2; if ($retval != 0) { push(@::errors, "Problems encountered testing '$cfg::buffer' on host '$host':"); if ($out ne '') { push(@::errors, " --> " . $out); } if (($cfg::buffer eq 'buffer') and ($retval == 255)) { push(@::errors, " You don't have enough shared memory to run '$cfg::buffer' on $host, or"); push(@::errors, " have exceeded buffering limits. Try lowering the amount specified in"); push(@::errors, " \$buffer_megs in your flexbackup.conf file, or reconfigure your"); push(@::errors, " kernel to include more SysV shared memory pages if using *BSD."); } else { push(@::errors, " Unknown problem trying to run '$cfg::buffer' (exit code $retval). Try disabling it"); push(@::errors, " or lowering \$buffer_megs."); } } } } close (PIPE); } else { print $::msg "\n(debug) $pipecmd\n"; } if ($retval == 0) { print $::msg "Ok\n"; } else { print $::msg "Failed!\n"; } unlink("$tmp_script"); return($retval); } ############################################################################# # Check that programs exist on remote systems # Check buffer execution on them too ############################################################################# sub check_remote_progs { my $remotehost_ref = shift(@_); my $remoteprogs_ref = shift(@_); my $err = 0; my @progs; foreach my $host (keys %$remotehost_ref) { &check_shell($host); } foreach (@$remoteprogs_ref) { # Could be '0' if original checkinpath failed on localhost if ($_ ne '0') { push(@progs,"type $_ 2>&1"); } else { $err++; } } my $string = join ('; ',@progs); foreach my $host (keys %$remotehost_ref) { print $::msg "| Checking for required programs on host $host... "; my $cmd = "$::remoteshell $host \"sh -c '$string'\""; if (defined($::debug)) { print $::msg "\n(debug) $cmd\n"; next; } if (!(open(PIPE,"$cmd |"))) { push (@::errors, "Could not open pipe to remote shell - $!"); $err++; last; } while () { if (m/(\S+) not found/) { push(@::errors, "Could not find program '$1' on remote machine '$host'"); $err++; } } close (PIPE); if ($err == 0) { print $::msg "Ok\n"; } else { print $::msg "Failed!\n"; } } if ($cfg::buffer ne 'false') { foreach my $host (keys %$remotehost_ref) { &test_bufferprog($::buffer_cmd, $host); } } } ############################################################################# # Check shell on remote systems # (Mainly to see if we should use bash pipe exit trick at this point) ############################################################################# sub check_shell { my $host = shift(@_); my $pipecmd; $pipecmd = 'set x = 1 && test $x && echo csh:yes; echo tcsh:$tcsh; echo bash:$BASH_VERSION; echo zsh:$ZSH_VERSION; echo ksh:$KSH_VERSION'; if ($host eq 'localhost') { print $::msg "| Checking /bin/sh on this machine... "; } else { print $::msg "| Checking shell on $host... "; $pipecmd = "$::remoteshell $host '" . $pipecmd . "'"; } $::shelltype{$host} = 'unknown'; if (defined($::debug)) { print $::msg "\n(debug) $pipecmd\n"; } if (!(open(PIPE,"$pipecmd 2>&1 |"))) { return; } while () { if (m/^(\S+):(\S.+)$/) { my $shell = $1; my $ver = $2; if ($shell eq 'bash') { if ($ver =~ m/^2/) { $::shelltype{$host} = 'bash2'; } else { $::shelltype{$host} = 'bash1'; } } else { $::shelltype{$host} = $shell; } } } close (PIPE); if (($::shelltype{$host} eq 'unknown') and ($::uname !~ m/Linux/)) { print $::msg "$::shelltype{$host} (probably Bourne Shell)\n"; } else { print $::msg "$::shelltype{$host}\n"; } } ############################################################################# # Wipe a tape for use. ############################################################################# sub newtape () { my $retval; if (defined($::tapedevice)) { &log('| Rewinding & erasing tape...'); } &mt('rewind'); &maybe_delete_old_index(); &mt('rewind'); &mt('generic-erase'); $retval = &new_tape_key(); return($retval); } ############################################################################# # Test writing a couple files to tape, then read & diff. To help make # sure filemarks, blocks, padding, are working as we need. ############################################################################# sub test_tape_drive { my $cmd; my $tmp1 = "$cfg::tmpdir/test1.$PROCESS_ID"; my $tmp2 = "$cfg::tmpdir/test2.$PROCESS_ID"; my $tmp3 = "$cfg::tmpdir/test3.$PROCESS_ID"; my $fail = 0; my $configfile; if (defined($::opt{'c'})) { $configfile = $::opt{'c'}; } else { $configfile = $::CONFFILE; } &mt("generic-blocksize $::mt_blksize"); &log("| Testing will *erase* the tape currently in the drive!"); &log("| Hit CTRL-C to abort within 10 seconds..."); &line(); sleep(10); &log("| If for some reason this program does not exit within a few minutes,"); &log("| Hit CTRL-C, and try adjusting \$blksize, \$pad_blocks, or \$mt_blksize."); &line(); &newtape(); &line(); &mt('generic-query'); &log(''); &log("Writing test file \#1"); $cmd = "$::path{cat} $0"; $cmd = &append_writer_cmd($cmd); if (!defined($::debug)) { system($cmd); if ($CHILD_ERROR) { $fail++; } } else { &log($cmd); } &mt('generic-query'); &log("Writing test file \#2"); $cmd = "$::path{cat} $configfile"; $cmd = &append_writer_cmd($cmd); if (!defined($::debug)) { system($cmd); if ($CHILD_ERROR) { $fail++; } } else { &log($cmd); } &mt('generic-query'); &log("Writing test file \#3"); $cmd = "$::path{cat} $0"; $cmd = &append_writer_cmd($cmd); if (!defined($::debug)) { system($cmd); if ($CHILD_ERROR) { $fail++; } } else { &log($cmd); } &mt('generic-query'); &log(''); &log('Rewinding...'); &mt('rewind'); if ($cfg::indexes eq 'true') { &log('Skipping index label...'); &mt('fsf 1'); } &mt('generic-query'); &log(''); &log("Reading test file \#1"); $cmd = &read_function($::device); if (defined($::remotetapehost)) { $cmd = &maybe_remote_cmd($cmd, $::remotetapehost); # Buffer both sides if remote $cmd .= $::buffer_cmd; } # if pad blocks was true we have nulls at the end (won't be in this script otherwise) if ($cfg::pad_blocks eq 'true') { $cmd .= " | $::path{tr} -d '\\0' > $tmp1"; } else { $cmd .= "> $tmp1"; } if (!defined($::debug)) { system($cmd); if ($CHILD_ERROR) { $fail++; } } else { &log("(debug) $cmd"); } &mt('generic-query'); &log("Reading test file \#2"); $cmd = &read_function($::device); if (defined($::remotetapehost)) { $cmd = &maybe_remote_cmd($cmd, $::remotetapehost); # Buffer both sides if remote $cmd .= $::buffer_cmd; } # if pad blocks was true we have nulls at the end (won't be in config file otherwise) if ($cfg::pad_blocks eq 'true') { $cmd .= " | $::path{tr} -d '\\0' > $tmp2"; } else { $cmd .= "> $tmp2"; } if (!defined($::debug)) { system($cmd); if ($CHILD_ERROR) { $fail++; } } else { &log("(debug) $cmd"); } &mt('generic-query'); &log("Reading test file \#3"); $cmd = &read_function($::device); if (defined($::remotetapehost)) { $cmd = &maybe_remote_cmd($cmd, $::remotetapehost); # Buffer both sides if remote $cmd .= $::buffer_cmd; } # if pad blocks was true we have nulls at the end (won't be in this script otherwise) if ($cfg::pad_blocks eq 'true') { $cmd .= " | $::path{tr} -d '\\0' > $tmp3"; } else { $cmd .= "> $tmp3"; } if (!defined($::debug)) { system($cmd); if ($CHILD_ERROR) { $fail++; } } else { &log("(debug) $cmd"); } &mt('generic-query'); &log(''); &mt('rewind'); &log("Comparing..."); if (!defined($::debug)) { system("$::path{diff} -q $0 $tmp1"); if ($CHILD_ERROR) { $fail++; } system("$::path{diff} -q $configfile $tmp2"); if ($CHILD_ERROR) { $fail++; } system("$::path{diff} -q $0 $tmp3"); if ($CHILD_ERROR) { $fail++; } } else { &log("(debug) $::path{diff} -q $0 $tmp1"); &log("(debug) $::path{diff} -q $configfile $tmp2"); &log("(debug) $::path{diff} -q $0 $tmp3"); } unlink $tmp1; unlink $tmp2; unlink $tmp3; if ($fail != 0) { print $::msg "\nFAILURE! Problem with tape driver or parameters. Please see the FAQ\n"; print $::msg "or try changing the \$blksize, \$pad_blocks, or \$mt_blksize settings.\n"; exit(1); } else { print $::msg "SUCCESS! Tape drive parameters seem to work just fine\n"; } } ###################################################################### # Check if the week day is as specified before backup (for complex cron setups) ###################################################################### sub check_wday { if (defined($::opt{'wday'})) { my @now = localtime; my $wday_now = $now[6]; # Just silently hard-limit these to valid set if ($::opt{'wday'} >= 7) { $::opt{'wday'} = 0; } if ($::opt{'wday'} < 0) { $::opt{'wday'} = 0; } if ($wday_now != $::opt{'wday'}) { exit(0); } } } ###################################################################### # Split whitespace-separated list. # If it contains quotes, do a bit differently so we can have # items containing whitespace, as long as all elements are quoted. ###################################################################### sub split_list { my $string = shift(@_); my @array; if ($string =~ m/\"/) { $string =~ s/^\s*\"//; $string =~ s/\"\s*$//; @array = split(/\"\s+\"/,$string); } elsif ($string =~ m/\'/) { $string =~ s/^\s*\'//; $string =~ s/\'\s*$//; @array = split(/\'\s+\'/,$string); } else { @array = split(/\s+/,$string); } return(@array); } ###################################################################### # To show activity.... ###################################################################### sub spinner { my $index = shift(@_); my (@spinner) = ('|','/','-','\\','|','/','-','\\'); $index = $index % $#spinner; return($spinner[$index]); } flexbackup-1.2.1/flexbackup.10100664000076400007640000001354207737566240015423 0ustar edwinhedwinh.TH "FLEXBACKUP" "1" "Oct 2003" "Flexbackup" .SH "NAME" flexbackup \- a flexible backup/archiving tool .SH "SYNOPSIS" \fBflexbackup\fR [\fIOPTION\fR] .SH "DESCRIPTION" Flexbackup is a perl script front-end to various low-level archiving utilities such as \fBtar\fR, \fBdump\fR/\fBrestore\fR, \fBcpio\fR, and others. .SH "BACKUP OPTIONS" .TP \fBflexbackup\fR \fI-dir\fR <\fIdir\fR> Backup a directory tree starting from path \(dqpath\(dq using level 0 (full). .TP \fBflexbackup\fR \fI-set\fR \fIall\fR Backup all sets defined in \fBflexbackup.conf(5)\fR using level 0 (full). .TP \fBflexbackup\fR \fI-set\fR <\fItag\fR> Backup a set named \(dqtag\(dq using level 0 (full). The set name is defined in \fIflexbackup.conf(5)\fR. .TP \fBflexbackup\fR [...] \fI-level\fR <\fI0-9\fR | \fIfull\fR | \fIdifferential\fR | \fIincremental\fR> Change backup level to a number \(dq0-9\(dq, or one of the symbolic names: \(dqfull\(dq (level 0); \(dqdifferential\(dq (level 1); \(dqincremental\(dq (previous backup level + 1). .TP \fBflexbackup\fR [...] \fI-pkgdelta\fR <\fIrpm\fR | \fIfreebsd\fR> prune backup to files not part of a package or changed from distributed version. .TP \fBflexbackup\fR [...] \fI-wday\fR <\fI0-7\fR> Prune backup to files not part of a package or changed from distributed version. backup only if the week day matches the input number. Sunday is 0 or 7. .TP \fBflexbackup\fR [...] \fI-pipe\fR Write backup data to stdout rather than file/device. .TP \fBflexbackup\fR [...] \fI-ignore-errors\fR Continue backups even if commands return error status .SH "RESTORE OPTIONS" .TP \fBflexbackup\fR \fI-list\fR List files in archive. .TP \fBflexbackup\fR \fI-extract\fR Extract (restore) all files from archive into your current working directory. .TP \fBflexbackup\fR \fI-extract\fR \fI-flist\fR <\fIfilelist\fR> Extract (restore) the files listed in text file \(dqfilelist\(dq into your current working directory. .TP \fBflexbackup\fR \fI-extract\fR \fI-flist\fR <\fIfilename\fR> Extract (restore) the single file named \(dqfilename\(dq into your current working directory. .TP \fBflexbackup\fR \fI-compare\fR Compare the archive with the files in your current working directory. .TP \fBflexbackup\fR \fI-restore\fR Interactive restore (\fBdump\fR type only for now). .TP \fBflexbackup\fR [...] \fI-num\fR <\fIn\fR> Read file number \(dqn\(dq from tape. If not given uses current tape position. .TP \fBflexbackup\fR [...] <\fIarchive\fR> If archiving to files rather than a device, use file named \(dqarchive\(dq for the \fIlist\fR/\fIextract\fR/\fIcompare\fR/\fIrestore\fR operations. .TP \fBflexbackup\fR [...] \fI-pipe\fR Read backup data from stdin rather than file/device. .SH "INDEXING OPTIONS" .TP \fBflexbackup\fR \fI-toc\fR List current device's table of contents. .TP \fBflexbackup\fR \fI-toc\fR \fIall\fR List all known table of contents. .TP \fBflexbackup\fR \fI-toc\fR <\fIkey\fR> List table of contents for specific database index key named \(dqkey\(dq. .TP \fBflexbackup\fR \fI-rmindex\fR \fIall\fR Force delete all database index info. .TP \fBflexbackup\fR \fI-toc\fR <\fIkey\fR> Force delete specified database tape/dir index key named \(dqkey\(dq. .TP \fBflexbackup\fR \fI-toc\fR <\fIkey\fR:\fIfile\fR> Force delete specified database tape/dir index key named \(dqkey\(dq, file number \(dqfile\(dq. .SH "MISCELLENEOUS OPTIONS" .TP \fBflexbackup\fR \fI-newtape\fR Erase and create new index key (but don't do any backups). .TP \fBflexbackup\fR \fI-rmfile\fR \fIall\fR If archiving to files rather than device, remove all files and index information. .TP \fBflexbackup\fR \fI-rmfile\fR <\fIfile\fR> If archiving to files rather than device, remove the file named \(dqfile\(dq and its index information. .TP \fBflexbackup\fR [...] \fI-c\fR <\fIfile.conf\fR> Use file named \(dqfile.conf\(dq for configuration information instead of \fB/etc/flexbackup.conf\fR(5). .TP \fBflexbackup\fR [...] \fI-type\fR <\fItype\fR> Override \fB$type\fR setting from config file with \(dqtype\(dq. .TP \fBflexbackup\fR [...] \fI-compress\fR <\fItype\fR> Override \fB$compress\fR setting from config file with \(dqtype\(dq. .TP \fBflexbackup\fR [...] \fI-device\fR <\fIdevice\fR> Override \fB$device\fR setting from config file with \(dqdevice\(dq. .TP \fBflexbackup\fR [...] \fI-d\fR <\fI'setting=value'\fR> Override \fB$setting\fR setting from config file with \(dqvalue\(dq. .TP \fBflexbackup\fR \fI-dir\fR <\fIdir\fR> \fI-erase\fR Force a rewind/erase before backup of directory named \(dqdir\(dq. .TP \fBflexbackup\fR \fI-dir\fR <\fIdir\fR> \fI-norewind\fR Do not rewind tape after a single backup of directory named \(dqdir\(dq. .TP \fBflexbackup\fR \fI-set\fR <\fItag\fR> \fI-noreten\fR Do not retension tape for level 0 (full) backups of set named \(dqtag\(dq. .TP \fBflexbackup\fR \fI-set\fR <\fItag\fR> \fI-noerase\fR Do not rewind/erase tape for level 0 (full) backups of set named \(dqtag\(dq. .TP \fBflexbackup\fR [...] \fI-reten\fR Force a retension of tape before reading. .TP \fBflexbackup\fR [...] \fI-defaults\fR Do not use any default values for config variables. .TP \fBflexbackup\fR \fI-version\fR Display version and exit. .SH "TESTING/DEBUGGING OPTIONS" .TP \fBflexbackup\fR \fI-test-tape-drive\fR Tries writing/reading files to make sure you have tape driver and parameters set up correctly in \fBflexbackup.conf\fR(5). .TP \fBflexbackup\fR [...] \fI-n\fR Don't run actual dump or mt commands, just print the steps that would be taken (dry run). .TP \fBflexbackup\fR [...] \fI-type\fR \fIfilelist\fR Run a special backup type that just saves a list of files that would have been archived. .SH "FILES" /etc/flexbackup.conf \- configuration settings .SH "REPORTING BUGS" Report bugs to (flexbackup-help@lists.sourceforge.net) .SH "AUTHOR" Written by Edwin Huffstutler (edwinh@computer.org) .SH "SEE ALSO" \fBflexbackup.conf\fR(5) \fBafio\fR(1) \fBmt\fR(1) \fBtar\fR(1) \fBstar\fR(1) \fBcpio\fR(1) \fBdump\fR(1) \fBrestore\fR(1) \fBbuffer\fR(1) \fBmbuffer\fR(1) flexbackup-1.2.1/flexbackup.conf0100664000076400007640000002215307733426556016207 0ustar edwinhedwinh# -*-Mode: perl-*- # ---------------------------------------------------------------------- # Flexbackup configuration file # check "flexbackup -help" for usage information # ---------------------------------------------------------------------- # General configuration section # Archive type? afio, dump, tar, cpio, star, pax, zip, lha, ar, shar # 'copy' or 'rsync' are extra options if running in archive-to-disk mode. # 'filelist' dumps a list of files in your cwd - for debugging setup/exclusion $type = 'afio'; # Configure backup "sets". # Not needed if you use "-dir " to backup one tree at a time. # Each set is a simple space-separated list of filesystems # Remote filesystems should denoted as 'host:dir' # You can use anything (other than 'all') as set names # # Example: # $set{'set1'} = "/home /usr"; # $set{'set2'} = "/dir3 machine2:/dir4 machine3:/dir5"; # # "-set all" will back up all defined sets. If you are doing a full backup # using tapes, each "set" will go onto a different tape and you will be # prompted for tape change in between. # $set{'backup'} = "/home"; # Subtree pruning # A space-separated list of directories to prune from each backup. # Key is a filesystem or host:dir spec as outlined above # regular expressions allowed (not shell-type wildcards!) $prune{'/'} = "tmp proc"; # Compression $compress = 'gzip'; # one of false/gzip/bzip2/lzop/zip/compress/hardware $compr_level = '4'; # compression level (1-9) (for gzip/bzip2/lzop/zip) # Buffering program - to help streaming $buffer = 'buffer'; # one of false/buffer/mbuffer $buffer_megs = '10'; # buffer memory size (in megabytes) $buffer_fill_pct = '75'; # start writing when buffer this percent full $buffer_pause_usec = '100'; # pause after write (tape devices only) # Device to backup to. -->> non-rewinding version, please! <<-- # # Examples: # Linux SCSI: /dev/nst0 Linux IDE: /dev/nht0 # Linux ftape: /dev/nqft0 FreeBSD SCSI: /dev/nrsa0 (4.x compat device node) # FreeBSD SCSI: /dev/nsa0 (5.x) # # If a directory, will archive to files in that directory rather than a device # If "host:/dev/tapedevice", will use remote tape drive via rsh/ssh # $device = '/dev/tape'; # Block size (in kilobytes!) to use for archive programs and dd. Default is # 10 for most things. Some tape drives need 32 or 64. Set to '0' to # disable all blocking $blksize = '10'; # Block size (in bytes!) to use for the tape device, with "mt setblk" or # equivalent. If set to 0, will use "variable" block size for the tape # device (which is recommended). Comment out or set to "$blksize * 1024" to # have it be the same as the archiver block size above. $mt_blksize = "0"; # Padding. True to pad blocks to blocksize # (devices only, not used when archiving to files) $pad_blocks = 'true'; # Other global flags $remoteshell = 'ssh'; # command for remote shell (rsh/ssh/ssh2) $remoteuser = ''; # if non-null, secondary username for remote shells $label = 'true'; # somehow store identifying label in archive? $verbose = 'true'; # echo each file? $sparse = 'true'; # handle sparse files? $indexes = 'true'; # false to turn off all table-of-contents support # If backing up to files, use static filenames - no date stamp # (same level backup of same directory will overwrite old backups) $staticfiles = 'false'; # True to try and preserve file access times during backup, if the selected # archive program can do so. Note that if this is true, -cnewer checks (file # permission/status changes only, not content) are turned off when deciding # which files to archive on the local system. $atime_preserve = 'false'; # Span across filesytems? ("dump" will ignore this option) # Set to "false" (don't) , "local" (all but nfs/smbfs), or "all" (everything) $traverse_fs = 'false'; # Exclude files that match these *regular expressions* (not shell wildcards) # from the backups (no affect on 'dump' archives). You can list more than one, # just keep incrementing the index in the brackets for each. Also, strip off # leading directories (the filesystem specs above or the "-dir" flag). # Matches paths, not filenames, so put .* on the front/back as needed. # Comment these out to exclude nothing. $exclude_expr[0] = '.*/[Cc]ache/.*'; $exclude_expr[1] = '.*~$'; # If true (default), and using a tape device, level zero "set" backups # assume you want to erase and use a new tape for each set. If false, level # zero "set" backups append to tapes. To force an erase for any backup, # use "-erase" on the commandline. $erase_tape_set_level_zero = 'true'; # Set this to "true" to make erase operations just call "mt rewind" - not # "mt rewind' followed by "mt erase". (For some tape drives, erase takes # hours rather than seconds or is otherwise undesirable) $erase_rewind_only = 'false'; # ---------------------------------------------------------------------- # Log/stamp files, path for temporary files $logdir = '/var/log/flexbackup'; # directory for log files $comp_log = 'gzip'; # compress log? false/gzip/bzip2/lzop/compress/zip $staticlogs = 'false'; # static log filenames w/ no date stamp $prefix = ''; # log files will start with this prefix $tmpdir = '/tmp'; # used for temporary refdate files, etc $stampdir = '/var/lib/flexbackup'; # directory for backup timestamps $index = '/var/lib/flexbackup/index'; # DB filename for tape indexes $keyfile = '00-index-key'; # filename for keyfile if archiving to dir $sprefix = ''; # stamp files will start with this prefix # ---------------------------------------------------------------------- # Parameters for 'afio' only # File extensions that should not be compressed (seperate with spaces) $afio_nocompress_types = 'mp3 MP3 Z z gz gif zip ZIP lha jpeg jpg JPG taz tgz deb rpm bz2 lzo'; # True to show block numbers $afio_echo_block = 'false'; # Files less than this size (kilobytes) won't be compressed $afio_compress_threshold = '3'; # Maximum amount of memory (megabytes) to use for temporary storage of # compression results. If a compressed file is bigger than this, compression # will have to run twice on the file (see manpage). $afio_compress_cache_size = '2'; # ---------------------------------------------------------------------- # Parameters for 'tar' only # True to show record numbers $tar_echo_record_num = 'false'; # ---------------------------------------------------------------------- # Parameters for 'cpio' only # Format of cpio archive $cpio_format = 'newc'; # ---------------------------------------------------------------------- # Parameters for 'dump' only # Estimated tape size (in kilobytes). This number doesn't really do much # but help 'dump' get size estimates if set to zero uses 'dump -a' $dump_length = '0'; # True to use /etc/dumpdates (could mess things up if you dump subdirectories # of mount points). False to use flexbackup's timestamps. $dump_use_dumpdates = 'false'; # ---------------------------------------------------------------------- # Parameters for 'star' only # Use fifo (buffering)? (you probably want to set $buffer=false above if so) $star_fifo = 'true'; # Handle ACLs? $star_acl = 'true'; # Format of star archive $star_format = 'exustar'; # True to show block numbers $star_echo_block_num = 'false'; # ---------------------------------------------------------------------- # Parameters for 'pax' only # Format of pax archive $pax_format = 'ustar'; # ---------------------------------------------------------------------- # Parameters for 'zip' only # File extensions that should not be compressed (seperate with spaces) $zip_nocompress_types = 'mp3 MP3 Z z gz gif zip ZIP lha jpeg jpg JPG taz tgz deb rpm bz2 lzo'; # ---------------------------------------------------------------------- # Parameters for 'package delta' backups # Archive a list of all installed packages in the top level of each backup? # Can be 'false', 'true' (save it for any backup), or # 'rootonly' (saves list only if the filesystem is '/') $pkgdelta_archive_list = 'rootonly'; # Archive files not "owned" by packages? $pkgdelta_archive_unowned = 'true'; # Archive any package-owned files which have been modified? $pkgdelta_archive_changed = 'true'; # ---------------------------------------------------------------------- # Paths to commands. Default will look for them in $PATH. Use this if # you want to set commands explicitly. You can use full paths or just # change command names. # # Example: If GNU tar is called "gtar" on your system: # $path{'tar'} = 'gtar'; # # Or can be used to "sudo" certain commands. Examples: # $path{'find'} = 'sudo find'; # $path{'dump'} = 'sudo dump'; # $path{'afio'} = 'sudo -u nonrootuser afio'; # # ---------------------------------------------------------------------- # mt operation overrides. Set if flexbackup doesn't know the right mt # command(s) for your OS/device, or you want to override things. # # Example: use "mt status" instead of "mt tell" # $mt{'tell'} = 'status'; ###################################################################### # # $Id: flexbackup.conf,v 1.74 2003/09/21 22:59:58 edwinh Exp $ # $Name: v1_2_1 $ # # Leave '1;' on the the next line - for perl 'require' 1; flexbackup-1.2.1/flexbackup.conf.50100664000076400007640000002704107737566240016352 0ustar edwinhedwinh.. .de TQ .br .ns .TP \\$1 .. .TH "FLEXBACKUP.CONF" "5" "Oct 2003" "Flexbackup" .SH "NAME" /etc/flexbackup.conf \- a flexible backup/archiving tool configuration file .SH "DESCRIPTION" Flexbackup configuration file. Check \fBflexbackup\fR(1) or \(dq\fBflexbackup\fR \fI-help\fR\(dq for usage information. .SH "SETTINGS" .TP General Configuration Settings .RS .TP \fB$type\fR = \fI'afio'\fI; Which archive type to use. Supported types are: \fBafio\fR(1), \fBdump\fR(1), \fBtar\fR(1), \fBcpio\fR(1), \fBstar\fR(1), \fBpax\fR(1), \fBzip\fR(1), \fBlha\fR(1), \fBar\fR(1), \fBshar\fR(1). \fI'copy'\fR is another option if running in archive-to-disk mode. Use \fI'filelist'\fR to dump a list of files in your cwd - for debugging setup/exclusion. .TP \fB$set{\fI'tag'\fR}\fR = \fI'/dir'\fR; Configure backup \(dqsets\(dq. Not needed if \(dq-dir \(dq is used to backup one tree at a time. Each set is a simple space-separated list of filesystems/directories. Remote filesystems should be denoted as \(dqhost:directory\(dq. You can use anything (other than \fI'all'\fR) as set names. Using \(dq-set all\(dq will back up all defined sets. If you are doing a full backup using tapes, each \(dqset\(dq will go onto a different tape and you will be prompted for tape change in between. Examples: .RS .PP \fB$set{\fI'set1'\fI}\fR = \fI'/home /usr'\fR; .br \fB$set{\fI'set2'\fI}\fR = \fI'/dir3 machine2:/dir4 machine3:/dir5'\fR; .RE .TP \fB$prune{\fI'/'\fR}\fR = \fI'tmp proc'\fR; Configure subtree pruning. A space-separated list of directories to prune from each backup. Key is a filesystem/directory or \(dqhost:directory\(dq spec as outlined above regular expressions allowed (not shell-type wildcards!). .TP \fB$compress\fR = \fI'false|gzip|bzip2|lzop|zip|compress|hardware'\fR; .TQ \fB$compr_level\fR = \fI'4'\fR; Configure compression. Choose a type of compression to use and configure the level. The compression level only applies to \fIgzip/bzip2/lzop/zip\fR compression types. .TP \fB$buffer\fR = \fI'false|buffer|mbuffer'\fR; .TQ \fB$buffer_megs\fR = \fI'10'\fR; .TQ \fB$buffer_fill_pct\fR = \fI'75'\fR; .TQ \fB$buffer_pause_usec\fR = \fI'100'\fR; Configure buffering program to help streaming to tapes. Specify the buffer memory size in \fB$buffer_megs\fR, how full buffer needs to be to start writing in \fB$buffer_fill_pct\fR, and how long to sleep after every write (which helps sustain stream bursts) in \fB$buffer_pause_usec\fR. .TP \fB$device\fR = \fI'/dev/tape'\fR; Configure device to backup to. \fBNon-rewinding version, please!\fR. If the target is a directory, \fBflexbackup\fR will archive to files in that directory rather than to a device. If configured as \(dqhost:/dev/tapedevice\(dq, will use remote tape drive via rsh/ssha. Examples: .TS afCW s l. _ Linux SCSI w/devfs: /dev/tapes/tape0/mtn Linux SCSI: /dev/nst0 Linux IDE: /dev/nht0 Linux ftape: /dev/nqft0 FreeBSD SCSI: /dev/nrsa0 _ .TE .TP \fB$blksize\fR = \fI'10'\fR; Configure the block size (in kilobytes!) to use for archive programs and dd. Default is IfB10\fR for most things. Some tape drives need \fI32\fR or \fI64\fR. Set to \fI'0'\fR to disable all blocking. .TP \fB$mt_blksize\fR = \fI'0'\fR; Configure the block size (in bytes!) to use for the tape device, with \(dqmt setblk\(dq or equivalent. If set to \fI'0'\fR, will use \(dqvariable\(dq block size for the tape device (which is recommended). Comment out or set to \(dq\fB$blksize\fR * 1024\(dq to have it be the same as the archiver block size above. .TP \fB$pad_blocks\fR = \fI'true|false'\fR; Configure block padding. True to pad blocks to blocksize (devices only, not used when archiving to files). .TP \fB$mt{\fI'command'\fR}\fR = \fI'other'\fR; Configure \fBmt\fR operation overrides. Set these if \fBflexbackup\fR doesn't know the right \fBmt\fR command(s) for your OS/tape device, or you want to override things. Example: .RS .TP Use \fB\(dqmt \fIstatus\fR\(dq\fR instead of \fB\(dqmt \fItell\fR\(dq\fR \fB$mt{\fI'tell'\fR}\fR = \fI'status'\fR; .RE .RE .TP Other Global Settings .RS .TP \fB$remoteshell\fR = \fI'rsh|ssh|ssh2'\fR; Configure the command to use for remote shell. .TP \fB$remoteuser\fR = \fI''\fR; If not empty (or not set), the secondary username for remote shells. .TP \fB$label\fR = \fI'true|false'\fR; Somehow store identifying label in archive? .TP \fB$verbose\fR = \fI'true|false'\fR; Print each file? .TP \fB$sparse\fR = \fI'true|false'\fR; Handle sparse files? .TP \fB$indexes\fR = \fI'true|false'\fR; Set to \fI'false'\fR to turn off all table-of-contents support. .TP \fB$staticfiles\fR = \fI'true|false'\fR; If backing up to files, use static filenames \- no date stamp (same level backup of same directory will overwrite old backups). .TP \fB$atime_preserve\fR = \fI'true|false'\fR; Set to true to try and preserve file access times during backup, if the selected archive program can do so. Note that if this is true, -cnewer checks (file permission/status changes only, not content) are turned off when deciding which files to archive on the local system. .TP \fB$traverse_fs\fR = \fI'false|local|all'\fR; Span across filesytems? (backups of type \fB'dump'\fR will ignore this option). Set to \fI'false'\fR (don't), \fI'local'\fR (all but nfs/smbfs), or \fI'all'\fR (everything) .TP \fB$exclude_expr[\fI0\fR]\fR = \fI'.*'\fR; Exclude files that match these \fBregular expressions\fR (not shell wildcards) from the backups (backups of type \fB'dump'\fR will ignore this option). You can list more than one, just keep incrementing the index in the brackets for each. Also, strip off leading directories (the filesystem specs above or the \(dq-dir\(dq flag). Matches paths, not filenames, so put .* on the front/back as needed. Examples: .RS .PP \fB$exclude_expr[\fI0\fR]\fR = \fI'.*/[Cc]ache/.*'\fR; .br \fB$exclude_expr[\fI1\fR]\fR = \fI'.*~$'\fR; .RE .TP \fB$erase_tape_set_level_zero\fR = \fI'true|false'\fR; If set to true (default), and using a tape device, level 0 (full) \(dqset\(dq backup types assume you want to erase and use a new tape for each set. If set to false, level 0 (full) \(dqset\(dq backup types append to tapes. To force an erase for any backup, use \fI\(dq-erase\(dq\fR on the command-line. .TP \fB$erase_rewind_only\fR = \fI'true|false'\fR; Set this to \fB'true'\fR to make erase operations just call \fB\(dqmt \fIrewind\fR\(dq\fR - not \fB\(dqmt rewind\(dq\fR followed by \fB\(dqmt \fIerase\fR\(dq\fR. (For some tape drives, erase takes hours rather than seconds or is otherwise undesirable). .RE .TP Log, Stamps, and Binary Location Settings .RS .TP \fB$logdir\fR = \fI'/var/log/flexbackup'\fR; Directory for log files. .TP \fB$comp_log\fR = \fI'false|gzip|bzip2|lzop|compress|zip'\fR; Compress log? .TP \fB$staticlogs\fR = \fI'true|false'\fR; Use static log filenames with no date stamp? .TP \fB$prefix\fR = \fI''\fR; Log filenames will start with this prefix. .TP \fB$tmpdir\fR = \fI'/tmp'\fR; Used for temporary refdate files, etc. .TP \fB$stampdir\fR = \fI'/var/lib/flexbackup'\fR; Directory for backup timestamp files. .TP \fB$index\fR = \fI'/var/lib/flexbackup/index'\fR; Full path (without the .db extension) to the database filename for tape indexes. .TP \fB$keyfile\fR = \fI'00-index-key'\fR; Filename for keyfile if archiving to dir. .TP \fB$sprefix\fR = \fI''\fR; Stamp filenames will start with this prefix. .TP \fB$path{\fI'program'\fR}\fR = \fI'/path/to/program'\fR; Override paths to commands. By default \fBflexbackup\fR will look for them in \fI$PATH\fR. Use this if you want to set commands explicitly. You can use full paths or just change command names. Examples: .RS .TP If GNU \fBtar\fR is called \fB\(dqgtar\(dq\fR on your system: \fB$path{'tar'} = 'gtar'; .TP Or it can be used to \fB\(dqsudo\(dq\fR certain commands: \fB$path{\fI'find'\fR}\fR = \fI'sudo find'\fR; .br \fB$path{\fI'dump'\fR}\fR = \fI'sudo dump'\fR; .br \fB$path{\fI'afio'\fR}\fR = \fI'sudo -u nonrootuser afio'\fR; .RE .RE .TP Specific Command Settings .RS .TP \fB$afio_nocompress_types\fR = \fI'ext1 ext2 ...'\fR; .TQ \fB$afio_echo_block\fR = \fI'true|false'\fR; .TQ \fB$afio_compress_threshold\fR = \fI'3'\fR; .TQ \fB$afio_compress_cache_size\fR = \fI'2'\fR; These settings apply to the \fB'afio'\fR backup types only. Files with extensions specified in \fB$afio_nocompress_types\fR will not be compressed. Define whether or not echo block numbers in \fB$afio_echo_block\fR. Configure the minimum file size (in kilobytes) required for compression in \fB$afio_compress_threshold\fR. \fB$afio_compress_cache_size\fR setting is used to specify the maximum amount of memory (megabytes) to use for temporary storage of compression results. If a compressed file is bigger than this, compression will have to run twice on the file. See the \fBafio\fR(1) manpage for more information. .TP \fB$tar_echo_record_num\fR = \fI'true|false'\fR; These settings apply to the \fB'tar'\fR backup types only. Define whether or not echo record numbers in \fB$tar_echo_record_num\fR. .TP \fB$cpio_format\fR = \fI'newc'\fR; These settings apply to the \fB'cpio'\fR backup types only. Configure the format of the archive in \fB$cpio_format\fR. See the \fBcpio\fR(1) manpage for allowed formats. .TP \fB$dump_length\fR = \fI'0'\fR; .TQ \fB$dump_use_dumpdates\fR = \fI'true|false'\fR; These settings apply to the \fB'dump'\fR backup types only. Configure the estimated tape size (in kilobytes) using the \fB$dump_length\fR setting. This number doesn't really do much but help \fBdump\fR get size estimates if set to 0, \fBflexbackup\fR uses \fB'dump \fI-a\fR'\fR to determine this. Set \fB$dump_use_dumpdates\fR setting to \fI'true\fR to use \fB/etc/dumpdates\fR (could mess things up if you dump subdirectories of mount points). Set it to \fI'false'\fR to use \fBflexbackup\fR's internal timestamps. .TP \fB$star_fifo\fR = \fI'true|false'\fR; .TQ \fB$star_acl\fR = \fI'true|false'\fR; .TQ \fB$star_echo_block_num\fR = \fI'true|false'\fR; .TQ \fB$star_format\fR = \fI'exustar'\fR; These settings apply to the \fB'star'\fR backup types only. Define whether or not use fifo (buffering) in \fB$star_fifo\fR. If you set this to \fB'true'\fR you probably want to set \fB$buffer\fB = \fI'false'\fR (see above). Configure whether or not to handle ACLs in \fB$star_acl\fR. Define whether or not echo record numbers in \fB$star_echo_block_num\fR. Configure the format of the archive in \fB$star_format\fR. See the \fBstar\fR(1) manpage for allowed formats. .TP \fB$pax_format\fR = \fI'ustar'\fR; These settings apply to the \fB'pax'\fR backup types only. Configure the format of the archive in \fB$pax_format\fR. See the \fBpax\fR(1) manpage for allowed formats. .TP \fB$zip_nocompress_types\fR = \fI'ext1 ext2 ...'\fR; These settings apply to the \fB'zip'\fR backup types only. Files with extensions specified in \fB$zip_nocompress_types\fR will not be compressed. .TP \fB$pkgdelta_archive_list\fR = \fI'true|false|rootonly'\fR; .TQ \fB$pkgdelta_archive_unowned\fR = \fI'true|false'\fR; .TQ \fB$pkgdelta_archive_changed\fR = \fI'true|false'\fR; These settings apply to the \fB'pkgdelta'\fR backup types only. Configure whether to archive a list of all installed packages in the top level of each backup in \fB$pkgdelta_archive_list\fR. Can be \fI'false'\fR (don't), \fI'true'\fR (save it for any backup), or \fI'rootonly'\fR (saves list only if the filesystem is \fI'/'\fR). Define whether or not to archive files not \(dqowned\(dq by any package in \fB$pkgdelta_archive_unowned\fR. Specify whether or not to archive any package-owned files which have been modified in \fB$pkgdelta_archive_changed\fR. .RE .SH "FILES" /etc/flexbackup.conf \- configuration settings .SH "REPORTING BUGS" Report bugs to (flexbackup-help@lists.sourceforge.net) .SH "AUTHOR" Written by Edwin Huffstutler (edwinh@computer.org) .SH "SEE ALSO" \fBflexbackup\fR(1) flexbackup-1.2.1/faq.html0100664000076400007640000006046607741574735014664 0ustar edwinhedwinh flexbackup FAQ

flexbackup frequently-asked questions

Suggestions/additions welcome

FAQ Revised: Friday 10 October 2003 11:26:37


Table of Contents

1. Configuration
2. Backup
3. Recovery / Extraction
4. Common problems
5. Misc

1. Configuration

1.1. How do I upgrade from 1.0.x to 1.2.x?
Use the new default config file, then tweak as needed. Specific things that changed:
  1. Filesystems: the filesystem spec now allows an arbitrary "backup set" concept. In the config file, you can do things like:
          $set{'tag1'} = "/dir1 /dir2 ...";
          $set{'tag2'} = "/dir3 host:/dir4 ...";
  2. The commandline -fs switch became:
    • "-dir <dir>" backs up one directory tree (old "-fs <dir>")
    • "-set <tag>" backs up a named set. Useful new feature.
    • "-set all" acts like old "-fs all". If using tapes & level 0, each set is a different tape (acts like the old numeric index to @filesystems)
  3. mt blocksize: old configuration boolean $mt_var_blksize is now an integer, $mt_blocksize (in bytes).
    • Set to 0 for variable blocksize - old "true" value
    • Comment out or set to "$blksize * 1024" for old "false" value.
    • Or use some other value, like "512". This wasn't possible before.
  4. "-extract -files" turned into "-extract -onefile" or "-extract -flist" (functionality should be more clear now).


1.2. Why won't my exclude or prune expressions work?
Use regular expressions, not wild cards (aka globs). See a perl, sed, or grep reference, or "man 7 regex". Simplistically, you probably mean to say ".*" instead of "*", and ".?" instead of "?". Also, leave OFF the first part of a path that is the filesystem you are archiving; the exclude expressions only match sub-paths of the backup directory.

You can test exclusion, levels, pruning, and package deltas by using "-type filelist". This writes a text file listing all files that would have been backed up into your current working directory.

You can also test to see if your expression matches what you want to exclude by:

cd <topdir>; find . -regex "<expression>"

1.3. How can I avoid running backups or remote shells as root?
You can do this with 'sudo'.
  1. Set up a passphraseless ssh key for a user to run the backups.
  2. Set $path{command} = 'sudo command'; in the config file. Do this for all commands that will need privilege (find, afio/tar/etc)
  3. Set up an /etc/sudoers file with something like this:

    Cmnd_Alias BACKUP = /usr/bin/afio, /usr/bin/find
    backupuser ALL = NOPASSWD: BACKUP


1.4. On my system, "tar" is not GNU tar but is the vendor's "tar". GNU tar is installed as "gtar". On my system, "dump" is "ufsdump".
Use the path overrides at the end of the config file. A couple examples are given there, such as: $path{'dump'} = 'ufsdump';. You can use this mechanism to override paths or just change command names.

1.5. How can I back up Windows systems?
There are currently two ways:
  • Use "smbmount" to mount the windows share on a directory and back that up just like any other directory
  • flexbackup runs natively under cygwin

In the future we might add a way to set up "smbtar" or something similar...

1.6. Does the to-disk backup mode use single archive files or directory tree copies of files?
You can do either. The "copy" or "rsync" types will do a tree mirror of the selected files, other types make single-file archives in the $device dir.

Note that archive-to-disk with remote directories ($device = host:/path/to/dir) is not currently possible. You need to either run the backup from the machine with the disk, or use NFS.

1.7. How do I deal with spaces in set or prune directory specifications?
Good question, since these variables are set up as space-separated lists. Just enclose each item in single or double quotes:

$set{'backup'} = "'/path/file system 1' '/path/file system 2'";

1.8. Does flexbackup work with autoloaders / mtx?
Not currently. If you want help add it, let us know. See also 2.2 below.


2. Backup

2.1. Can you explain numeric dump levels?
Level 0 gets everything. For 1 or higher, files that have been changed since the last backup at a lower level are archived. For instance, if a level 2 backup was done on Monday, followed by a level 4 backup on Tuesday, a subsequent level 3 backup on Wednesday would contain all files modified or added since the level 2 (Monday) backup.

See also these sun docs for an example with some pictures.

Traditional Unix 'dump' behavior only allows levels 0-9; flexbackup removes this limit if you are using any of the non-dump archive formats.

For those people coming from other platforms that are not used to dump-style numeric levels, the latest versions of flexbackup have added -full, -differential, -incremental that get translated into appropriate dump-style level numbers. Definitions:

  • full: everything (duh!)
  • differential: only files that have been changed or added since the last full backup
  • incremental: only the files that have changed or been added to a system since doing the last backup (of any type)

These are simply aliased to numeric levels: full = 0, differential = 1, and incremental = (latest backup + 1)

2.2. Do you support multi-tape archives?
If you split filesystems/directories to be in different "sets", each set will go on a different tape for full backups (you'll be prompted).

As of now, having one directory archive span multiple tapes is not supported. True, some of the archiver programs can deal with end-of media if and only if they are writing directly to the device, but it can't be done in a shell pipeline, or in any kind of generic fashion. For now, split the directory up into workable portions, use larger media, or backup to disk...

We need a program like "buffer", but that also deals with the volume end detection & media change prompting, has configuraable "new tape" scripts, etc.

Version 1.2.1 has basic support for using mbuffer with multivolumes. Please try it out and let me know.

2.3. I'm backing up to files on disk -- How can I get filenames to not have timestamps in them?
Set $staticfiles to true in the config file. Archives will then have names such as "etc.3.dump.gz" rather than something like "home.0.200301212305.afio-bz2". There is a similar parameter for the log files, by the way.

2.4. What is the maxiumum size of the archives flexbackup can produce?
This is a limitation of the archive program, tape driver, underlying filesystem, OS, or specific compilation of perl - nothing in flexbackup controls that.

Some older programs/systems barf at 2GB files -- anything recent should deal with large files no problem, using standard APIs.

If you are using "buffer" be aware that you need to compile with extra flags, or patch the 1.19 version to handle >2GB files. (The RPMS linked to from this site have been)

See also Large File Support in Linux or Large file size changes to the single Unix spec for more info and hints on how to fix things if you need to recompile something.


3. Recovery / Extraction

3.1. How do I extract only certain files in an archive rather than the whole thing?
Use the -flist or -onefile switches.

To quickly extract just a single file, use "-extract -onefile path/to/my/file", giving the path from the archive.

To extract a list of multiple files, put them into a text file, for instance "restorelist", then use "-extract -flist restorelist". The format is one line per pathname, using the path of the file in the archive. Note if you are using afio with compression you need to append ".z" to filenames for any compressed files (depends on threshold and exclusion patterns).

3.2. How do I find out which archive(s) contain a certain file?
If you don't know in which archive to find a certain file, look at the log files. As long as you have verbose turned on (default), you can just 'zgrep filename /var/log/flexbackup/*.gz', and that works pretty well.

3.3. The disk totally died. How do I get flexbackup's archives off the tapes on a machine without flexbackup or even perl installed?
Something like this, if using compression with tar/cpio/dump:

dd if=/dev/tapedevice bs=10k | gzip -dc | tar xvf -

replace "tar xvf -" with "restore -i -f -" or whatever. Skip the gzip pipe if you aren't using compression. afio is a bit more complex, you might need something like:

afio -i -k -x -P bzip2 -Q -d -Z -v

Check the manpages for the archive utility you used...

3.4. I don't have the index files! (disk died, different machine, etc.)
This is OK, you can still extract/restore/etc. The index files are for human convenience only. Just skip the first file on the tape, it's a simple "tag" text file. The rest of the tape is your normal archives.


4. Common problems

4.1. I get I/O errors or other errors from my tape drive.
Run this: flexbackup -test-tape-drive. It writes a couple small files to the tape, then reads & diffs them. This will flush out any problems with parameters like blocking, filemarks, and padding; or issues with the tape drive or driver itself.

If it fails, try these one at a time:

  1. If you are using buffering, turn it off ($buffer='false') until you can get things working without it.
  2. Change the block size parameter ($blksize) in the configuration file. The default is 10k, but some tape drives like 32k or 64k much better.
  3. Try setting $pad_blocks to false
  4. Switch the setting of $mt_blocksize.
  5. Try with with $indexes set to false

If you are using an IDE tape drive under Linux, and are still having trouble, you might also try the 'ide-scsi' layer with the 'st' module, and treating the thing like a SCSI tape - I've had more success that way.

You can also just try writing/reading stuff straight from the drive with an archiver and dd (to factor flexbackup out of the loop), although that's what the -test-tape-drive switch tries to do. If you can get that working, but flexbackup still seems to malfunction, let us know. Try things like: (The example below uses afio to test, tar/cpio/others will be different)

  # Backup two dirs
  mt -f /dev/tape setblk 32768
  mt -f /dev/tape rewind
  mt -f /dev/tape erase
  find /dir1 -print | afio -o -z -v -b 32k - | dd ibs=32k obs=32k of=/dev/tape
  find /dir2 -print | afio -o -z -v -b 32k - | dd ibs=32k obs=32k of=/dev/tape

  # List
  mt -f /dev/tape rewind
  dd ibs=32k obs=32k if=/dev/tape | afio -t -z -v -b 32k -
  dd ibs=32k obs=32k if=/dev/tape | afio -t -z -v -b 32k -


4.2. Backups seem to work ok, but I can't get extracting/listing to work.
Usually a matter of tape positioning, or else is a tape drive parameter problem as talked about above. For positioning, use 'mt rewind; mt fsf <n>' to get to the right file number on the tape, or use the "-num" switch with "-list|extract|compare|restore" to have flexbackup do it for you. If you are using flexbackup's indexes, remember that file number 0 is a header used for the index itself.

4.3. How do I get remote backups over ssh to work without prompting for a password?
For rsh, use .rhosts or hosts.equiv. But you really should NOT be using rsh.

If you are using ssh and are having a problem with it asking for a password, then you don't have a passphrase-less authorized key set up, your RSAAuthentication parameters are incorrect, or you have some other public/private key problem. Section 7.2.2 of the ssh FAQ might help:

http://www.onsight.com/faq/ssh/ssh-faq.html

In a nutshell the value of your ~/.ssh/identity.pub needs to be present in your ~/.ssh/authorized_keys file for RSA authentication to work. For root accounts you might also need to put

PermitRootLogin yes

in your sshd_config file, as root is treated with more care than normal user accounts. Note: you might choose without-password or forced-commands-only instead of yes for more security. See your ssh(1) and sshd(1) man pages.

If you do not want to use passphrase-less ssh keys with root logins, See the section on use with "sudo". (Or else sit at the terminal and type passwords during the backups if you *really* want to...)

4.4. I keep getting only one backup per tape when I do "-set all"!
Use the non-rewinding device.

4.5. 'buffer' complains that it can't run successfully or that it doesn't have enough shared memory.
Set $buffer_megs to a lower value and try again. I usually run with it set somewhere between 5-20MB.

On FreeBSD, SysV shared memory can run low using the GENERIC kernel at its default value (especially if you are running something like GNOME). You can modify your kernel file and change the line

options SHMMAXPGS=2048

To something really big. You can also change this dynamically at startup by putting

kern.ipc.shmmax={large integer}

in /etc/sysctl.conf. Other BSD's will have very similar mechanisms.

4.6. When using -newtape or -erase, the tape drive sits there forever making noise and nothing happens on the screen
Some types of tape drives/drivers take an extremely long time to do an "mt erase" operation, or its unnecessary. If you suspect this is the case, check to see if "mt erase" process is running. In these cases, you can set the config variable $erase_rewind_only to true - then an "erase" operation becomes just a rewind. If an archive is then written, filemarks and tape end-of-data should work fine.

Also, this hang can be caused by incorrect tape drive parameters on some systems. Try "flexbackup -test-tape-drive". If it hangs, and there is a dd or buffer process continuously running, kill it and adjust blocksizes and/or padding.

4.7. My "mt" or tape drive doesn't support operation xxxx.
mt commands definitely vary greatly from platform to platform. We try to pick the best default, but it won't be right for everyone. To override mt operation 'xxxx', just put $mt{'xxxx'} = 'yyyy' in the config (see the comments near the end). For instance, to make flexbackup use "mt rdspos" instead of "mt rdhpos, use:

$mt{'rdhpos'} = 'rdspos';

4.8. Flexbackup caused my system to hang!
No it didn't. Give me a break, it's a script. If you are asking this question most likely your tape driver lost its mind and hung the system somehow.

4.9. The backup looks like it works ok, but flexbackup complains of an error at the end of the archive, then exits.
If flexbackup detects a non-zero exit status for any command in the pipeline, it will stop. Something probably happened further up in the log that caused the archive or buffering program to exit with an error status. Look over the log again, or run with "-d verbose=false" to turn off the printing of filenames to make it more obvious. See also the next question.

4.10. I get an error from the backup due to files moving/changing/being deleted during the backup
You really shouldn't be doing that!

If flexbackup detects a non-zero exit status for any command in the pipeline, it will stop. You can override this with the "--ignore-errors" switch, but then it will just blindly continue on such cases. You have been warned.

Some archivers are more picky than others. You can try switching the archive type.

If it is something like a SQL database data file causing the problem, you should dump the database to a secondary location and back THAT data up. (See the mysqldump command, for instance).


5. Misc

5.1. How does the "packaging system delta" (or "-pkgdelta") feature work?
When you have OS CD's sitting around, and/or you know darn well that 90% of the non-user, non-data files on your system you can get off the 'net easily, there is no need to back up "stock" files.

Flexbackup can use a "package delta" mode that cuts down the number of files to be archived by basically doing the following:

  1. List the files in the directory/filesystem to be backed up, using the normal level & timstamp mechanism
  2. Subtract any files that are "owned" by packages.
  3. Add back any package files modified from the distributed version (optional)
  4. Backup the files in the list.

It was orginally intended for use with RPM-based systems ("-pkgdelta rpm"). Initial support for FreeBSD packages is included ("-pkgdelta freebsd"), although there the "base" files aren't part of package so they can't be excluded.

If someone knows how to do the equivalent things for .deb packages or others, let me know and we can add it.

5.2. What exactly is the "-wday" switch for?
To help cron do things like once-a-month, certain day of the week jobs. With crontab fields, you can easily set up something to run on certain day(s) of the week, or on certain day(s) of the month.

Now, tell me you how to set up a cron job to run once a month on the first Sunday only.... you can't do it.

The "-wday" flag tells flexbackup to just exit if the current day of the week does not match the flag. Now, to fulfill the above requirement, you add a cron entry like:

   0 3 1-7 * * flexbackup -wday 7 -set all ....
Which will run at 3:00AM on the first Sunday of every month.

5.3. How can I tell flexbackup to read from stdin or backup to stdout?
See the "-pipe" switch. Only lets you do one directory at a time, obviously. You can use this to interact with other programs in a pipeline.

5.4. Why keep the table of contents db on disk rather than on the tape?
  • We don't know the table size ahead of time unless we use a hard-limited reserved size.
  • Updating the db at the beginning of the tape will add tape-travel overhead for every backup.
  • a backup->rewind->write start of tape sequence will lose the rest of the contents past the first file unless we start to play filemark tricks.


5.5. This index stuff is a useless mess/I can't get it to work!
It isn't necessary if you don't like it. Set $indexes = 'false'; in the config file, and just use written labels on the tapes...


flexbackup-help at lists dot sourceforge dot net

FAQ compilation thanks to makefaq