#!/usr/bin/perl -w

use strict;
use App::Options (
    option => {
        rsync_opts => {
            description => "Extra rsync options",
            type        => "string",
            default     => "",
            required    => 0,
        },
        hist_levels => {
            description => "A comma-separated list of numbers to specify the history levels",
            type        => "string",
            default     => "-7,4,3",
            required    => 0,
        },
        nobackup => {
            description => "Don't do backup, just do the history rotation",
            type        => "bool",
            default     => 0,
            required    => 0,
        },
    }
);
use Cwd qw(abs_path);
use Log::Log4perl qw(:easy);
use POSIX;
use Time::Local;

# --- subs

sub esc {
    local $_ = shift;
    s/'/'"'"'/g;
    "'$_'";
}

# --- main

# check args
my $hist_levels;
if ($App::options{hist_levels} !~ /^(-?\d+)(,\s*(-?\d+))*$/) {
    die "Invalid hist_levels, please use a comma-separated list of numbers\n";
}
$hist_levels = [split /,\s*/, $App::options{hist_levels}];
@ARGV == 2 or die "Usage: $0 <src> <dest>\n";
my ( $src, $dst ) = @ARGV;
for ( $src, $dst ) {s!/$!!}
system "mkdir -p " . esc("$dst/current");
( -d "$dst/current" ) or die "Gagal membuat creating $dst/current\n";

Log::Log4perl->easy_init($DEBUG);

# do backup!
unless ( $App::options{nobackup} ) {
    unless (-e "$dst/.work") {
	INFO "cp -la $dst/current ==> $dst/.work ...";
	system "nice -n19 cp -al " . esc("$dst/current") . " " . esc("$dst/.work");
	WARN "cp command didn't succeed ($?), please recheck" if $?;
    }

    INFO "rsync $src ==> $dst/.work ...";
    system "nice -n19 rsync $App::options{rsync_opts} -a --del --force "
        . esc("$src/") . " "
        . esc("$dst/.work/") . "\n";
    WARN "rsync command didn't succeed ($?), please recheck" if $?;
}

chdir($dst) or LOGDIE "Can't chdir to $dst: $!";

my $now    = time;
unless ( $App::options{nobackup} ) {
    INFO "rename .work ==> new current ...";
    system "touch .current.timestamp";
    my @st     = stat(".current.timestamp");
    my $tstamp = POSIX::strftime( "%Y-%m-%d\@%H:%M:%S+00",
        gmtime( $st[9] || $now ) );
    rmdir "current" or rename "current", "hist.$tstamp";
    rename ".work", "current";
}

INFO "Removing old histories ...";
for my $level ( 1 .. @$hist_levels ) {
    my $is_highest_level  = $level == @$hist_levels;
    my $prefix            = "hist" . ( $level == 1 ? '' : $level );
    my $prefix_next_level = "hist" . ( $level + 1 );
    my $n                 = $hist_levels->[ $level - 1 ];
    my $moved             = 0;

    if ( $n > 0 ) {
        INFO "Only keeping $n level-$level histories ...";
        my @f = reverse sort grep { !/\.work$/ } glob "$prefix.*";
        my $any_tagged = ( grep {/t$/} @f ) ? 1 : 0;
        for my $f ( @f[ $n .. @f - 1 ] ) {
            my ( $st, $tagged ) = $f =~ /[^.]+\.(.+?)(t)?$/;
            my $f2 = "$prefix_next_level.$st";
            if (   !$is_highest_level
                && !$moved
                && ( $tagged || !$any_tagged ) )
            {
                INFO "Moving history level: $f -> $f2";
                system "mv " . esc($f) . " " . esc($f2);
                $moved++;
                if ( $f ne $f[0] ) {
                    my $e3 = esc( $f[0] );
                    system "mv $e3 ${e3}t";
                }
            }
            else {
                INFO "Removing history: $f ...";
                system "nice -n19 rm -rf " . esc($f);
            }
        }
    }
    else {
        $n = -$n;
        INFO "Only keeping $n day(s) of level-$level histories ...";
        my @f = reverse sort grep { !/\.work$/ } glob "$prefix.*";
        my $any_tagged = ( grep {/t$/} @f ) ? 1 : 0;
        for my $f (@f) {
            my ( $st, $tagged ) = $f =~ /[^.]+\.(.+?)(t)?$/;
            my $f2 = "$prefix_next_level.$st";
            my $t;
            $st =~ /(\d\d\d\d)-(\d\d)-(\d\d)\@(\d\d):(\d\d):(\d\d)\+00/;
            $t = timegm( $6, $5, $4, $3, $2 - 1, $1 ) if $1;
            $st && $t or do {
                WARN "Wrong format of history, ignored: $f";
                next;
            };
            if ( $t > $now ) {
                WARN "History in the future, ignored: $f";
                next;
            }
            my $delta = ( $now - $t ) / 86400;
            if ( $delta > $n ) {
                if (   !$is_highest_level
                    && !$moved
                    && ( $tagged || !$any_tagged ) )
                {
                    INFO "Moving history level: $f -> $f2";
                    system "mv " . esc($f) . " " . esc($f2);
                    $moved++;
                    if ( $f ne $f[0] ) {
                        my $e3 = esc( $f[0] );
                        system "mv $e3 ${e3}t";
                    }
                }
                else {
                    INFO "Removing history: $f ...";
                    system "nice -n19 rm -rf " . esc($f);
                }
            }
        }
    }
}

__END__

algorithm:

* creating current backup:

0. mkdir dest if not exists
1. cp -la dest/current -> dest/.work, unless current doesnt exist or .work already exists
2. mkdir .work is not exists
3. rsync src -> dest/.work
4. if dest/current exist (and dest/.current.timestamp exist), rename into
   dest/hist.TIMESTAMP, where TIMESTAMP is from dest/.current.timestamp
5. touch dest/.current.timestamp && rename dest/.work -> dest/current

so timestamp is time where the backup is _finished_, not started
(could be either way, though, we pick finish time).

if rsync process (#3) (or cp, #2) is interrupted, we will just
continue from here after restart, since .work already exists.

* backup history:
