App-bk

 view release on metacpan or  search on metacpan

lib/App/bk.pm  view on Meta::CPAN

package App::bk;

use warnings;
use strict;

use Getopt::Long qw(:config no_ignore_case bundling no_auto_abbrev);
use Pod::Usage;
use English 'no-match-vars';
use POSIX qw(strftime);
use File::Basename;
use File::Copy;
use File::Which qw(which);
use Carp;

=head1 NAME

App::bk - A module for functions used by the F<bk> program.

=head1 VERSION

Version 0.05

=cut

our $VERSION = '0.06';

my %opts = (
    'help|h|?'  => 0,
    'man'       => 0,
    'version|V' => 0,
    'debug:+'   => 0,
    'diff|d'    => 0,
    'edit|e'    => 0,
);
my %options;

# 'tidier' way to store global variables
# probably shouldnt do it like this - will rework later
$options{debug} ||= 0;
$options{username} = getpwuid($EUID);

if ( $options{username} eq 'root' ) {
    logmsg( 2, 'Running as root so dropping username from file backups' );
    $options{username} = '';
}

=head1 SYNOPSIS

Please see the file F<bk> for more information about the F<bk> program.

=head1 SUBROUTINES/METHODS

=head2 backup_files

Main function to process ARGV and backup files as necessary

=cut

sub backup_files {

    # make sure we don't clobber any callers variables

    local @ARGV = @ARGV;
    GetOptions( \%options, keys(%opts) ) || pod2usage( -verbose => 1 );

    die("Version: $VERSION\n") if ( $options{version} );
    pod2usage( -verbose => 1 ) if ( $options{'?'}  || $options{help} );
    pod2usage( -verbose => 2 ) if ( $options{HELP} || $options{man} );

    $options{debug} ||= 0;
    $options{debug} = 8 if ( $options{debug} > 8 );

    if ( !@ARGV ) {
        pod2usage(
            -message => 'No filenames provided.',
            -verbose => 0,
        );
    }

    my $date = strftime( '%Y%m%d', localtime() );
    my $time = strftime( '%H%M%S', localtime() );

    foreach my $filename (@ARGV) {
        my ( $basename, $dirname ) = fileparse($filename);

      # do this via savedir as we might move this somewhere else dir in future
        my $savedir = $dirname;

        logmsg( 2, "dirname=$dirname" );
        logmsg( 2, "basename=$basename" );

        if ( !-f $filename ) {
            warn "WARNING: File $filename not found", $/;
            next;
        }

        if ( !$savedir ) {
            warn "WARNING: $savedir does not exist", $/;
            next;
        }

        # compare the last file found with the current file
        my $last_backup = get_last_backup( $savedir, $basename );

        if ( $options{diff} ) {
            if ( !$last_backup ) {
                print "'$filename' not previously backed up.", $/;
            }
            else {
                print get_diff( $last_backup, $filename );
            }
            next;
        }

        if ($last_backup) {
            logmsg( 1, "Found last backup as: $last_backup" );

            my $last_backup_sum = get_chksum($last_backup);
            my $current_sum     = get_chksum($filename);

            logmsg( 2, "Last backup file $options{sum}: $last_backup_sum" );
            logmsg( 2, "Current file $options{sum}: $current_sum" );

            if ( $last_backup_sum eq $current_sum ) {
                logmsg( 0, "No change since last backup of $filename" );
                next;
            }
        }

        my $savefilename = "$savedir$basename";
        $savefilename .= ".$options{username}" if ( $options{username} );
        $savefilename .= ".$date";
        if ( -f $savefilename ) {
            $savefilename .= ".$time";
        }

        logmsg( 1, "Backing up to $savefilename" );

        # use OS cp to preserve ownership/permissions/etc
        if ( system("cp $filename $savefilename") != 0 ) {
            warn "Failed to back up $filename", $/;
            next;
        }

        logmsg( 0, "Backed up $filename to $savefilename" );
    }

    if ( $options{edit} ) {
        my $editor 
            = $ENV{EDITOR}
            || $ENV{VISUAL}
            || die 'Neither "EDITOR" nor "VISUAL" environment variables set',
            $/;

        print "Running: $editor @ARGV", $/;
        exec("$editor @ARGV");
    }

    return 1;
}

=head2 logmsg($level, @message);

Output @message if $level is equal or less than $options{debug}

=cut

sub logmsg {
    my ( $level, @text ) = @_;
    print @text, $/ if ( $level <= $options{debug} );
}

=head2 $binary = find_sum_binary();

Locate a binary to use to calculate a file checksum.  Looks first for md5sum, then sum.  Dies on failure to find either.

=cut

sub find_sum_binary {
    return
           which('md5sum')
        || which('sum')
        || die 'Unable to locate "md5sum" or "sum"', $/;
}

=head2 $sum = get_chksum($file);

Get the chksum of a file

=cut

sub get_chksum {
    my ($filename) = @_;

    croak 'No filename provided' if ( !$filename );

    if ( !$options{sum} ) {
        $options{sum} = find_sum_binary();
        logmsg( 2, "Using $options{sum}" );
    }

    my $chksum = qx/$options{sum} $filename/;
    chomp($chksum);

    ($chksum) = $chksum =~ m/^(\w+)\s/;
    return $chksum;
}

=head2 $binary = find_diff_binary();

Locate a binary to use for diff

=cut

sub find_diff_binary {
    return which('diff')
        || die 'Unable to locate "diff"', $/;
}

=head2 $differences = get_diff ($old, $new);

Get the differences between two files

=cut

sub get_diff {
    my ( $old, $new ) = @_;

    my $diff_binary = find_diff_binary();
    my $differences = qx/$diff_binary -u $old $new/;
    return $differences
        ? $differences
        : "No differences between '$old' and '$new'" . $/;
}

=head2 $filename = get_last_backup($file);

Get the last backup filename for given file

=cut

sub get_last_backup {
    my ( $savedir, $filename ) = @_;

    if ( !$savedir || !-d $savedir ) {
        croak 'Invalid save directory provided';
    }

    # get last backup and compare to current file to prevent
    # unnecessary backups being created
    opendir( my $savedir_fh, $savedir )
        || die( "Unable to read $savedir: $!", $/ );
    my @save_files = sort
        grep( /$filename\.(?:$options{username}\.)?\d{8}/,
        readdir($savedir_fh) );
    closedir($savedir_fh) || die( "Unable to close $savedir: $!", $/ );

    if ( $options{debug} > 2 ) {
        logmsg( 3, "Previous backups found:" );
        foreach my $bk (@save_files) {
            logmsg( 3, "\t$bk" );
        }
    }

    return $save_files[-1];
}

=head1 AUTHOR

Duncan Ferguson, C<< <duncan_j_ferguson at yahoo.co.uk> >>

=head1 BUGS

Please report any bugs or feature requests via the web interface at 
L<https://github.com/duncs/perl-app-bk/issues>/  
I will be notified, and then you'll automatically be notified of 
progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc App::bk


You can also look for information at:

=over 4

=item * HitHUB: request tracker

L<https://github.com/duncs/perl-app-bk/issues>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/App-bk>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/App-bk>

=item * Search CPAN

L<http://search.cpan.org/dist/App-bk/>

=back


=head1 ACKNOWLEDGEMENTS


=head1 LICENSE AND COPYRIGHT

Copyright 2011 Duncan Ferguson.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.



( run in 0.551 second using v1.01-cache-2.11-cpan-63c85eba8c4 )