#!/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