CCCPaste

hometar

#!/usr/bin/env perl

use Modern::Perl '2009';    # Enable Perl 5.10 features
use strict;                 # Included so that percritic does not complain
use warnings;               #                 ditto
use 5.010;                  # Only works under Perl 5.10 and later
use autodie qw( :all );     # Make system functions either succeed or die
use strictures 2;           # Turn on strict and make most carpings fatal

use Carp;
use Cwd;
use English;
use Getopt::Euclid;
use POSIX;
use Readonly;

##############################################################################
# Usage      : remove_old_tars( \%options_ref );
# Purpose    : Only keeps the newest archives that match the given pattern
#              if options_ref->{'remove'} is true
# Returns    : Nothing
# Parameters : $options_ref - a reference to a hash table containing:
#                   'pattern':  the glob pattern of preexisting tar files
#                   'keep':     the number of existsing tars to keep
#                   'remove':   whether or not to proceed with the removal of
#                               obsolete tar files
#                   'verbose':  whether or not to output text describing
#                               non-fatal events
sub remove_old_tars {
    my $options_ref = shift
        or croak 'No options hash reference supplied';
    foreach my $key (qw( pattern keep remove verbose )) {
        if ( !defined $options_ref->{$key} ) {
            croak "Required hash key '$key' not given";
        }
    }

    if ( $options_ref->{'remove'} ) {
        my $is_verbose    = $options_ref->{'verbose'};
        my @files         = glob $options_ref->{'pattern'};
        my $num_to_remove = @files - $options_ref->{'keep'};

        foreach my $file ( splice( @files, 0, $num_to_remove ) ) {
            eval {
                unlink $file;
                say "Removed: $file" if $is_verbose;
            };
            if ($EVAL_ERROR) {
                carp "Unable to remove: $file - $EVAL_ERROR";
            }
        }
    }
    return;
}

##############################################################################
# Usage      : tar_src_dir( \%options );
# Purpose    : Archives the source directory to the destination tar
# Returns    : Nothing
# Parameters : $options_ref - a reference to a hash table containing:
#                   'src_dir':  the source directory to be tarred
#                   'dest_tar': the tar file to create
#                   'verbose':  whether or not to output text describing
#                               non-fatal events
sub tar_src_dir {
    my $options_ref = shift
        or croak 'No options hash reference supplied';
    foreach my $key (qw( src_dir dest_tar verbose )) {
        if ( !defined $options_ref->{$key} ) {
            croak "Required hash key '$key' not given";
        }
    }
    my $src_dir    = $options_ref->{ 'src_dir'  };
    my $dest_tar   = $options_ref->{ 'dest_tar' };
    my $is_verbose = $options_ref->{ 'verbose'  };

    Readonly my $TAR_FLAGS => join(
        ' ', qw(
            --create
            --preserve-permissions
            --atime-preserve
            --use-compress-program='pigz'
            )
    );

    # Determine the home directory

    my $index = rindex($src_dir, '/');
    my $base_dir = substr( $src_dir, 0, $index);
    my $home_dir = substr( $src_dir, $index + 1);

    # Using cd instead of tar's -C (i.e., directory) flag because I was
    # getting the unexpected result of only the Desktop directory being
    # archived

    my $original_dir = getcwd;
    eval { Cwd::chdir($base_dir); };
    if ($EVAL_ERROR) {
        croak "Unable to chdir to: $base_dir: $EVAL_ERROR";
        exit 1;
    }

    # Tar the source dir to $dest_tar

    eval {
        if ($is_verbose) {
            my $tar_cmd = "sudo tar $TAR_FLAGS --verbose";
            system( qq{$tar_cmd --file = "$dest_tar" $home_dir} );
        }
        else {
            my $tar_cmd = "sudo tar $TAR_FLAGS";
            system( qq{$tar_cmd --file = "$dest_tar" $home_dir &>/dev/null} );
        }
    };
    if ($EVAL_ERROR) {
        croak "Unable to create: $dest_tar: $EVAL_ERROR";
        exit 2;
    }

    # And return to the original directory

    Cwd::chdir($original_dir);
    return;
}

