/
usr
/
local
/
stat_watch
/
modules
/
File Upload :
llllll
Current File: //usr/local/stat_watch/modules/backup.pm
use warnings; use strict; package SWBackup; my $v_captured_md5; my @v_captured_stats; my $b_do_backup; sub fn_backup_initial { ### Given a Stat Watch report, backup the files within that match the "BackupR" and "Backup+" control strings ### $_[0] is the report my $v_file = $_[0]; $Main::b_md5_all = 0; my @v_lines = &Main::fn_diff_check_lines(undef, 1, $v_file); for my $_line (@v_lines) { chomp($_line); if ( $_line =~ m/^(Maximum depth reached at |Processing: )'/ ) { next; } my @v_file = split( m/'/, $_line ); $_line = pop(@v_file); shift(@v_file); my $v_file = join( "'", @v_file ); my @v_line = split( m/ -- /, $_line ); if ( scalar(@v_line) == 8 ) { if ( length($v_line[7]) == 32 ) { ### If there's an md5sum, turn on md5sum use $Main::b_md5_all = 1; } } fn_backup_file($v_file, $Main::d_backup); $Main::b_md5_all = 0; } } sub fn_check_retention { ### Given the name of a backed-up file, check to ensure that there aren't old copies that exceed the retention limits. If there are, remove them ### $_[0] is the full path to that file in the backup directory, but with the trailing underscore and timestamp removed my $v_file = $_[0]; my @v_dirs = split( m/\//, $v_file ); my $v_name = pop(@v_dirs); my $v_dir = join( '/', @v_dirs ); my @v_files = fn_list_backups($v_name, $v_dir); ### Sort the matching files in reverse @v_files = sort {$b cmp $a} @v_files; ### skip over the most recent X files, where X is the retention count my $v_count = scalar(@v_files) - 1; if ( $Main::v_retention_max_copies > -1 ) { ### Go through the backups from oldest to newest; remove the ones that are outside of the parameters while ( $v_count >= $Main::v_retention_max_copies ) { my $v_file = $v_dir . "/" . $v_files[$v_count]; ### Get the string of numbers from the end of the file name my $v_stamp = (split( m/_/, $v_files[$v_count] ))[-1]; ### Note, because of the added digit, everything gets multiplied by 10 if ( ((time() * 10) - $v_stamp) > (864000 * $Main::v_retention_min_days) ) { ### Delete anything outside of the retention count that's too old and that doesn't have a "_hold" file if ( ! -f $v_file . "_hold" ) { my $v_file_escape = &SWEscape::fn_escape_filename($v_file); &Main::fn_log("Removing backed-up file " . $v_file_escape . "\n"); unlink( $v_file ); if ( -f $v_file . "_comment" ) { unlink( $v_file . "_comment" ); } if ( -f $v_file . "_stat" ) { unlink( $v_file . "_stat" ); } if ( -f $v_file . "_md5" ) { unlink( $v_file . "_md5" ); } if ( -f $v_file . "_pointer" ) { unlink( $v_file . "_pointer" ); } } elsif ( -f $v_file . "_pointer" ) { ### If this backup would have been deleted but for the fact that there's a hold file, AND this backup is a pointer ### If a pointer backup is held, we need to make sure that the content it points to does not get rotated out ### Find the file that it points to my $f_content = fn_backup_details($v_file, 2); my $v_content_stamp = (split( m/_/, $f_content ))[-1]; ### If the content file is at risk of being deleted, replace this pointer file with content ### Note, because of the added digit, everything gets multiplied by 10 if ( ((time() * 10) - $v_content_stamp) > (864000 * $Main::v_retention_min_days) && ! -f $f_content . "_hold" ) { unlink($v_file); ### This should be the ONLY instance where a file with a newer stamp is pointing to a file with an older stamp ### (And it will remedy itself as pruning continues) fn_create_pointer($f_content, $v_file); unlink($v_file . '_pointer'); } elsif ($v_stamp > $v_content_stamp) { ### In the edge case where two pointer backups were being held, and the file they were pointing to is going to be deleted, ### The content will initially end up with the older of the two backup stamps ### This is where we fix it unlink($v_file); fn_create_pointer($f_content, $v_file); unlink($v_file . '_pointer'); } } } $v_count--; } } } sub fn_prune_backups_main { ### This is the main function that's run with the "--prune" option my @v_dirs = @_; for my $_dir (@v_dirs) { if ( -d $_dir ) { ### Note the start time for pruning the backup if ( open( my $fh_write, ">", $_dir . "/__last_prune" ) ) { print $fh_write time(); close($fh_write); } fn_prune_backups($_dir); ### And the overwrite the start time with the time when we finished if ( open( my $fh_write, ">", $_dir . "/__last_prune" ) ) { print $fh_write time(); close($fh_write); } } } } sub fn_prune_backups { ### When pruning backups wee will recurse through this function ### $_[0] is the backup directory my $v_dir = $_[0]; if ($Main::b_verbose) { my $v_dir_escape = &SWEscape::fn_escape_filename($v_dir); print STDERR "Directory: " . $v_dir_escape . "\n"; } ### Capture what the pruning settings are currently my $v_cur_mc = $Main::v_retention_max_copies; my $v_cur_md = $Main::v_retention_min_days; if ( -e $v_dir && -d $v_dir ) { if ( -f $v_dir . '/__prune_rules' ) { my $v_rules = fn_read_first_line( $v_dir . '/__prune_rules' ); if ( $v_rules =~ m/^[0-9]+:[0-9]+$/ ) { ( $Main::v_retention_max_copies, $Main::v_retention_min_days ) = split( m/:/, $v_rules ); } } ### Open the directory and get a file list if ( opendir my $fh_dir, $v_dir ) { my @files = readdir $fh_dir; closedir $fh_dir; my @dirs; my @files2; for my $_file (@files) { if ( $_file eq "." || $_file eq ".." || $_file eq "__origin_path" || $_file eq "__last_prune" || $_file eq "__prune_rules" ) { next; } elsif ( -d ($v_dir . "/" . $_file) && ! -l ($v_dir . "/" . $_file) ) { push( @dirs, ($v_dir . "/" . $_file) ); next; } elsif ( $_file =~ m/_(hold|comment|stat|md5|pointer)$/ ) { ### If there's a comment or ctime file present, but the base file isn't there anymore, remove it $_file = $v_dir . "/" . $_file; my $v_base = $_file; $v_base =~ s/_(hold|comment|stat|md5|pointer)$//; if ( -d $v_base ) { } elsif ( ! -f $v_base && ! -l $v_base ) { unlink( $_file ); } next; } ### Only the actual backup files should still be present at this point $_file =~ s/_[0-9]+$//; push( @files2, $_file ); } @files = &Main::fn_uniq(@files2); for my $_file (@files) { $_file = $v_dir . "/" . $_file; fn_check_retention($_file); } ### Sorting here is only necessary so that we can get verifiably consistant results during testing @dirs = sort {$a cmp $b} @dirs; for my $_dir (@dirs) { ### For each of the directories we found, go through RECURSIVELY! fn_prune_backups($_dir); } } } ### reset the pruning settings $Main::v_retention_max_copies = $v_cur_mc; $Main::v_retention_min_days = $v_cur_md; } sub fn_get_backup_name { ### Given a backup directory and a file name (without full path) return an appropriate name for a backup, and just the stamp my $v_dir = $_[0]; my $v_name = $_[1]; ### Determine the file name we'll use for the new backup my $v_time = time(); my $c = 0; no warnings; while ( -e $v_dir . "/" . $v_name . "_" . $v_time . $c ) { ### The objective here is to allow us to perform two backups of a file within the same second. That alone is an edge case - the thought of us doing ten will probably never occur use warnings; $c++; no warnings; if ( $c == 10 ) { ### We should probably never get this far, but if we do, just wait until the next second use warnings; $c = 0; sleep 1; } } use warnings; return( ($v_dir . "/" . $v_name . "_" . $v_time . $c), ($v_time . $c) ); } sub fn_backup_file { ### Check to make sure that a file matches the desired regex, then make a copy of the file ### $_[0] is the file we're copying, $_[1] is the backup directory my $v_file = $_[0]; my $d_backup = $Main::d_backup; if ( $_[1] ) { $d_backup = $_[1]; } my $b_continue; if ( ! -f $v_file && ! -l $v_file ) { return; } if ($b_do_backup) { $b_continue = 1; } if ( ! $b_continue ) { for my $_string (@Main::v_backup_plus) { if ( $v_file eq $_string ) { $b_continue = 1; last; } } } if ( ! $b_continue ) { for my $_string (@Main::v_backupr) { if ( $v_file =~ m/$_string/ ) { $b_continue = 1; last; } } } if ($b_continue) { my @v_dirs = split( m/\//, $v_file ); my $v_name = pop(@v_dirs); my $d_origin = ''; ### Go down the directory path and ensure that each of the directories on the way exist while (@v_dirs) { my $_dir = shift(@v_dirs); $d_backup .= "/" . $_dir; ### The origin dir should not end in a slash unless it is ONLY a slash if ( $d_origin ne "/" ) { $d_origin .= "/" . $_dir; } else { $d_origin .= $_dir; } ### If the backup directory doesn't exist, create it if ( ! -d $d_backup ) { mkdir( $d_backup ); chmod( 0700, $d_backup ); } ### Save the path to the original directory if ( ! -f $d_backup . "/__origin_path" && open( my $fh_write, ">", $d_backup . "/__origin_path" ) ) { print $fh_write $d_origin; close($fh_write); } } my $v_file_escape = &SWEscape::fn_escape_filename($v_file); if ( -d $d_backup ) { ( my $f_content, my $b_stats ) = fn_compare_backup($v_file, $d_backup); if ( $f_content && $b_stats ) { ### The current backup matches; no need to create a new one return( $f_content ); } ( my $f_backup, my $v_stamp ) = fn_get_backup_name( $d_backup, $v_name ); my $fh_write; if ( $f_content ) { ### If the content matches, but the stats do not, move the old content to the new stamp fn_create_pointer( $f_content, $f_backup ); } else { ### Copy the file system( "cp", "-a", $v_file, $f_backup ); } ### Test if the files was successfully copied if ( -f $f_backup || -l $f_backup ) { if ( open( $fh_write, ">", $f_backup . "_stat" ) ) { print $fh_write join( ' -- ', @v_captured_stats ); close($fh_write); } undef @v_captured_stats; if ($Main::b_verbose) { my $f_backup_escape = &SWEscape::fn_escape_filename($f_backup); print STDERR $v_file_escape . " -> " . $f_backup_escape . "\n"; } if ( $f_content ) { &Main::fn_log("Backed up changes to file stats for file " . $v_file_escape . "\n"); } else { &Main::fn_log("Backed up file " . $v_file_escape . "\n"); } if ($Main::b_retention) { fn_check_retention( $d_backup . "/" . $v_name ); } ### If the content different and we captured an md5sum, write it if ( ! $f_content && $v_captured_md5 ) { ### If we captured the md5sums earlier, might as well hold on to them if ( open( $fh_write, ">", $f_backup . "_md5" ) ) { print $fh_write $v_captured_md5; close($fh_write); } undef $v_captured_md5; } return $f_backup; } } &Main::fn_log("Failed to backup file " . $v_file_escape . "\n"); if ($Main::b_verbose) { print STDERR "Failed to backup file " . $v_file_escape . "\n"; } } return 0; } sub fn_list_backups { ### Given a file name (without path) and a directory, return an array of all backup files in that directory ### $_[0] is the file name, $_[1] is the directory my $v_name = $_[0]; my $v_dir = $_[1]; my @v_files; ### Open the directory and find all matching files if ( opendir my $fh_dir, $v_dir ) { my @files = readdir $fh_dir; closedir $fh_dir; my $re_name = qr/^\Q$v_name\E_[0-9]+$/; for my $_file (@files) { if ( $_file =~ m/$re_name/ ) { push( @v_files, $_file ); } } } return @v_files; } sub fn_file_stats { ### Given a file name, return stats for that file my @v_stats1; if ( -l $_[0] ) { @v_stats1 = (lstat($_[0]))[2,4,5,7,9,10]; } else { @v_stats1 = (stat($_[0]))[2,4,5,7,9,10]; } return(@v_stats1) } sub fn_read_first_line { ### Given the name of a file that we only want to read the frst line from, return that first line my $v_return; no warnings; if ( -f $_[0] ) { use warnings; if ( open( my $fh_read, "<", $_[0] ) ) { while (<$fh_read>) { $v_return = $_; chomp( $v_return ); last; } close($fh_read); } } use warnings; return( $v_return ); } sub fn_compare_backup { ### Compare an existing backup to the files it was taken from ### This will return two values: ### 1) either the number "0" or the name of the backup file with matching content ### 2) "0" to indicate that the permissions and stamps do not match, "1" to indicate that they do ### $_[0] is the file in-place, $_[1] is the directory that it's in my $v_file = $_[0]; my $v_dir = $_[1]; ### Get the stats for the file currently present my @v_stats1 = fn_file_stats($v_file); ### Hold on to the file stats in case we need them @v_captured_stats = @v_stats1; my @v_dirs = split( m/\//, $v_file ); my $v_name = pop(@v_dirs); my @v_files = fn_list_backups($v_name, $v_dir); if ( ! @v_files ) { return( 0, 0 ); } else { @v_files = sort {$b cmp $a} @v_files; ### The last one should be the most recent backup - the one we want to compare to my $v_file2 = $v_dir . "/" . $v_files[0]; ### Get the stats for the backup my @v_stats2; my $v_stats = fn_read_first_line( $v_file2 . "_stat" ); if ($v_stats) { @v_stats2 = split( m/ -- /, $v_stats ); } ### Find if the stats are different my $b_perms; my $b_size; my $b_stamps; my $c; for ($c=0; $c < scalar(@v_stats1); $c++) { if ( ! defined $v_stats2[$c] || $v_stats1[$c] != $v_stats2[$c] ) { if ( $c == 0 || $c == 1 || $c == 2 ) { $b_perms = 1; } elsif ( $c == 3 ) { $b_size = 1; } else { $b_stamps = 1; } } } ### Assess how we need to respond based on differences in the stats if ( ! $b_perms && ! $b_size && ! $b_stamps ) { ### If everything is the same, return the name of the most recent backed up file ### Note, while it's possible for the ctime and size to be the same and the content to still be different, ### this is such an extreme edge case, that performing an md5sum check for it every time is ridiculous return( $v_file2, 1 ); } elsif ( $b_size ) { ### If the size of the file changed, we know that both its content and ctime should be different return( 0, 0 ); } else { ### We do not know whether or not content is different - better get md5sums! if ($Main::b_use_md5) { ### Start by getting the md5sum of the file in place $v_captured_md5 = &SWmd5::get_md5($v_file); ### Then get the md5sum of the backed up file my $v_md5_backup = fn_read_first_line($v_file2 . "_md5"); if ( ! $v_md5_backup ) { ### If it's not there, Maybe the backup is a pointer and we can find it elsewhere my $f_content = fn_backup_details($v_file2, 2); if ( $f_content ne $v_file2 ) { $v_md5_backup = fn_read_first_line($f_content . "_md5"); } if ( ! $v_md5_backup ) { ### Otherwise, we'll just have to GET IT OUR SELVES $v_md5_backup = &SWmd5::get_md5($f_content); if ( open( my $fh_write, ">", $v_file2 . "_md5" ) ) { print $fh_write $v_md5_backup; close($fh_write); } } } ### Compare them if ( $v_captured_md5 eq $v_md5_backup ) { ### The content is the same, but something else is different undef $v_captured_md5; return( $v_file2, 0 ); } else { ### The content is different! return( 0, 0 ); } } else { ### We don't know if the content changed. Better back it up just to be safe return( 0, 0 ); } } } } sub fn_find_backup { ### Given the display name of a backup or the full path to a backup, find the full path to that backup my $v_disp = $_[0]; $v_disp = &Main::fn_test_file($v_disp); my $b_maybe_full_path; no warnings; if ( -f $v_disp || (-l $v_disp && ! -d $v_disp) ) { use warnings; $b_maybe_full_path = 1; } use warnings; my @v_backup_dirs; if ($Main::d_backup) { @v_backup_dirs = ($Main::d_backup); } ### Find all of the backup directories that have been used if ( ! -d $Main::d_working ) { mkdir( $Main::d_working ); } ### Open the list and read from it if ( -f $Main::d_working . "/backup_locations" ) { if ( open( my $fh_read, "<", $Main::d_working . "/backup_locations" ) ) { while (<$fh_read>) { my $_line = $_; chomp($_line); if ( ! $Main::d_backup || $_line ne $Main::d_backup ) { push( @v_backup_dirs, $_line ); } } close($fh_read); } } my @v_dirs = split( m/\//, $v_disp ); shift(@v_dirs); ### this will have been empty my $v_name = pop(@v_dirs); ### Go through each backup directory to see if this file is present DBACKUP: for my $d_backup (@v_backup_dirs) { if ( $b_maybe_full_path && $v_disp =~ m/^\Q$d_backup\E/ ) { ### It turns out that we were given the full path to begin with return( $v_disp ); } for my $_dir (@v_dirs) { $d_backup .= "/" . $_dir; if ( ! -d $d_backup ) { next DBACKUP; } } my $v_return = $d_backup . '/' . $v_name; if ( -l $v_return || -f $v_return ) { ### If we find a file that matches the name, return it return( $v_return ); } } return; } sub fn_restore { ### Given the display name or full path to a backup, restore it my $v_disp = $_[0]; ### Find the location of the backup and the original file path my $f_backup = fn_find_backup($v_disp); if ( ! $f_backup ) { print STDERR "No such backup " . &SWEscape::fn_escape_filename($v_disp) . "\n"; exit 1 } ( my $f_stats, my $f_origin, my $f_content, my $d_backup, my $v_name, my $d_backup2 ) = fn_backup_details($f_backup); ### Extract the stats my $v_stats = fn_read_first_line($f_stats); if ( ! $v_stats ) { ##### If we're trying to restore a backup that doesn't have a stat file... should we even allow that? print STDERR "No stat data for this backup\n"; exit 1; } my @v_stats = split( m/ -- /, $v_stats ); ### Make a backup of the file as it currently is, then remove it $b_do_backup = 1; $Main::b_retention = 0; if ( -e $f_origin ) { my $f_backup2 = fn_backup_file($f_origin, $d_backup2); if ( ! $f_backup2 ) { print STDERR "Failed to create backup of " . &SWEscape::fn_escape_filename($f_origin) . ". File left as-is.\n"; exit 1; } unlink($f_origin) } ### Restore the backup system( "cp", "-a", $f_content, $f_origin ); chmod( oct( sprintf( "%04o", $v_stats[0] & 07777) ), $f_origin ); chown( $v_stats[1], $v_stats[2], $f_origin ); utime( $v_stats[4], $v_stats[4], $f_origin ); @v_stats = fn_file_stats($f_origin); ### Create a new backup showing that the file has been restored ( my $f_backup2, my $v_stamp ) = fn_get_backup_name( $d_backup, $v_name ); fn_create_pointer($f_content, $f_backup2, $v_stamp); if ( $f_content ne $f_backup ) { fn_write_pointer( $f_backup, $v_stamp ); } my $v_stamp2 = substr((split( m/_/, $f_backup ))[-1], 0, -1); $v_stamp2 = &Main::strftime( '%Y-%m-%d %T %z', localtime($v_stamp2) ); my $fh_write; if ( open( $fh_write, ">>", $f_backup2 . "_comment" ) ) { print $fh_write "Restored from backup taken at " . $v_stamp2 . "\n"; close($fh_write); } if ( open( $fh_write, ">", $f_backup2 . "_stat" ) ) { print $fh_write join( ' -- ', @v_stats ); close($fh_write); } } sub fn_create_pointer { ### Given the full path to a file where content is currently, and the full path to a file where the content needs to be ### And optionally, the stamp for the new location ### Move the content, and replace it with a pointer file my $v_orig = $_[0]; my $v_new = $_[1]; my $v_stamp = ( $_[2] || '' ); no warnings; if ( -e $v_new ) { use warnings; print STDERR "There is already a file at " . &SWEscape::fn_escape_filename($v_new) . "\n"; exit 1; } use warnings; if ( ! $v_stamp ) { $v_stamp = (split( m/_/, $v_new ))[-1]; } rename( $v_orig, $v_new ); if ( -e $v_new ) { fn_write_pointer( $v_orig, $v_stamp ); no warnings; if ( -f $v_orig . '_md5' ) { use warnings; system( "cp", "-a", ($v_orig . '_md5'), ($v_new . '_md5') ); } use warnings; } else { print STDERR "Failed to move file to " . &SWEscape::fn_escape_filename($v_new) . "\n"; exit 1; } } sub fn_write_pointer { ### Given the name of a backup file, and the stamp of a file it should point to, write pointer files my $fh_write; if ( open( $fh_write, ">", $_[0] ) ) { print $fh_write $_[1]; close($fh_write); } if ( open( $fh_write, ">", $_[0] . "_pointer" ) ) { print $fh_write $_[1]; close($fh_write); } } sub fn_backup_details { ### Given the full path to a backed up file, return the following: ### 0) It's stat file ### 1) The original path ### 2) The content file (I.E. follow any pointers to their source) ### 3) The backup directory that the file will go in ### 4) The name of the original file (without path) ### 5) The path to the root of the backup directory ### Optional second argument: the specific array numbers for the desired items my $f_backup = $_[0]; my @v_return_nums; if ( defined $_[1] && defined $_[2] ) { shift( @_ ); @v_return_nums = @_; } elsif ( defined $_[1] ) { push( @v_return_nums, $_[1] ); } my $f_stat = $f_backup . '_stat'; my @v_path = split( m/\//, $f_backup ); my $v_name = pop(@v_path); my $d_backup = join( '/', @v_path ); my $v_path = fn_read_first_line( $d_backup . '/__origin_path' ); my $d_backup2 = $d_backup; $d_backup2 =~ s/\Q$v_path\E$//; $v_name =~ s/_[0-9]+//; my $f_content = fn_follow_pointers($f_backup, $d_backup, $v_name); my $f_orig = $v_path . '/' . $v_name; my @v_return = ( $f_stat, $f_orig, $f_content, $d_backup, $v_name, $d_backup2 ); if (! @v_return_nums) { return( @v_return ); } elsif ( scalar(@v_return_nums) == 1 ) { return( $v_return[$v_return_nums[0]] ); } else { my @v_return2; for my $i (@v_return_nums) { push( @v_return2, $v_return[$i] ); } return( @v_return2 ); } } sub fn_follow_pointers { ### Given the full path to a backed up file, the full path to the directory it's in, and the original file name, follow all pointers until the content is reached my $v_file = $_[0]; my $d_backup = $_[1]; my $v_name = $_[2]; my $v_file2 = $v_file; my $v_file3 = $v_file; no warnings; if ( -f $v_file . "_pointer" ) { use warnings; $v_file2 = $d_backup . '/' . $v_name . '_' . fn_read_first_line($v_file . "_pointer"); $v_file3 = fn_follow_pointers($v_file2, $d_backup, $v_name); ### Eliminate pointer chains by pointing them to the most recent file if ( $v_file ne $v_file3 ) { my $v_stamp = (split( m/_/, $v_file3 ))[-1]; fn_write_pointer( $v_file, $v_stamp ); } } use warnings; return($v_file3); } sub fn_find_backups { ### Given the full path to a file, discover all of the backups that exist for that file ### $_[0] is the full path for the file my $v_file = $_[0]; my @v_backup_dirs; if ($Main::d_backup) { @v_backup_dirs = ($Main::d_backup); } ### Find all of the backup directories that have been used if ( ! -d $Main::d_working ) { mkdir( $Main::d_working ); } ### Open the list and read from it if ( -f $Main::d_working . "/backup_locations" ) { if ( open( my $fh_read, "<", $Main::d_working . "/backup_locations" ) ) { while (<$fh_read>) { my $_line = $_; chomp($_line); if ( ! $Main::d_backup || $_line ne $Main::d_backup ) { push( @v_backup_dirs, $_line ); } } close($fh_read); } } my @v_dirs = split( m/\//, $v_file ); shift(@v_dirs); ### this will have been empty my $v_name = pop(@v_dirs); if ( ! defined $v_name || $v_name eq '' ) { return; } my $d_orig = '/' . join( '/', @v_dirs ); my %v_files; ### Go through each backup directory to find instances where this file has been backed up DBACKUP: for my $d_backup (@v_backup_dirs) { for my $_dir (@v_dirs) { $d_backup .= "/" . $_dir; if ( ! -d $d_backup ) { next DBACKUP; } } my @v_files2 = fn_list_backups($v_name, $d_backup); for my $_file (@v_files2) { my $v_stamp = (split( m/_/, $_file ))[-1]; $_file = $d_backup . "/" . $_file; $v_files{$v_stamp} = $_file } } return( $v_name, $d_orig, %v_files ); } sub fn_list_file { ### Given a file name, list the backups that are available for it ### $_[0] is the full path for the file my $v_file = $_[0]; ( my $v_name, my $d_orig, my %v_files ) = fn_find_backups($v_file); ### Output the details my $v_file_escape = &SWEscape::fn_escape_filename($v_file); print "\nAvailable Backups for " . $v_file_escape . ":\n"; if (%v_files) { my @v_files = sort {$a cmp $b} keys( %v_files ); for my $v_stamp (@v_files) { ### Get the string of numbers from the end of the file name, then trim off the last number - this is the timestamp my $v_file = $v_files{$v_stamp}; my $v_disp_name = $d_orig . '/' . $v_name . '_' . $v_stamp; $v_stamp = substr( $v_stamp, 0, -1); $v_stamp = &Main::strftime( '%Y-%m-%d %T %z', localtime($v_stamp) ); ### Get the size of the file my $v_size; ### Apparently earlier versions of perl error if you ask about a file that has a new line character and turns out to be not present. Turning off warnings fixes this. no warnings; if ( -f $v_file . "_stat" ) { if ( ! -f $v_file . "_pointer" ) { use warnings; ### If this isn't a pointer backup, it's easier to just get the stat if ( -l $v_file ) { $v_size = (lstat($v_file))[7]; } else { $v_size = (stat($v_file))[7]; } } else { use warnings; ### If it's a pointer backup, easier to get it from the _stat file $v_size = (split( m/ -- /, fn_read_first_line( $v_file . "_stat" ) ))[3]; } } else { ##### should we even bother listing it if there's no stat file next; } use warnings; my $v_file_escape = &SWEscape::fn_escape_filename($v_disp_name); ### This would print the full path for the backup rather than just the display name # my $v_file_escape = &SWEscape::fn_escape_filename($v_file); print " " . $v_file_escape . " -- Timestamp: " . $v_stamp . " -- " . $v_size . " bytes"; no warnings; if ( -f $v_file . "_hold" ) { use warnings; print " -- HELD" } use warnings; print "\n"; no warnings; if ( -f $v_file . "_comment" ) { use warnings; &Main::fn_print_files( " - ", $v_file . "_comment" ); } use warnings; } } else { print "There are no backups of this file\n" } } sub fn_compare_contents { ### Given a backup file, show how it compares to the existing file ### If a second file is given, show how the file compares to that file instead ### If the first file is just a regular file, and the SECOND file is a backup... yeah we can do that too. require( $Main::d_program . '/modules/report_details.pm' ); my $v_disp1 = $_[0]; ### Find the location of the backup and the original file path my $f_comp1 = fn_find_backup($v_disp1); my $f_stats1; my $f_origin; my $f_content1; if ( $f_comp1 ) { ( $f_stats1, $f_origin, $f_content1 ) = fn_backup_details($f_comp1, 0, 1, 2); } elsif ( -f $v_disp1 || (-l $v_disp1 && ! -d $v_disp1) ) { $f_comp1 = &Main::fn_test_file($v_disp1, 1, 'lf'); $f_content1 = $f_comp1; } if ( ! $f_comp1 ) { print STDERR "No such file or backup " . &SWEscape::fn_escape_filename($v_disp1) . "\n"; exit 1; } ### And figure out what we're comparing it to my $v_disp2 = $f_origin; my $f_comp2 = $f_origin; my $f_content2 = $f_origin; my $f_stats2; my $b_matches_current; if ( defined $_[1] ) { my $v_file = fn_find_backup($_[1]); if ( $v_file ) { $f_comp2 = $v_file; $v_disp2 = $_[1]; ( $f_stats2, $f_content2 ) = fn_backup_details($f_comp2, 0, 2); } elsif ( -f $_[1] || (-l $_[1] && ! -d $_[1]) ) { $f_comp2 = $_[1]; $v_disp2 = $_[1]; $f_content2 = $_[1]; } else { print STDERR "No such file " . &SWEscape::fn_escape_filename($_[1]) . "\n"; exit 1; } } elsif ( ! $f_origin ) { ### Given a situation where we're only given a file (that is not a backup), we should compare it to the most recent backup that does not have a matching ctime ( my $v_name, my $d_orig, my %v_files ) = fn_find_backups($f_comp1); ### Get the ctime of the file my $comp1_ctime = (fn_file_stats($f_comp1))[-1]; my @v_files = sort {$b cmp $a} keys( %v_files ); for my $v_stamp (@v_files) { ### Get the string of numbers from the end of the file name, then trim off the last number - this is the timestamp my $v_file = $v_files{$v_stamp}; my $comp2_ctime = fn_read_first_line( $v_file . '_stat' ); if ($comp2_ctime) { $comp2_ctime = (split( m/ -- /, $comp2_ctime ))[-1]; if ( $comp1_ctime > $comp2_ctime ) { ### The first file with a ctime lower than the existing file is the one we want $v_disp2 = $d_orig . '/' . $v_name . '_' . $v_stamp; $f_comp2 = $v_file; ( $f_stats2, $f_content2 ) = fn_backup_details($f_comp2, 0, 2); last; } elsif ( $comp1_ctime == $comp2_ctime ) { $b_matches_current = 1; } } } } if ( ! $f_content2 ) { if ($b_matches_current) { print STDERR "File " . &SWEscape::fn_escape_filename($v_disp1) . " is the same as the only backup\n"; } else { print STDERR "Nothing to compare " . &SWEscape::fn_escape_filename($v_disp1) . " against\n"; } exit 1; } ### Get the stats for both files my @v_stats1; if ( ! $f_stats1 ) { @v_stats1 = fn_file_stats($f_content1); } else { @v_stats1 = split( m/ -- /, fn_read_first_line($f_stats1) ); } my @v_stats2; if ( ! $f_stats2 ) { @v_stats2 = fn_file_stats($f_comp2); } else { @v_stats2 = split( m/ -- /, fn_read_first_line($f_stats2) ); } ### Get the names of users and groups &SWReportDetails::fn_get_users_groups(); &SWReportDetails::fn_get_user_group($v_stats1[1], $v_stats1[2]); &SWReportDetails::fn_get_user_group($v_stats2[1], $v_stats2[2]); ### Put the stat differences into strings my @stat1 = ( &Main::fn_format_perms($v_stats1[0]), '(' . $v_stats1[1] . ' / ' . $SWReportDetails::v_users{$v_stats1[1]} . ')', '(' . $v_stats1[2] . ' / ' . $SWReportDetails::v_groups{$v_stats1[2]} . ')', $v_stats1[3] . ' bytes', &Main::strftime( '%Y-%m-%d %T %z', localtime($v_stats1[4]) ), &Main::strftime( '%Y-%m-%d %T %z', localtime($v_stats1[5]) ) ); my @stat2 = ( &Main::fn_format_perms($v_stats2[0]), '(' . $v_stats2[1] . ' / ' . $SWReportDetails::v_users{$v_stats2[1]} . ')', '(' . $v_stats2[2] . ' / ' . $SWReportDetails::v_groups{$v_stats2[2]} . ')', $v_stats2[3] . ' bytes', &Main::strftime( '%Y-%m-%d %T %z', localtime($v_stats2[4]) ), &Main::strftime( '%Y-%m-%d %T %z', localtime($v_stats2[5]) ) ); my $v_len1 = 0; my @v_len1; for (@stat1) { my $len = length($_); push( @v_len1, $len ); if ( $len > $v_len1 ) { $v_len1 = $len; } } ### Find if there are stat differences for (my $c=0; $c < scalar(@stat1); $c++) { if ( $stat1[$c] ne $stat2[$c] ) { $stat1[$c] = "\e[33m" . $stat1[$c] . "\e[00m"; $stat2[$c] = "\e[33m" . $stat2[$c] . "\e[00m"; } } ### Begin output print "\n"; print "Comparing\n " . &SWEscape::fn_escape_filename($v_disp1) . "\nTo\n " . &SWEscape::fn_escape_filename($v_disp2) . "\n\n"; print 'Type and Perms: ' . $stat1[0] . ( ' ' x ($v_len1 - $v_len1[0]) ) . ' > ' . $stat2[0] . "\n"; print 'User: ' . $stat1[1] . ( ' ' x ($v_len1 - $v_len1[1]) ) . ' > ' . $stat2[1] . "\n"; print 'Group: ' . $stat1[2] . ( ' ' x ($v_len1 - $v_len1[2]) ) . ' > ' . $stat2[2] . "\n"; print 'File Size: ' . $stat1[3] . ( ' ' x ($v_len1 - $v_len1[3]) ) . ' > ' . $stat2[3] . "\n"; print 'Modify Time: ' . $stat1[4] . ( ' ' x ($v_len1 - $v_len1[4]) ) . ' > ' . $stat2[4] . "\n"; print 'Change Time: ' . $stat1[5] . ( ' ' x ($v_len1 - $v_len1[5]) ) . ' > ' . $stat2[5] . "\n\n"; ### Now just diff the files if ( ! -l $f_content1 && ! -l $f_content2 ) { print "Content Diff:\n\n"; my $diff1_escape = &SWEscape::fn_shell_escape_filename($f_content1); my $diff2_escape = &SWEscape::fn_shell_escape_filename($f_content2); ### This just uses the binary version of diff, as it's faster and easier my $b_lines; if ( open( my $fh_pipe, "diff $diff1_escape $diff2_escape |" ) ) { while (<$fh_pipe>) { $b_lines = 1; print $_; } close($fh_pipe); if ( ! $b_lines ) { print "Same\n"; } print "\n"; } } elsif ( -l $f_content1 && -l $f_content2 ) { my $v_readlink1 = readlink($f_content1); my $v_readlink2 = readlink($f_content2); print "Content Diff:\n\n"; if ( $v_readlink1 eq $v_readlink2 ) { print "Same\n\n"; } else { print "< " . &SWEscape::fn_escape_filename($v_readlink1) . "\n"; print "> " . &SWEscape::fn_escape_filename($v_readlink2) . "\n\n"; } } else { print "Cannot show the difference between a file and a symlink\n\n"; } } sub fn_single_backup { ### Backup a single file ### $_[0] is the full path to the file, $_[1] is the path to the backup directory, $_[2] is boolean whether or not to hold the backup, $_[3] is a comment for the backup my $v_file = $_[0]; my $d_backup = ( $_[1] || $Main::d_backup ); my $b_hold = $_[2]; my $v_comment; if ( defined $_[3] ) { $v_comment = $_[3]; } $b_do_backup = 1; my $f_backup = fn_backup_file( $v_file, $d_backup ); if ( $b_hold && ! -f $f_backup . "_hold" ) { if ( open( my $fh_write, ">>", $f_backup . "_hold" ) ) { print $fh_write time(); close($fh_write); } } if ($v_comment) { if ( open( my $fh_write, ">>", $f_backup . "_comment" ) ) { print $fh_write $v_comment . "\n"; close($fh_write); } } return $f_backup; } sub fn_backup_stat { my $v_disp = shift(@_); ### Find the location of the backup and the original file path my $f_backup = fn_find_backup($v_disp); if ( ! $f_backup ) { print STDERR "No such backup " . &SWEscape::fn_escape_filename($v_disp) . "\n"; } else { my $v_stats = fn_read_first_line( $f_backup . "_stat" ); no warnings; if ( -f $f_backup . "_md5" ) { use warnings; $v_stats .= " -- " . fn_read_first_line( $f_backup . "_md5" ); } use warnings; &SWReportDetails::fn_stat_file( $v_disp, $v_stats ); } ### If we were given more files, do those too if (@_) { fn_backup_stat(@_); } } sub fn_hold { ### Given a backup file, create a hold for that backup my $v_disp = shift(@_); ### Find the location of the backup and the original file path my $f_backup = fn_find_backup($v_disp); if ( ! $f_backup ) { print STDERR "No such backup " . &SWEscape::fn_escape_filename($v_disp) . "\n"; exit 1 } if ( open( my $fh_write, ">", $f_backup . "_hold" ) ) { close($fh_write); } if ( defined $_[0] && $_[0] eq "--comment" && defined $_[1] ) { fn_comment($v_disp, $_[1]); } } sub fn_unhold { ### Given a backup file; remove the hold for that backup my $v_disp = $_[0]; ### Find the location of the backup and the original file path my $f_backup = fn_find_backup($v_disp); if ( ! $f_backup ) { print STDERR "No such backup " . &SWEscape::fn_escape_filename($v_disp) . "\n"; exit 1 } if ( -f $f_backup . '_hold' ) { unlink $f_backup . '_hold' } } sub fn_comment { ### Given a backup file, add a comment to that backup my $v_disp = shift(@_); my $v_comment = shift(@_); ### Find the location of the backup and the original file path my $f_backup = fn_find_backup($v_disp); if ( ! $f_backup ) { print STDERR "No such backup " . &SWEscape::fn_escape_filename($v_disp) . "\n"; exit 1 } if ($v_comment) { if ( open( my $fh_write, ">>", $f_backup . "_comment" ) ) { print $fh_write $v_comment . "\n"; close($fh_write); } } if ( defined $_[0] && $_[0] eq "--hold" ) { fn_hold($v_disp); } } 1;
Copyright ©2k19 -
Hexid
|
Tex7ure