#!/usr/bin/perl -w
# $Id: slack-sync,v 1.15 2006/09/25 18:35:17 alan Exp $
# vim:sw=2
# vim600:fdm=marker
# Copyright (C) 2004-2006 Alan Sundell <alan@sundell.net>
# All Rights Reserved.  This program comes with ABSOLUTELY NO WARRANTY.
# See the file COPYING for details.
#
# This script is in charge of copying files from the (possibly remote)
# master directory to a local cache, using rsync

require 5.006;
use warnings FATAL => qw(all);
use strict;
use sigtrap qw(die untrapped normal-signals
               stack-trace any error-signals);

use File::Path;

use constant LIB_DIR => '/usr/lib/slack';
use lib LIB_DIR;
use Slack;

my @rsync = ('rsync',
              '--cvs-exclude',
              '--recursive',
              '--copy-links',
              '--times',
              '--perms',
              '--sparse',
              '--delete',
              );

(my $PROG = $0) =~ s#.*/##;

sub check_cache ();
sub sync_role ($);
sub apply_default_perms_to_role ($);

########################################
# Environment
# Helpful prefix to die messages
$SIG{__DIE__} = sub { die "FATAL[$PROG]: @_"; };
# Set a reasonable umask
umask 077;
# Get out of wherever (possibly NFS-mounted) we were
chdir("/")
  or die "Could not chdir /: $!";
# Autoflush on STDERR
select((select(STDERR), $|=1)[0]);

########################################
# Config and option parsing {{{
my $usage = Slack::default_usage("$PROG [options] <role> [<role>...]");
# Option defaults
my %opt = ();
Slack::get_options(
  opthash => \%opt,
  usage => $usage,
  required_options => [ qw(source cache) ],
);

# Arguments are required
die "No roles given!\n\n$usage" unless @ARGV;

# Prepare for backups
if ($opt{backup} and $opt{'backup-dir'}) {
  # Make sure backup directory exists
  unless (-d $opt{'backup-dir'}) {
    ($opt{verbose} > 0) and print STDERR "Creating backup directory '$opt{'backup-dir'}'\n";
    if (not $opt{'dry-run'}) {
      eval { mkpath($opt{'backup-dir'}); };
      die "Could not mkpath backup dir '$opt{'backup-dir'}': $@\n" if $@;
    }
  }
  push(@rsync, "--backup", "--backup-dir=$opt{'backup-dir'}");
}
# Look at source type, and add options if necessary
if ($opt{source} =~ m/^[\w@\.-]+::/) {
  # This is tunnelled rsync, and so needs an extra option
  push @rsync, '-e', 'ssh';
}

# Pass options along to rsync
if ($opt{'dry-run'}) {
  push @rsync, '--dry-run';
}
# Pass options along to rsync
if ($opt{'verbose'} > 1) {
  push @rsync, '--verbose';
}
# }}}

# Make sure we've got the right perms before we copy stuff down
check_cache();

# This hash is just to avoid calling rsync twice if two subroles are
# installed.  We only care since it's remote, and therefore slow.
my %already_synced;

# copy over the new files
for my $full_role (@ARGV) {
  # Get the first element of the role name (the base role)
  # e.g., from "google.foogle.woogle", get "google"
  my $base_role = (split /\./, $full_role, 2)[0];

  # Don't bother syncing twice
  next if $already_synced{$base_role};

  ($opt{verbose} > 1)
    and print STDERR "$PROG: Calling sync_role for $base_role\n";

  sync_role($base_role) unless ($already_synced{$base_role});
  $already_synced{$base_role} = 1;
}
exit 0;

# Make sure the cache directory exists and is mode 0700, to protect files
# underneath in transit
sub check_cache () {
  my $cache = $opt{cache} . "/roles";
  if (not $opt{'dry-run'}) {
    if (not -d $cache) {
      ($opt{verbose} > 0) and print STDERR "$PROG: Creating '$cache'\n";
        eval { mkpath($cache); };
        die "Could not mkpath cache dir '$cache': $@\n" if $@;
    }
    ($opt{verbose} > 0) and print STDERR "$PROG: Checking perms on '$cache'\n";
    if ($> != 0) {
      warn "WARNING[$PROG]: Not superuser; unable to chown files\n";
    } else {
      chown(0, 0, $cache)
        or die "Could not chown 0:0 '$cache': $!\n";
    }
    chmod(0700, $cache)
      or die "Could not chmod 0700 '$cache': $!\n";
  }
}

# Copy the files for a role from SOURCE to CACHE
sub sync_role ($) {
  # $role is the base role name
  my ($role) = @_;
  my @subdirs;
  my ($source, $destination);

  # Pull down the whole tree locally
  $source = $opt{source} . "/roles/" . $role . "/";
  $destination = $opt{cache} . "/roles/" . $role . "/";
  
  my @command = (@rsync, $source, $destination);
  ($opt{verbose} > 0) and print STDERR "$PROG: Syncing cache from $source with '@command'\n";
  my ($rsync_pid);

  # Make sure our destination directory exists (or rsync will error out)
  if (not -d $destination) {
    if (not $opt{'dry-run'}) {
      ($opt{verbose} > 0) and print STDERR "$PROG: Creating '$destination'\n";
      eval { mkpath($destination); };
      die "Could not mkpath cache dir '$destination': $@\n" if $@;
    } else {
      warn "WARNING[$PROG]: '$destination' does not exist and we can't create it when doing dry run.  rsync will fail.\n";
    }
  }

  # Divide into parent (which will make a list of files to install)
  # and child (which will exec rsync)
  if ($rsync_pid = fork) {
    # Parent
  } elsif (defined $rsync_pid) {
    # Child
    # This redirection is necessary because rsync sends
    #   verbose output to STDOUT
    open(STDIN, "<", "/dev/null")
      or die "Could not redirect STDIN from /dev/null\n";
    open(STDOUT, ">&STDERR")
      or die "Could not redirect STDOUT to STDERR\n";
    exec(@command);
    die "Could not exec '@command': $!\n";
  } else {
    die "Could not fork: $!\n";
  }

  # Wait for rsync to exit and check return value
  my $kid = waitpid($rsync_pid, 0);
  if ($kid != $rsync_pid) {
    # this should never happen, but just in case...
    die "waitpid returned $kid\n";
  } elsif ($?) {
    Slack::check_system_exit(@command);
  }
}

