Skip to content
Snippets Groups Projects
dbackup 45.6 KiB
Newer Older
					)."\n"
				);
			}
			# Finally close
			$gz->gzclose();
		}

		# State clear
		delete($origFileList{$path});
	}

	printLog(LOG_NOTICE,"BACKUP START: $source => $dest\n");
	printLog(LOG_INFO,"Compression: ".$config{'compress'}." [".$config{'compress-'.$config{'compress'}}."]\n");

	# Check if we excluding system files
	if ($config{'exclude-system'}) {
		printLog(LOG_NOTICE,"Exclude System Base: ".join(", ",@{$config{'system-base'}})."\n");
		printLog(LOG_NOTICE,"Exclude System Dirs: ".join(", ",@defaultSystemExcl,@{$config{'system-dir'}})."\n");
		# If we excluding data too...
		if ($config{'exclude-data'}) {
			printLog(LOG_NOTICE,"Exclude Data Dirs: ".join(", ",@defaultDataExcl,@{$config{'data-dir'}})."\n");
		}
	}
	printLog(LOG_NOTICE,"Exclude Paths: ".join(", ",@{$config{'exclude-path'}})."\n");
	printLog(LOG_NOTICE,"Exclude Files: ".join(", ",@{$config{'exclude-file'}})."\n");

	# We need an entry for our main dir before we start
	$doBackup{""} = 0;

	# This basically does our backup for us...
	find (
		{
			preprocess => \&backup_preprocess,
			wanted => \&backup_process,
			postprocess => \&backup_postprocess
		},
		$source
	);


	printLog(LOG_INFO,"Processing stale directories...\n");
	my @rmlist;
	# Total item by item
	find (
		{
			wanted => sub { },
			# We just need post processing ...
			postprocess => sub {
				# Strip first part of the path
				(my $path = $File::Find::dir) =~ s#^$dest/?##;
				# Does this dir exist on the backup, but not on the fileserver?
				if (!defined($srcFileList{$path})) {
					push(@rmlist,$path);
				}
			},
		},
		$dest
	);
	foreach my $rmitem ( @rmlist ) {
		printLog(LOG_DEBUG,"Remove path '[$dest]/($rmitem)'\n");
   		rmtree("$dest/$rmitem");
	}

	printLog(LOG_NOTICE,"BACKUP END\n");
}



# If we backing up or restoring, we need to check the compression program
if (defined($optctl{'backup'}) || defined($optctl{'restore'})) {
	# One last check for to make sure the compression program exists
	if (checkPATH(my $compressProgram = "compress-".$config{'compress'})) {
		print(STDERR "ERROR: Compression program '$compressProgram' cannot be found in path!");
		exit 1;
	}
}
# Check if we backing up
if (defined($optctl{'backup'})) {
	backup($sourceDir,$destDir);

# Or restoring...
} elsif (defined($optctl{'restore'})) {
	restore($sourceDir,$destDir);

} else {
	print(STDERR "ERROR: No command given\n\n");
	displayHelp();
	exit 1;
}


