# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Backup.pm 3239 2008-08-22 14:59:28Z josh $

package BigMed::Backup;
use strict;
use warnings;
use BigMed;
use BigMed::Archive;
use BigMed::DiskUtil qw(bm_file_path bm_delete_dir bm_untaint_filepath);
use BigMed::Log;
use BigMed::Trigger;

my @DATA_DIRS = qw(counters data search templates_custom);
my @SITE_DIRS = qw(bm~pix bm~doc bm.theme);   # ~ replaced with bm->env('DOT')

sub new {
    my $class   = shift;
    my $bm      = BigMed->bigmed;
    my $mdata   = bm_untaint_filepath( $bm->env('MOXIEDATA') );
    my $bakdir  = bm_file_path( $mdata, 'backups' );
    my $bmadmin = bm_file_path( $bm->env('BMADMINDIR') );
    my $type =
      bma_can_compress('tgz') ? 'tgz' : ( bma_all_compress_types() )[0];
    my $self = bless {
        bakdir  => $bakdir,
        mdata   => $mdata,
        bmadmin => $bmadmin,
        type    => $type,
    }, $class;

    return $self;
}

sub do_backup {
    my $self = shift;
    if ( !$self->can_backup ) {
        $self->log( 'warning' =>
              'Backup: Backup requested, but no compression tools detected' );
        return 1;
    }

    my ( $sec, $min, $hour, $day, $month, $year ) = (localtime)[0 .. 5];
    my $file = sprintf(
        "%04d.%02d.%02d-%02d.%02d.%02d",
        $year + 1900,
        $month + 1, $day, $hour, $min, $sec
    );
    my $tdir = bm_file_path( $self->backup_dir, $file );
    my $type = $self->archive_type;

    #moxiedata files
    foreach my $dir (@DATA_DIRS) {
        my $orig = bm_file_path( $self->moxiedata, $dir );
        next if !-d $orig;
        my $tfile = bm_file_path( $tdir, 'moxiedata-files', $dir );
        $self->log( 'info' => "Backup: Compressing $dir" );
        bma_compress( $orig, $tfile, $type ) or return;
        $self->call_trigger( 'after_moxiedata_dir', $orig );
    }

    #site files from html directory
    my $dot         = BigMed->bigmed->env('DOT');
    my @site_dirs   = map { s/~/$dot/g; $_; } @SITE_DIRS;
    my $site_select = BigMed::Site->select();
    my $site;
    while ( $site = $site_select->next ) {
        my $url_file = $site->html_url;
        $url_file =~ s{^https?://(www[.])?}{}msi;
        $url_file =~ s{[^a-zA-Z0-9\-_.]+}{-}msg;
        next if !$url_file;

        my $tag      = $self->log_data_tag($site);
        my $html_dir = $site->html_dir;
        foreach my $dir (@site_dirs) {
            my $orig = bm_file_path( $html_dir, $dir );
            next if !-d $orig;
            my $tfile =
              bm_file_path( $tdir, 'public-files', $url_file, $dir );
            $self->log( 'info' => "Backup: Compressing $dir for $tag" );
            bma_compress( $orig, $tfile, $type ) or return;
            $self->call_trigger( 'after_site_dir', $site, $tdir, $dir );
        }
    }
    return if !defined $site;

    #theme files
    my $theme_dir = bm_file_path( $self->bmadmin, 'themes', '_custom' );
    if ( -d $theme_dir ) {
        my $tfile = bm_file_path( $tdir, 'bmadmin-themes', '_custom' );
        $self->log( 'info' => "Backup: Compressing custom theme directory" );
        bma_compress( $theme_dir, $tfile, $type ) or return;
        $self->call_trigger('after_theme_dir');
    }

    $self->log( 'info' => "Backup: Backup complete, saved in backups/$file" );
    return 1;
}

sub prune {
    my ($self, $limit) = @_;
    my $bm   = BigMed->bigmed;
    $limit ||= $bm->env('BACKUP_KEEP');
    $limit = int($limit);

    my @files;
    my $bakdir      = $self->backup_dir;
    if ( -e $bakdir ) {
        my ( $BAKDIR, $file );
        opendir( $BAKDIR, $bakdir )
          or return $bm->set_io_error( $BAKDIR, 'opendir', $bakdir, $! );
        while ( $file = readdir $BAKDIR ) {
            next if $file !~ /^\d{4}[.]/;
            my $path = bm_file_path( $bakdir, $file );
            push @files, [$path, -M $path];
        }
        closedir $BAKDIR;
    }
    my $num_files = scalar @files;
    return 1 if $num_files <= $limit;
    
    #prune the oldest
    my @by_age = map { $_->[0] } sort { $a->[1] <=> $b->[1] } @files;
    foreach my $path ( @by_age[$limit .. $num_files - 1] ) {
        $self->log( 'info' => "Backup: Pruning $path" );
        bm_delete_dir($path) or return;
    }
    return 1;
}

sub archive_type {
    return $_[0]->{type};
}

sub backup_dir {
    return $_[0]->{bakdir};
}

sub bmadmin {
    return $_[0]->{bmadmin};
}

sub moxiedata {
    return $_[0]->{mdata};
}

sub last_backup {    #in days, including fractional days
    my $self = shift;
    $self->_init_day_age() if !exists $self->{day_age};
    return $self->{day_age};
}

sub can_backup {
    return $_[0]->{type};
}

sub _init_day_age {
    my $self = shift;
    my $bm   = BigMed->bigmed;

    my $most_recent = 0;
    my $bakdir      = $self->backup_dir;
    if ( -e $bakdir ) {
        my ( $BAKDIR, $file );
        opendir( $BAKDIR, $bakdir )
          or return $bm->set_io_error( $BAKDIR, 'opendir', $bakdir, $! );
        while ( $file = readdir $BAKDIR ) {
            next if $file !~ /^\d{4}[.]/mso;
            my $days = -M bm_file_path( $bakdir, $file );
            $most_recent = $days if !$most_recent || $days < $most_recent;
        }
        closedir $BAKDIR;
    }
    $self->{day_age} = $most_recent;
    return $self;
}

1;

__END__

=head1 NAME

BigMed::Backup - Creates compressed backups of Big Medium data directories

=head1 SYNOPSIS

    use BigMed::Backup;

    my $backup = BigMed::Backup->new();
    $backup->do_backup();    #make the backup
    $backup->prune(3);       #remove all but most recent 3 backups


=head1 METHODS

=head2 C<new()>

Returns a new BigMed::Backup object.

=head2 C<do_backup()>

    $backup->do_backup();

Creates compressed backups of several key Big Medium data directories.
Directories are compressed as C<.tgz> (aka C<.tar.gz>) files, unless
that format is not available. Fallbacks are C<.zip>, C<.tbz> and C<.tar>.

The backup is stored in a time-stamped directory in Big Medium's
C<moxiedata/backups> directory. Inside that directory are three
directories:

=over 4

=item * C<moxiedata-files>

Contains compressed copies of the C<counters>, C<data>, C<search> and
C<templates_custom> directories from Big Medium's C<moxiedata> directory.

=item * C<public-files>

Contains a directory for each site. Each of these site directories is
named based on the URL of the site's page directory in Big Medium. These
site directories each contain compresed backups of the site's bm.assets,
bm.doc, bm.pix and bm.theme directories (if they exist for the site).

=item * C<bmadmin-themes>

Contains a compressed copy of the C<_custom> directory (if there is one)
from C<bmadmin/themes>.

=back

Returns true on success, or false on failure (along with adding a
message to the Big Medium error queue).

=head2 C<prune($num)>

    $backup->prune($num);

Deletes all but the most recent C<$num backups. If no C<$num> argument is
provided, the value is taken from Big Medium's C<BACKUP_KEEP> environmental
variable.

Returns true on success, or false on failure (along with adding a
message to the Big Medium error queue).

=head2 C<last_backup()>

    my $days = $backup->last_backup();

Returns the number of days, including fractional days, since the last backup
in the C<moxiedata/backups> directory. If no backup exists, returns 0.

=head2 C<can_backup()>

    $backup->can_backup();

Returns true if a compression mechanism is detected (via BigMed::Archive).
More specifically, it returns the archive type that will be used to compress
files. Same result as C<archive_type()>.

=head2 C<archive_type()>

    my $type = $backup->archive_type(); #tgz, zip, tbz or tar

Returns the archive type that will be used to compress files. Returns
false if cannot backup. Same result as C<can_backup()>.

=head2 C<backup_dir()>

    my $dir = $backup->backup_dir();

Returns the path to the backup directory where backups will be stored.

=head2 C<bmadmin()>

    my $dir = $backup->bmadmin();

Returns the path to the system's C<bmadmin> directory.

=head2 C<moxiedata()>

    my $dir = $backup->moxiedata();

Returns the path to the system's C<moxiedata> directory.

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut
