#!/usr/bin/perl
#
# charon by Stefan Tomanek <stefan.tomanek@wertarbyte.de>
#              http://wertarbyte.de/tartarus.shtml
#
# This script will remove tartarus backup files from an FTP server
# that have reached a certain age.
#
# WARNING: This script will delete your backup data when called
# improperly

use strict;
use warnings;
use Getopt::Long;
use Tartarus::Charon::Filter;

# your backup FTP server
my $host = undef;
# username and password
my ($user, $pass) = (undef, "");
my $read_password = 0;
my $use_ssl = 0;
my $dir = "/";

my $days_to_expire = undef;
my $uprofile = undef;
my $all = 0;

my $dry_run = 0;
my $verbose = 0;
my $help = 0;

=pod

=head1 NAME

charon.ftp - remove backup files that exceed a certain age from an FTP aite

=head1 SYNOPSIS

B<charon.ftp> --host I<host> [--ssl] --user I<name> (--password I<password> | --readpassword) [--dir F<directory>] --maxage I<days> (--profile I<identifier> | --all) [--dry-run] [--verbose] [--help]

=head1

The program charon.ftp removes backup archives created by tartarus from an FTP
server once they reach a certain age and expire. Files that are older than the
number of days specified on the command line are considerer candidates for
removal unless other, non-expired backups are based on them, so full backups
may survive longer than expected.

=head1 OPTIONS AND ARGUMENTS

=over

=item B<--host> I<server>

Specify the FTP server to connect to

=item B<--ssl>

Use SSL to secure the connection (requires Net::FTPSSL)

=item B<--user> I<name>

Define the username to authenticate with

=item B<--password> I<pass>

Set the password to authenticate with

=item B<--readpassword>

Instead of specifying the password on the commandline, it can also be read from
the standard input. This is preferable for obvious security reasons.

=item B<--dir>

Specify the directory on the server the backups files are stored in.

=item B<--maxage> I<n>

Set the maximum number of days after which a backup is expired.

=item B<--profile> I<name>

Set the profile name to process. This is mandatory unless B<--all> is supplied.

=item B<--all>

When this is set, the expiration date is applied to alle files found on the server
and identified as tartarus backups.

=item B<--dry-run>

This option prevents charon from actually deleting files.

=item B<--verbose>

This option makes charon more verbose about the actions made

=item B<--help>

This parameter shows a brief command reference.

=back

=cut

sub usage {
    my ($error) = @_;
    print <<EOF;
charon.ftp by Stefan Tomanek <stefan.tomanek\@wertarbyte.de>

    --host          FTP server to connect to
    --ssl           use SSL for contacting the server
    --user          username to authenticate with
    --password      password to authenticate with
    --readpassword  read password from stdin
    --dir           server directory of the backup files
    --maxage        maximum age of backup files (in days)
    --profile       backup profile name to process
    --all           process all files found in the directory
    --dry-run       do a test run, don't actually delete files
    --verbose       be more verbose about the actions made
    --help          show this help text
EOF
    if (defined $error) {
        print "\n$error\n";
    }
    exit 1;
}

GetOptions(
    "host|h|server=s"   => \$host,
    "ssl"               => \$use_ssl,
    "username|user|u=s" => \$user,
    "password|p=s"      => \$pass,
    "readpassword|r"    => \$read_password,
    "maxage|m=i"        => \$days_to_expire,
    "profile|p=s"       => \$uprofile,
    "all|a"             => \$all,
    "directory|dir|d=s" => \$dir,
    "test|dry-run|n"    => \$dry_run,
    "verbose|v"         => \$verbose,
    "help|h"            => \$help
) || usage();

# which module do we use for the connection?
my $ftpclass;
if ($use_ssl) {
    require Net::FTPSSL;
    $ftpclass = 'Net::FTPSSL';
} else {
    require Net::FTP;
    $ftpclass = 'Net::FTP';
}

usage "No expiration age given" unless defined $days_to_expire;
usage "No servername specified" unless defined $host;
usage "No username specified" unless defined $user;
usage "Neither --all nor a single backup profile specified" unless ($all || defined $uprofile);

if ($read_password) {
    print STDERR "Reading password:" if $verbose;
    while (<STDIN>) {
        $pass .= $_;
    }
    print STDERR " Thank you.\n" if $verbose;
}

my $ftp = $ftpclass->new($host, Debug => 0, Passive => 1) || die "Unable to connect to server";
$ftp->login($user, $pass) || die "Unable to authenticate, ", $ftp->message();
$ftp->cwd($dir) || die "Error changing to backup directory, ", $ftp->message();

my @listing;
if (ref($ftp) eq 'Net::FTP') {
    @listing = $ftp->ls();
} elsif (ref($ftp) eq 'Net::FTPSSL') {
    # FTPSSL uses a different method name for
    # retrieving the directory listing
    @listing = $ftp->nlst();
}

my $c = new Tartarus::Charon::Filter();
$c->verbose($verbose);
# some FTP servers include . and .. in their listing
$c->files( grep {!/^\.\.?$/} @listing );

# if $uprofile is undefined, the filter works on all profiles
for my $file ($c->expire($days_to_expire, $uprofile)) {
    print STDERR "Removing file $file...\n";
    unless ($dry_run) {
        $ftp->delete("$file") || print STDERR "Error removing $file!\n";
    }
}

$ftp->quit();

=head1 EXAMPLE

Using the script is fairly simple and can probably be best explained by looking
at an example:

    $ charon.ftp --host safehaven --user john --password SECRET --dir / --maxage 7 --profile home --verbose --dry-run

This command line will try to log into the server "safehaven" using the user
name "john" and his password "SECRET" and remove backup file from the profile
"home" with are more than 7 days old. Due to the command line switch
"--dry-run", no files are actually deleted - the script will only explain its
potential actions in its output.

=head1 Integration with Tartarus

Charon does not (and in fact, cannot) read tartarus backup profiles; by using
hooks however it can be called from Tartarus after completing a successfull
backup run. This way, Tartarus can pass the configuration variables to Charon:

    # Hook in Charon
    TARTARUS_POST_PROCESS_HOOK() {
        # pass configuration variables to charon
        # transmit the password through stdin to hide it from "ps ax"
        local CHARON="/usr/sbin/charon.ftp"
        local MAX_AGE_IN_DAYS="7"
        echo -n "$STORAGE_FTP_PASSWORD" | $CHARON \
            --host "$STORAGE_FTP_SERVER" --user "$STORAGE_FTP_USER" \
            --readpassword \
            --dir "$STORAGE_FTP_DIR" \
            --maxage "$MAX_AGE_IN_DAYS" \
            --profile "$NAME"
    }

TARTARUS_POST_PROCESS_HOOK will only be executed in case of a successfull
backup, so there won't be any files removed if tartarus encounters an error
during the new backup creation.

=head1 AUTHOR

Stefan Tomanek E<lt>stefan.tomanek@wertarbyte.deE<gt>

=cut