# Restore function
sub restore
{
	our ($source,$dest) = @_;

	# State infor for current dir
	our %origFileList;
	our %origDirList;
	our %origPathAttribs;


	# Preprocess dir
	sub restore_preprocess {
		my @list = @_;

		# Strip first part of the path
		(my $path = $File::Find::dir) =~ s#^$source/?##;

		# Check if we have all our source files
		if ( ! -f "$source/$path/.dbackup-state" ) {
			printLog(LOG_WARNING,"S: Path '[$source]/($path)' is MISSING .dbackup-state\n");
			return @list;
		}

		# Load state file
		loadStateFile("$source/$path/.dbackup-state",$path,\%origFileList,\%origDirList,\%origPathAttribs);

		# Check we have at least the version attribute
		if (!defined($origPathAttribs{$path}) || !defined($origPathAttribs{$path}->{'dbackup.version'})) {
			printLog(LOG_ERROR, "No dbackup version information found in '[$source]/($path)/.dbackup-state', IGNORING\n");
			delete($origDirList{$path});
			delete($origFileList{$path});
			return @list;
		}

		# During our check, we need to create the dir, we may not have a backup file
		# so we do it here.
		if ( ! -d "$dest/$path") {
			printLog(LOG_INFO,"R: Path '[$dest]/($path)' creating directory\n");
			mkpath("$dest/$path");
		}

		# The lack of a sequence number probably means we didn't output any tar files during backup
		if (!defined($origPathAttribs{$path}->{'sequence'})) {
			return @list;
		}

		# Loop through backup sequences
		for (my $seq = 0; $seq <= $origPathAttribs{$path}->{'sequence'}; $seq++) {
			printLog(LOG_DEBUG,"R: Path '[$dest]/($path)' restoring data ".($seq+1)." of ".
					($origPathAttribs{$path}->{'sequence'}+1)."\n");

			# Args for tar
			my @tarArgs = ();

			my $tarExt;
			if ($origPathAttribs{$path}->{'compression'} eq "xz") {
				push(@tarArgs,"--use-compress-program",$config{'compress-xz'});
				$tarExt = ".xz";
			} elsif ($origPathAttribs{$path}->{'compression'} eq "bzip2") {
				push(@tarArgs,"--use-compress-program",$config{'compress-bzip2'});
				$tarExt = ".bz2";
			} elsif ($origPathAttribs{$path}->{'compression'} eq "gzip") {
				push(@tarArgs,"--use-compress-program",$config{'compress-gzip'});
				$tarExt = ".gz";
			} elsif ($origPathAttribs{$path}->{'compression'} eq "lz") {
				push(@tarArgs,"--use-compress-program",$config{'compress-lz'});
				$tarExt = ".lz";
			} elsif ($origPathAttribs{$path}->{'compression'} eq "none") {
				$tarExt = "";
			}

			# Check what type of backup we did
			if ( ! -f "$source/$path/dbackup$seq.tar$tarExt" ) {
				# If we have any other part of the backup, show a message
				if (
							( -f "$source/$path/dbackup$seq.index" ||
							-f "$source/$path/dbackup$seq.manifest" ||
							-f "$source/$path/dbackup$seq.snar" ) && $seq > 0
				) {
					printLog(LOG_WARNING,"Backup file '[$source]/($path)/dbackup$seq.tar$tarExt' not found, IGNORING\n");
				}

				return @list;
			}

			# Check we have files we need/created
			if ( ! -f "$source/$path/dbackup$seq.index" ) {
				printLog(LOG_WARNING,"Manifest file '[$source]/($path)/dbackup$seq.index' not found\n");
				return @list;
			}
			if ( ! -f "$source/$path/dbackup$seq.manifest" ) {
				printLog(LOG_WARNING,"Manifest file '[$source]/($path)/dbackup$seq.manifest' not found\n");
				return @list;
			}

			# Check if manifest.format is undefined, if it is, use the default
			if (!defined($origPathAttribs{$path}->{'manifest.format'})) {
				# Check if we have a commandline override
				if (defined($optctl{'manifest-format'})) {
					$origPathAttribs{$path}->{'manifest.format'} = $optctl{'manifest-format'};
				} else {
					# NK:
					# Default to null
					# This saves us from having to regenerate manifest.format attrs and backups for older
					# versions of dbackup.
					$origPathAttribs{$path}->{'manifest.format'} = "null";
				}
			}

			# Setup manifest delim & tar options
			if ($origPathAttribs{$path}->{'manifest.format'} eq "null") {
				push(@tarArgs,"--null");
				push(@tarArgs,"--files-from", "$source/$path/dbackup$seq.manifest");
				push(@tarArgs,"--no-unquote");
			} elsif ($origPathAttribs{$path}->{'manifest.format'} eq "newline") {
				push(@tarArgs,"--files-from", "$source/$path/dbackup$seq.manifest");
			} else {
				printLog(LOG_ERROR,"Invalid manifest.format '".$origPathAttribs{$path}->{'manifest.format'}.
						"' for '[$source]/($path)', try setting the default with --manifest-format=\n");
				exit 1;
			}

			# Sanity check
			if ($tarVer < 122 && $origPathAttribs{$path}->{'manifest.format'} eq "null") {
				printLog(LOG_ERROR,"Your version of tar does NOT support manifest.format of 'null'\n");
				exit 1;
			}

			# Other tar options
			if (defined($optctl{'tar-keep-newer'})) {
				push(@tarArgs,"--keep-newer-files");
			}
			if (defined($optctl{'tar-keep-old-files'})) {
				push(@tarArgs,"--keep-old-files");
			}

			# Change dir and start backup
			system(
				"tar",
				"--extract",
				# Output file
				"--file", "$source/$path/dbackup$seq.tar$tarExt",
				# cd into here first...
				"--directory", "$dest",
				# Do not recurse
				"--no-recursion",
				# Incremental options
#			"--incremental",
				@tarArgs
			);
			if ($? == -1) {
				printLog(LOG_ERROR,"Failed to execute: $!\n");
				exit 1;
			} elsif ($? & 127) {
				printLog(LOG_ERROR,"Child died with signal ".($? & 127)."\n");
				exit 1;
			} else {
				my $retcode = $? >> 8;
				# If tar died, lets die too
				if ($retcode >= 2) {
					printLog(LOG_ERROR,"tar died with error code $retcode\n");
#					exit 1;
				}
			}
		}

		return @list;
	}

	# Process files
	sub restore_process {
		my $name = $File::Find::name;
		# Strip first part of the path
		(my $path = $File::Find::dir) =~ s#^$source/?##;

		# Store current filename
		my $item = $_;

#		print(STDERR "PROCESS: Path = $path, Name = $name, Item = $item)\n");
	}


	# Time to do stuff
	sub restore_postprocess {
		# Strip first part of the path
		(my $path = $File::Find::dir) =~ s#^$source/?##;

		printLog(LOG_DEBUG,"R: Path '[$dest]/($path)' restoring meta-data\n");

		# Loop with directories in this path
		foreach my $dname (keys %{$origDirList{$path}}) {
			# Set dir
			my $dir = $origDirList{$path}->{$dname};

			# Full dirname
			my $fdirname = "$dest/$path/$dname";

			# Could be a dir with no contents of one which was ignored, in which case we must create it
			if (! -d $fdirname) {
				printLog(LOG_INFO,"R: Path '[$dest]/($path)/$dname' creating missing directory\n");
				mkpath($fdirname);
			}

			# Restore attribs
			if (!chown($dir->{'uid'},$dir->{'gid'},$fdirname)) {
				printLog(LOG_ERROR,"Failed to chown(".$dir->{'uid'}.",".$dir->{'gid'}.") '$fdirname': $!\n");
			}
			if (!chmod($dir->{'mode'},$fdirname)) {
				printLog(LOG_ERROR,"Failed to chmod(".$dir->{'mode'}.") '$fdirname': $!\n");
			}
			if (!utime($dir->{'atime'},$dir->{'mtime'},$fdirname)) {
				printLog(LOG_ERROR,"Failed to utime(".$dir->{'atime'}.",".$dir->{'mtime'}.") '$fdirname': $!\n");
			}
		}

		# Loop with files in this path
		foreach my $fname (keys %{$origFileList{$path}}) {
			# Set dir
			my $file = $origFileList{$path}->{$fname};

			# Full dirname
			my $ffilename = "$dest/$path/$fname";

			# Restore for links and files
			if (!lchown($file->{'uid'},$file->{'gid'},$ffilename)) {
				printLog(LOG_ERROR,"Failed to lchown(".$file->{'uid'}.",".$file->{'gid'}.") '$ffilename': $!\n");
			}

			# Ignore links for the rest...
			next if (S_ISLNK($file->{'mode'}));

			# Restore mode & utime only for files
			if (!chmod($file->{'mode'},$ffilename)) {
				printLog(LOG_ERROR,"Failed to chmod(".$file->{'mode'}.") '$ffilename': $!\n");
			}
			if (!utime($file->{'atime'},$file->{'mtime'},$ffilename)) {
				printLog(LOG_ERROR,"Failed to utime(".$file->{'atime'}.",".$file->{'mtime'}.") '$ffilename': $!\n");
			}
		}
	}

	printLog(LOG_NOTICE,"RESTORE START: $source => $dest\n");

	# This basically does our backup for us...
	find (
		{
			preprocess => \&restore_preprocess,
			wanted => \&restore_process,
			postprocess => \&restore_postprocess
		},
		$source
	);

	printLog(LOG_NOTICE,"RESTORE END\n");
}