##############################################################################
# Usage      : change_file_owner_group( \%options );
# Purpose    : Changes the owner and group of the named file
# Returns    : Nothing
# Parameters : $options_ref - a reference to a hash table containing:
#                   'filename': the name of the file whose ownership is to
#                               be changed
#                   'user':     the name of the owner and group to assign to
#                               filename
#                   'verbose':  whether or not to output text describing
#                               non-fatal events
#                   'group':    name of the group to assign to filename;
#                               same as user if not given
sub change_file_owner_group {
    my $options_ref = shift
        or croak 'No options hash reference supplied';
    foreach my $key (qw( filename user verbose )) {
        if ( !defined $options_ref->{$key} ) {
            croak "Required hash key '$key' not given";
        }
    }
    my $filename   = $options_ref->{ 'filename' };
    my $user       = $options_ref->{ 'user'     };
    my $is_verbose = $options_ref->{ 'verbose'  };
    my $group      = $options_ref->{ 'group'    };
    if ( !defined $group ) {
        $group = $user;
    }

    eval {
        # Get the user id (uid) and group id (gid) of $user

        my $uid = getpwnam $user;
        my $gid = getgrnam $group;

        # Change the uid and gid of the given filename

        chown $uid, $gid, $filename;

        if ($is_verbose) {
            say "Changed ownership of '$filename' to $user:$group";
        }
    };
    if ($EVAL_ERROR) {
        croak "Unable to chown $filename to $user:$group - $EVAL_ERROR";
        exit 3;
    }
    return;
}

##############################################################################
# Usage      : my $fn = generate_tar_name( $dest_dir, $prefix, $suffix);
# Purpose    : Generates a unique fully qualified tar filename
# Returns    : Returns the generated tar name
# Parameters : $dest_dir - directory name to store tar file
#              $prefix   - prefix of the tar file name
#              $suffix   - suffix of the tar file name
sub generate_tar_name {
    my ( $dest_dir, $prefix, $suffix ) = @_;
    my $timestamp = POSIX::strftime( '%Y%m%d%H%M%S', localtime );
    return $dest_dir . '/' . $prefix . $timestamp . $suffix;
}

##############################################################################
# Usage      : chomp_dir $directory;
# Purpose    : Removes any trailing slashes from the given directory
# Returns    :
# Parameters : $dir - directory from which to chomp terminating slashes
sub chomp_dir {
    my $dir = shift;
    $dir =~ s{              # substitute
                 /+         # any number of slashes
                 $          # that occur at the end of the string
             }
             {}xms;         # replacing them with nothing
    return $dir;
}

# Retrieve the command-line arguments from %ARGV
# (Getopt::Euclid has already parsed the command-line and placed the results
# in %ARGV).

my $user       = $ARGV{ '--user'    };
my $src_dir    = chomp_dir( $ARGV{'--srcdir'} );
my $dest_dir   = chomp_dir( $ARGV{'--destdir'} );
my $prefix     = $ARGV{ '--prefix'  };
my $suffix     = $ARGV{ '--suffix'  };
my $keep       = $ARGV{ '--keep'    };
my $is_remove  = $ARGV{ '--remove'  };
my $is_verbose = $ARGV{ '--verbose' };

# Keep only the last $option_of{'keep'}-1 archives if $is_remove is true

remove_old_tars(
    {   'pattern' => "$dest_dir/$prefix*$suffix",
        'keep'    => $keep - 1,     # subtract 1: we will create a new one
        'remove'  => $is_remove,
        'verbose' => $is_verbose,
    }
);

# Archive the source directory to a tar in the destination dir

my $dest_tar = generate_tar_name( $dest_dir, $prefix, $suffix );
tar_src_dir(
    {   'src_dir'  => $src_dir,
        'dest_tar' => $dest_tar,
        'verbose'  => $is_verbose,
    }
);

# Change the owner and group of the generated archive file

change_file_owner_group(
    {   'filename' => $dest_tar,
        'user'     => $user,
        'verbose'  => $is_verbose,
    }
);

__END__

=head1 NAME

hometar - Archives (tars) the contents of a user's home directory

=head1 VERSION

This documentation refers to hometar version 1.0.0

=head1 USAGE

hometar [options]...

=head1 REQUIRED ARGUMENTS

=over

=item -[-]u[ser] [=] 

Specify the name of the owner of the archive file. The archive files user and
group will be set to this value

=item -[-]s[rcdir] [=] 

Specify the home directory to be archived

=item -[-]d[estdir] [=] 

Specify the directory into which the archive file is to be saved

=back

=head1 OPTIONS

=over

=item -[-]p[refix]  [=] 

Specify the prefix to use for the created archive file. The default is


=for Euclid:
    prefix.default: 'home_'

=cut

=item -x [=]  | --suffix [=] 

Specify the suffix of the created archive file. The default is
'suffix.default'

=for Euclid:
 suffix.default: <.tar.gz>

=cut

=item -[-]k[eep]  [=] 

Specify the number of archives to keep, including the newly created archive.
The default is 

=for Euclid:
    keep.type: +int
    keep.default: 5

=cut

=item -[-]r[emove]  [=] []

Specify this to enable removal of obsolete archives

=for Euclid:
    remove.type: int
    remove.default: 0
    remove.opt_default: 1

=cut

=item -[-]v[erbose]  [=] []

Specify this to get output of the archival process

=for Euclid:
    verbose.type: int
    verbose.default: 0
    verbose.opt_default: 1

=cut

=back

=head1 COPYRIGHT

Copyright (c) 2019 Justin Hanekom