# Display log line
sub printLog {
	my ($level,$msg,@args) = @_;


	# Work out level txt all nicely
	my $levelTxt = "UNKNOWN";
	if ($level == LOG_DEBUG) {
		$levelTxt = "DEBUG";
	} elsif ($level == LOG_INFO) {
		$levelTxt = "INFO";
	} elsif ($level == LOG_NOTICE) {
		$levelTxt = "NOTICE";
	} elsif ($level == LOG_WARNING) {
		$levelTxt = "WARNING";
	} elsif ($level == LOG_ERROR) {
		$levelTxt = "ERROR";
	}

	# Check log level
	if ($level <= $config{'log-level'}) {
		printf(STDERR "%s/$levelTxt: %s", strftime('%F %T',localtime()), $msg, @args);
	}
}



# Display usage
sub displayHelp {
	# Build some strings we need
	my $systemDirStr = join (", ",@defaultSystemExcl);
	my $dataDirStr = join (", ",@defaultDataExcl);

	print(STDERR<<EOF);
Usage: $0 [args] <src> <dst>

    General Options:
      --help                             What you seeing now.
      --config=file                      Config file to use.
      --log-level                        5 = debug, 4 = info, 3 = notice
                                         2 = warning, 1 = error
      --tar                              Path to tar binary.

    Backing Options:
      --backup                           Backup src to dst.
      --backup-upgrade                   Upgrade backup to new dbackup ver.
      --compress=<xz|bz2|gzip|lz|none>   Compression method to use defaults
                                         to using xz, or bzip2 if xz unavail.

      --exclude-data                     Exclude all data dirs listed below.
      --exclude-system                   Exclude system dirs listed below.

      --exclude-path=pcre                PCRE to exclude paths from backup.
      --exclude-file=pcre                PCRE to exclude files from backup.

      --exclude-fs-type=fstype           Filesystem type to exclude.

      --data-dir=dir                     Add an additional data directory.
      --system-dir=dir                   Add an additional system directory.
      --system-base=path                 Add a system base. This defaults to /
                                         and this option will override that.

      --tar-ignore-failed-read=path      This is passed to tar only. It will
                                         not cause errors of files that cannot
                                         be read for the path matched.

    Restore Options:
      --restore                          Restore src to dst.
      --tar-keep-newer                   Do not overwrite newer files.
      --tar-keep-old-files               Don't replace existing files.


Builtins:
---------
    system-dirs:
      $systemDirStr

    data-dirs:
      $dataDirStr
EOF
}


#
# LIBRARY
#

sub loadStateFile {
		my ($statefile,$key,$fileList,$dirList,$pathAttribs) = @_;


		# Read in backup state file, we do it here because we may not have a backup file
		# to restore
		my $gz = gzopen($statefile,"rb")
				or die "FATAL ERROR: Failed to open '$statefile': $!";
		# Read in file list
		my $line;
		while ($gz->gzreadline($line) > 0) {
			chomp($line);
			# Pull out array of items
			my @aline = split(/\0/,$line);
			my $type = shift(@aline);
			# Check if its defined
			if (defined($type)) {
				# If its a file...
				if ($type eq "f") {
					my ($ename,$size,$ctime,$atime,$mtime,$uid,$gid,$mode) = @aline;
					my $name = decode_base64($ename);
					# Setup our hash
					$fileList->{$key}->{$name}->{'size'} = $size;
					$fileList->{$key}->{$name}->{'ctime'} = $ctime;
					$fileList->{$key}->{$name}->{'atime'} = $atime;
					$fileList->{$key}->{$name}->{'mtime'} = $mtime;
					$fileList->{$key}->{$name}->{'uid'} = $uid;
					$fileList->{$key}->{$name}->{'gid'} = $gid;
					$fileList->{$key}->{$name}->{'mode'} = $mode;
				# If its a dir
				} elsif ($type eq "d") {
					my ($ename,$ctime,$atime,$mtime,$uid,$gid,$mode) = @aline;
					my $name = decode_base64($ename);
					# Setup our hash
					$dirList->{$key}->{$name}->{'ctime'} = $ctime;
					$dirList->{$key}->{$name}->{'atime'} = $atime;
					$dirList->{$key}->{$name}->{'mtime'} = $mtime;
					$dirList->{$key}->{$name}->{'uid'} = $uid;
					$dirList->{$key}->{$name}->{'gid'} = $gid;
					$dirList->{$key}->{$name}->{'mode'} = $mode;
				# Attribute
				} elsif ($type eq "a") {
					my ($name,$value) = @aline;
					$pathAttribs->{$key}->{$name} = $value;
				# Unknown
				} else {
					print(STDERR "ERROR: Invalid type '$type' in '$statefile'\n");
					exit 1;
				}
			}
		}
		# Close gzip file
		$gz->gzclose();
}


# Remove backup and its sequences
sub removeBackups
{
	my ($path,$format,$compression,$seqs) = @_;


	# Loop through backup sequences
	for (my $i = 0; $i <= $seqs; $i++) {

		my $tarExt;
		if ($compression eq "xz") {
			$tarExt = ".xz";
		} elsif ($compression eq "bzip2") {
			$tarExt = ".bz2";
		} elsif ($compression eq "gzip") {
			$tarExt = ".gz";
		} elsif ($compression eq "lz") {
			$tarExt = ".lz";
		} elsif ($compression eq "none") {
			$tarExt = "";
		}

		# Unlink backup files
		if (-f "$path/dbackup$i.tar$tarExt") {
			unlink("$path/dbackup$i.tar$tarExt")
					or die "ERROR: Failed to remove file '$path/dbackup$i.tar$tarExt': $!";
		}
		# Unlink manifest
		if (-f "$path/dbackup$i.manifest") {
			unlink("$path/dbackup$i.manifest")
					or die "ERROR: Failed to remove file '$path/dbackup$i.manifest': $!";
		}
		# Unlink index
		if (-f "$path/dbackup$i.index") {
			unlink("$path/dbackup$i.index")
					or die "ERROR: Failed to remove file '$path/dbackup$i.index': $!";
		}
		# Unlink snar
		if (-f "$path/dbackup$i.snar") {
			unlink("$path/dbackup$i.snar")
					or die "ERROR: Failed to remove file '$path/dbackup$i.snar': $!";
		}

	}
}


# Convert a possible string to an array, or return the array if it is indeed an array
sub toArray
{
	my $param = shift;

	if (ref $param eq "ARRAY") {
		return $param;
	} else {
		return [ split(/\n/,$param) ];
	}
}


# Grab tar version
sub getTarVer
{
	my $tar = shift;


	# Open tar and grab its version string
	open(my $ph, "-|", $config{'tar'} . " --version")
		or die "FAILED to execute '".$config{'tar'}."': $!";
	if (!($tarVer = <$ph>)) {
		print(STDERR "ERROR: Failed to read tar version\n");
		exit 1;
	}
	close($ph);

	# Convert version string into integer and return it
	($tarVer) = ($tarVer =~ /([0-9]+\.[0-9]+)/);
	if (!defined($tarVer) || $tarVer eq "") {
		print(STDERR "ERROR: Failed to parse tar version\n");
		exit 1;
	}
	$tarVer =~ s/\.//g;
	if ($tarVer < 100) {
		print(STDERR "ERROR: Failed to read tar version or your version is simply too old\n");
		exit 1;
	}

	return $tarVer;
}


# Convert a triplet version into an integer
sub getNumericVer
{
	my $a = shift;

	# 000 000 000
	my @a = split(/\./,$a);

	# Quick fix for devel versions
	$a[2] = 0 if ($a[2] eq "x");
	$a[2] =~ s/[a-z]$//;

	return ( ($a[0]*1000000) + ($a[1]*1000) + $a[2] );
}


# Function to check if a binary is in our PATH
sub checkPATH
{
	my $binary = shift;

	# Loop with PATH components
	for my $path (split(/:/,$ENV{PATH})) {
		# And check if the binary is executable
		if (-x "$path/$binary") {
			return 1;
		}
	}

	return 0;
}


# vim: ts=4