MOON
Server: Apache/2.2.31 (Unix) mod_ssl/2.2.31 OpenSSL/0.9.8e-fips-rhel5 mod_bwlimited/1.4
System: Linux csr818.wilogic.com 2.6.18-419.el5xen #1 SMP Fri Feb 24 22:50:37 UTC 2017 x86_64
User: digitals (531)
PHP: 5.4.45
Disabled: NONE
Upload Files
File: //scripts.20110531.215904.25158/cpanelsync
#!/usr/bin/perl
package Scripts::cpanelsync;

# cpanel - cpanelsync                             Copyright(c) 2010 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

## "grep '###' cpanelsync" gives an overview of the logic and files downloaded

BEGIN {
    my $running_in_debugger = exists $INC{'perl5db.pl'};
    if ($running_in_debugger) {
        $ENV{'LANG'} = 'C';
    }

    if ( $ENV{'LANG'} ne 'C' ) {
        $ENV{'LANG'} = 'C';
        exec $0, @ARGV;
        die 'Failed to recreate self in a sane env';
    }
    unshift @INC, '/usr/local/cpanel';
}

use strict;
use warnings;
use Socket;
use Cpanel::Tar         ();
use Cpanel::HttpRequest ();
use Cpanel::SafeDir::MK ();
use bytes;    # must import no () allowed

# Package global; redefined in unit tests
our $cpanelsync_excludes       = '/etc/cpanelsync.exclude';
our $cpanelsync_chmod_excludes = '/etc/cpanelsync.no_chmod';

## if invoked as a script, there is nothing in the call stack
my $invoked_as_script = !caller();
__PACKAGE__->script(@ARGV) if ($invoked_as_script);

sub script {
    my ( $package, @argv ) = @_;
    $| = 1;

    my $gotSigALRM = 0;

    $SIG{'INT'} = 'IGNORE';

    my $hasmd5 = 0;
    eval { require Digest::MD5; $hasmd5 = 1; };
    my $hasbzip2 = 0;
    eval { require Compress::Bzip2; $hasbzip2 = 1; };
    my $has_rollback;
    eval { require Cpanel::RollBack; $has_rollback = 1; };

    my $nfok = 0;
    if ( $argv[0] && $argv[0] =~ m/404/ ) {
        $nfok = 1;
        shift @argv;
    }
    my $learn_repo = 0;
    my $repomode   = 0;
    my $repo;
    if ( $argv[0] && $argv[0] =~ m/\-\-repo/ ) {
        shift @argv;
        $repomode = 1;
        $repo     = shift(@argv);
    }
    if ( $argv[0] && $argv[0] =~ m/\-\-learnrepo/ ) {
        shift(@argv);
        $learn_repo = 1;
    }

    my $exit_code = 0;

    my $connecttimeout = 25;
    my $host           = $argv[0] || '';
    my $url            = $argv[1] || '/';
    my $root           = $argv[2] || '/';

    if ( !$host || $host eq '' ) {
        die "$0: Usage $0 <host> <uri> <syncroot>";
    }

    eval {
        no warnings;
        local $SIG{'__DIE__'};
        local $SIG{'__WARN__'};
        require Cpanel::Carp;
        local $SIG{'__DIE__'};
        local $SIG{'__WARN__'};
        Cpanel::Carp::enable();
        $Cpanel::Carp::OUTPUT_FORMAT = 'supress';
    };

    if ($has_rollback) {
        my $append_to_rollback_conf_ref = UNIVERSAL::can( 'Cpanel::RollBack', 'append_to_rollback_conf' );
        if ( defined $append_to_rollback_conf_ref ) {
            $append_to_rollback_conf_ref->($root);
        }
    }

    my $httpClient = Cpanel::HttpRequest->new();
    if ( !-d $root ) {
        Cpanel::SafeDir::MK::safemkdir( $root, '0755', 2 );
        if ( !-d $root ) {
            die "Unable to create directory $root";
        }
    }

    my $lock = 'locked';

    my $sleep      = 30;
    my $lock_count = 0;
    my $trycount   = 0;

    # Check for Locked file
    ### "$url/.cpanelsync.lock",
    while ( $lock =~ m/locked/i ) {
        my ( $lock, $status ) = $httpClient->request(
            'exitOn404' => $nfok,
            'host'      => $host,
            'url'       => "$url/.cpanelsync.lock",

            #http 1.1 on the first request, but drop the connection on the
            #second one to not tie up the server
            'protocol' => ( $lock_count == 0 ? 1 : 0 )
        );
        exit if ( $status == 0 );
        last if ( !$lock || $lock !~ m/locked/i );
        $lock_count++;
        if ( $lock_count > 20 ) {
            $sleep += 30;
        }
        elsif ( $lock_count == 20 ) {
            $sleep = 120;
        }

        # if these next lines are changed, update /scripts/cpanel_easy_sanity_check's check for them as well
        print "The update server is currently updating its files.\n";
        print "It may take up to 30 minutes before access can be obtained.\n";
        print "Waiting $sleep seconds for access to the update server......\n";

        $httpClient->disconnect();    #do not leave the connection open

        if ( ++$trycount % 30 == 0 ) { $httpClient->skiphost(); }
        sleep $sleep;
        print "Checking again....\n";

    }

    ### "$url/.cpanelsync.version",
    if ($repomode) {
        $exit_code = check_repo_version( $repo, $learn_repo, $httpClient, $host, $url );
    }

    my $dotcpanelsync = "$root/.cpanelsync";

    my %OLDFILES;
    my %NEWFILES;
    if ( !-e $dotcpanelsync ) {    ###
        ## if .cpanelsync does not exist, download/extract the .tar.bz2
        my $basedir = $root;
        my @DIRS = split m{ [/] }xms, $basedir;
        pop @DIRS;
        $basedir = join '/', @DIRS;

        my $basename = $url;
        my @BDIR = split m{ [/] }xms, $basename;
        $basename = pop @BDIR;

        if ( $basedir eq '' ) { $basedir = '/'; }

        my $bz2 = "$basedir/$basename.tar.bz2";
        unlink $bz2;
        ### "http://${host}${url}.tar.bz2"
        downloadfile( $httpClient, "http://${host}${url}.tar.bz2", $bz2 );

        my $tarcfg = Cpanel::Tar::load_tarcfg();
        system( $tarcfg->{'bin'}, '-x', '-p', $tarcfg->{'no_same_owner'}, '-j', '-v', '-C', $basedir, '-f', $bz2 );
        unlink $bz2;

        ## TODO?: take the md5sums of the new files, and manipulate %OLDFILES (or %MD5LIST?)
    }
    else {
        if ( open my $cpsync_fh, '<', $dotcpanelsync ) {
            while (<$cpsync_fh>) {
                chomp;
                my ( $type, $file ) = ( split( /===/, $_ ) )[ 0, 1 ];
                if ($file) {
                    $OLDFILES{$file} = $type;
                }
            }
            close $cpsync_fh;
        }
    }

    my @FILELIST;
    $trycount = 0;
    my $usebz2  = 1;
    my $skipbz2 = 0;

    # Download file list to sync
    while (1) {    ###
        $trycount++;

        if   ( $trycount % 2 == 0 ) { $usebz2 = 0; }
        else                        { $usebz2 = 1; }

        ### "$url/.cpanelsync.bz2",  -or-
        ### "$url/.cpanelsync"
        ## note: this overwrites the local $dotcpanelsync[.bz2] file
        if ( !$skipbz2 && $usebz2 ) {
            $httpClient->request(
                'host'     => $host,
                'url'      => "$url/.cpanelsync.bz2",
                'protocol' => 1,
                'destfile' => "$dotcpanelsync.bz2"
            );

            my $size = ( stat("$dotcpanelsync.bz2") )[7];
            if ( $size && $size > 0 ) {
                unbzip2( $hasbzip2, "$dotcpanelsync.bz2" );
            }
        }
        else {
            $httpClient->request(
                'host'     => $host,
                'url'      => "$url/.cpanelsync",
                'protocol' => 1,
                'destfile' => $dotcpanelsync
            );
        }

        if ( -e $dotcpanelsync ) {
            open( FL, $dotcpanelsync );
            while (<FL>) {
                chomp;
                push( @FILELIST, $_ );
            }
            close(FL);
        }

        if ( -e "$dotcpanelsync.bz2" ) {
            unlink "$dotcpanelsync.bz2";
            $skipbz2 = 1;
        }

        last if ( @FILELIST && $FILELIST[-1] eq '.' );

        if ( $trycount > 1 ) {
            if ( $trycount == 10 ) {
                print "Tried to download the sync file 10 times and failed!\n";
                ## TODO?: document exit codes
                exit 1;
            }
            downloadfailed($httpClient);
        }
    }

    # Global excludes for handling excluded files from update or permission checks
    my @excludes       = get_excludes($cpanelsync_excludes);
    my @chmod_excludes = get_excludes($cpanelsync_chmod_excludes);

    my %MD5LIST;
    if ( -e "$dotcpanelsync.md5s" ) {
        loadmd5s( \%MD5LIST, $root );
    }

    foreach my $fileinfo (@FILELIST) {    ###
        chomp $fileinfo;
        next if ( $fileinfo eq '.' );

        ## note: appending 'r' to these vars to denote that they represent info on the
        ##   remote (incoming) resource. Ideally, they should be packaged in a hash,
        ##   similar to how $target is handled.
        ## $rextra is either an md5 for 'file' $ftype, or symlink destination
        my ( $rtype, $rfile, $rperm, $rextra ) = split( /===/, $fileinfo );

        my $target_info = lstat_target( $root, $rfile );

        ## using %04d as $rperm is a string (comes from the .cpanelsync file)
        $rperm = sprintf( "%04d", $rperm );

        prune_OLDFILES( \%OLDFILES, $rfile, $rtype );

        next if is_excluded( \@excludes, $root, $rfile );

        if ( $rtype eq 'f' ) {
            handle_file( $target_info, \%MD5LIST, $root, $rfile, $hasmd5, $rextra, $skipbz2, $httpClient, $host, $url, $hasbzip2, $repomode, $exit_code, $repo, \@chmod_excludes, $rperm, \%NEWFILES );    ###
        }
        elsif ( $rtype eq 'd' ) {
            handle_dir( $target_info, \@chmod_excludes, $root, $rfile, $rperm );
        }
        elsif ( $rtype eq 'l' ) {
            handle_symlink( $target_info, $rextra );
        }
    }

    my $saferoot = $root;
    $saferoot =~ s/\.\///g;

    handle_deletes( \@FILELIST, \%OLDFILES, $saferoot );

    write_newlist( \%NEWFILES, $saferoot );

    writemd5s( \%MD5LIST, $saferoot );

    ### "$url/.cpanelsync.version",
    if ( $exit_code == 0 && $repomode ) {
        $exit_code = check_repo_version( $repo, 0, $httpClient, $host, $url );
    }

    if ( -x '/scripts/cpanelsync_postprocessor' ) {
        system '/scripts/cpanelsync_postprocessor', $saferoot;
    }
    if ( -x '/scripts/cpanelsync_postprocessor.custom' ) {
        system '/scripts/cpanelsync_postprocessor.custom', $saferoot;
    }

    if ($invoked_as_script) {
        exit $exit_code;
    }
    return $exit_code;
}

sub downloadfile {
    my ( $httpClient, $file, $where ) = @_;

    $file =~ m!http://([^/]+)(.*)!;

    my $host = $1;
    my $url  = $2;

    $httpClient->request(
        'host'     => $1,
        'url'      => $2,
        'protocol' => 1,
        'destfile' => $where,
    );

}

sub getmd5sum {
    my ( $hr_MD5LIST, $root, $file, $hasmd5, %OPTS ) = @_;

    return if ( !defined $file || $file eq '' );

    my $md5;
    my $skipcache = $OPTS{'skipcache'};
    my $mtime     = $OPTS{'mtime'};
    my $size      = $OPTS{'size'};

    my $fullfile = $root . '/' . $file;
    $fullfile =~ s/\/\.\//\//g;
    $fullfile =~ s/\/+/\//g;

    if ( defined $OPTS{'is_normal_file'} ) {
        if ( !$OPTS{'is_normal_file'} ) {
            return '';
        }
    }
    else {
        return '' if ( !-f $fullfile );
    }

    if ( !$size || !$mtime ) {
        ( $size, $mtime ) = ( stat($fullfile) )[ 7, 9 ];
    }

    if (   !$skipcache
        && defined $hr_MD5LIST->{$file}{'size'}
        && $hr_MD5LIST->{$file}{'size'} == $size
        && defined $hr_MD5LIST->{$file}{'mtime'}
        && $hr_MD5LIST->{$file}{'mtime'} == $mtime
        && $hr_MD5LIST->{$file}{'md5'} ne '' ) {
        $hr_MD5LIST->{$file}{'used'} = 1;
        return ( $hr_MD5LIST->{$file}{'md5'} );
    }

    if ($hasmd5) {
        my $fsize = ( stat($fullfile) )[7];
        if ( open my $md5_fh, '<', $fullfile ) {
            my $ctx = Digest::MD5->new;

            my $buf = '';
            my $len = 0;

            while ( my $n = read( $md5_fh, $buf, 65535 ) ) {
                $len += bytes::length($buf);
                $ctx->add($buf);
            }

            $md5 = $ctx->hexdigest() if $len == $fsize;
        }
        else {
            warn "Unable to open $fullfile: $!";
            system 'stat', $fullfile;    #give more debug info
            return '';
        }
    }
    else {
        if ( $ENV{'PATH'} !~ m/\/sbin/ ) {
            $ENV{'PATH'} .= ':/sbin';
        }

        if (   -e '/bin/md5sum'
            || -e '/usr/bin/md5sum'
            || -e '/usr/local/bin/md5sum' ) {
            open( MD5, '-|' ) || exec 'md5sum', $fullfile;
        }
        else {
            open( MD5, '-|' ) || exec 'md5', '-r', $fullfile;
        }
        while (<MD5>) {
            chomp();
            ( $md5, undef ) = split( /\s+/, $_ );
            last();
        }
        close(MD5);
    }

    $hr_MD5LIST->{$file}{'size'}  = $size;
    $hr_MD5LIST->{$file}{'mtime'} = $mtime;
    $hr_MD5LIST->{$file}{'md5'}   = $md5;
    $hr_MD5LIST->{$file}{'used'}  = 1;

    return $md5;
}

sub get_excludes {
    my ($file) = @_;

    return if ( !-e $file || -z _ );

    my @excludes;
    open( EX, '<', $file ) or return undef;
    while (<EX>) {
        next if m/^\s*$/;
        chomp;
        s!/$!!;
        push @excludes, $_;
    }
    close(EX);

    return @excludes;
}

sub loadmd5s {
    my ( $hr_MD5LIST, $dir ) = @_;

    open( MD5, '<', $dir . '/.cpanelsync.md5s' );
    while (<MD5>) {
        chomp();
        my ( $filename, $size, $mtime, $md5 ) = split( /:::/, $_, 4 );
        $hr_MD5LIST->{$filename}{'size'}  = $size;
        $hr_MD5LIST->{$filename}{'mtime'} = $mtime;
        $hr_MD5LIST->{$filename}{'md5'}   = $md5;
    }
    return;
}

sub write_newlist {
    my ( $hr_NEWFILES, $dir ) = @_;

    $dir =~ s/\/$//g;
    open( my $new_fh, '>', $dir . '/.cpanelsync.new' ) || do {
        warn "Could not write new list: " . $dir . '/.cpanelsync.new';
        return;
    };
    foreach my $filename ( keys %$hr_NEWFILES ) {
        print {$new_fh} $filename . "\n";
    }
    close($new_fh);
}

sub writemd5s {
    my ( $hr_MD5LIST, $dir ) = @_;

    $dir =~ s/\/$//g;
    open( MD5, '>', $dir . '/.cpanelsync.md5s' ) || do {
        warn "Could not write md5 cache: " . $dir . '/.cpanelsync.md5s';
        return;
    };
    foreach my $filename ( keys %$hr_MD5LIST ) {
        next if ( !$hr_MD5LIST->{$filename}{'used'} || substr( $filename, 0, 1 ) eq '/' );
        print MD5 join( ':::', $filename, $hr_MD5LIST->{$filename}{'size'}, $hr_MD5LIST->{$filename}{'mtime'}, $hr_MD5LIST->{$filename}{'md5'} ) . "\n";
    }
    close(MD5);
}

sub downloadfailed {
    my ($httpClient) = @_;
    print 'Download Failed... trying again...in..';
    if ($httpClient) {
        $httpClient->disconnect();
    }
    my $sleepsecs = 60;
    for ( my $i = $sleepsecs; $i > 0; $i-- ) {
        print '..' . $i . '..';
        sleep 1;
    }
}

sub unbzip2 {
    my ( $hasbzip2, $file ) = @_;

    if ($hasbzip2) {
        my $outfile = $file;
        $outfile =~ s/\.bz2$//g;
        if ( $outfile eq $file ) { return; }
        my $out_fh;
        open( $out_fh, '>', $outfile ) || do {
            print STDERR "cpanelsync: unbzip2: error opening $outfile\n";
            return;
        };
        my $bzip2 = Compress::Bzip2->new( '-verbosity' => 0 );
        $bzip2->bzopen( $file, 'r' );
        my $buf;
        my $read;
        while ( $read = $bzip2->bzread( $buf, 65535 ) ) {    #65535 is about 35% faster then 512
            if ( $read < 0 ) {
                print "error: $Compress::Bzip2::bzerrno\n";
                print STDERR "error: $Compress::Bzip2::bzerrno\n";
                last;
            }
            syswrite( $out_fh, $buf, $read );
        }
        close($out_fh);
        unlink($file);
    }
    else {
        system( 'bzip2', '-df', $file );
    }
}

sub check_repo_version {
    my ( $repo, $learn, $httpClient, $host, $url ) = @_;
    my $local_repo_v;
    if ( !-e '/var/cpanel' )            { mkdir( '/var/cpanel',            0755 ); }
    if ( !-e '/var/cpanel/cpanelsync' ) { mkdir( '/var/cpanel/cpanelsync', 0755 ); }
    if ( !-e '/var/cpanel/cpanelsync/repoversions' ) {
        mkdir( '/var/cpanel/cpanelsync/repoversions', 0755 );
    }
    my $repo_fh;
    open( $repo_fh, '<', '/var/cpanel/cpanelsync/repoversions/' . $repo ) && do {
        $local_repo_v = readline($repo_fh);
        chomp($local_repo_v);
        close($repo_fh);
    };
    my ( $remote_repo_v, $status ) = $httpClient->request(
        'exitOn404' => 0,
        'host'      => $host,
        'url'       => "$url/.cpanelsync.version",
        'protocol'  => 1,
    );
    if ($remote_repo_v) {
        if ( !$local_repo_v || $remote_repo_v ne $local_repo_v ) {
            open( my $repo_fh, '>', '/var/cpanel/cpanelsync/repoversions/' . $repo );
            print {$repo_fh} $remote_repo_v;
            close($repo_fh);
            if ( !$learn && $local_repo_v ) {
                print "Repo: $repo : version changed from $local_repo_v to $remote_repo_v in mid sync.  Sync needs to be restarted. (learn=$learn)\n";
                return 16;
            }
            else {
                print "Repo: $repo : learned new version: $remote_repo_v (learn=$learn)\n";
            }
        }
        else {
            print "Repo: $repo : check passed : local=$local_repo_v & remote=$remote_repo_v (learn=$learn)\n";
        }
    }
    return 0;
}

sub is_excluded {
    my ( $ar_excludes, $root, $rfile ) = @_;
    my @excludes = @$ar_excludes;

    if (@excludes) {
        my $clean_rfile = $rfile;
        $clean_rfile =~ s!^\./!!;
        my $absfile = $root . '/' . $clean_rfile;
        $absfile =~ tr{/}{}s;
        $absfile =~ s{ [/] \z }{}xmsg;

        ## Note: to take advantage of the "implicit" exclusion of a directory's contents, as written
        ##   the explicitly listed exclude directory must exist at the installation site.
        if ( grep { $_ eq $absfile || ( -d $_ && $absfile =~ m/^\Q$_\E\// ) } @excludes ) {
            print "Skipping sync of $absfile (check /etc/cpanelsync.exclude)\n";
            return 1;
        }

        ## Maintain support for old broken behavior --------------
        $absfile = $root . '/' . $rfile;
        $absfile =~ tr{/}{}s;
        $absfile =~ s{ [/] \z }{}xmsg;
        if ( grep { $_ eq $absfile } @excludes ) {
            print "Skipping sync of $absfile (check /etc/cpanelsync.exclude)\n";
            return 1;
        }
        ## -------------------------------------------------------
    }
    return;
}

sub in_chmod_excludes {
    my ( $ar_chmod_excludes, $root, $rfile ) = @_;
    if ( scalar @$ar_chmod_excludes ) {
        my $clean_rfile = $rfile;
        $clean_rfile =~ s/^\.\///;
        my $absfile = $root . '/' . $clean_rfile;
        $absfile =~ tr{/}{}s;
        $absfile =~ s{ [/] \z }{}xmsg;
        return 1 if ( grep { $_ eq $absfile } @$ar_chmod_excludes );
    }
    return;
}

sub handle_symlink {
    my ( $target, $rextra ) = @_;

    my $dolink = 0;
    if ( !$target->{'exists'} ) {
        $dolink = 1;
    }
    elsif ( $target->{'islnk'} ) {
        if ( readlink $target->{'path'} ne $rextra ) {
            unlink $target->{'path'};
            $dolink = 1;
        }
    }
    elsif ( $target->{'isnormfile'} ) {
        unlink $target->{'path'};
        $dolink = 1;
    }
    elsif ( $target->{'isdir'} ) {
        system 'rm', '-rf', '--', $target->{'path'};
        $dolink = 1;
    }

    if ($dolink) {
        if ( symlink( $rextra, $target->{'path'} ) ) {
            print "Created symlink $target->{'path'} -> $rextra successfully\n";
        }
        else {
            print "Failed to create symlink $target->{'path'} -> $rextra: $!\n";
        }
    }

    return;
}

sub handle_dir {
    my ( $target, $ar_chmod_excludes, $root, $rfile, $rperm ) = @_;
    ## note: $rperm is an octal string (e.g. '0751')

    if ( $target->{'islnk'} || $target->{'isnormfile'} ) {
        unlink $target->{'path'};
        $target->{'exists'} = 0;
    }

    if ( !$target->{'exists'} ) {
        ## FIX: used to be created with a hardcoded mode of '0755'. The only case this
        ##   will not account for is a new directory that is also in chmod_excludes. I believe
        ##   this to be a very edge case.
        if ( Cpanel::SafeDir::MK::safemkdir( $target->{'path'}, $rperm, 2 ) ) {
            print "Created directory $target->{'path'} successfully\n";
        }
    }
    elsif ( !in_chmod_excludes( $ar_chmod_excludes, $root, $rfile )
        && sprintf( "%04o", ( $target->{'perm'} & 07777 ) ) ne $rperm ) {
        if ( chmod( oct($rperm), $target->{'path'} ) ) {
            print "Directory $target->{'path'} verified\n";
        }
        else {
            print "Failed to update permissions on directory $target->{'path'}: $!";
        }
    }
}

sub handle_file {    ###
    my ( $target, $hr_MD5LIST, $root, $rfile, $hasmd5, $rextra, $skipbz2, $httpClient, $host, $url, $hasbzip2, $repomode, $exit_code, $repo, $ar_chmod_excludes, $rperm, $newfiles_ref ) = @_;

    if ( $target->{'isdir'} ) {
        system 'rm', '-rf', '--', $target->{'path'};
    }
    elsif ( $target->{'islnk'} ) {
        unlink $target->{'path'};
    }

    if (
        ( $target->{'isdir'} || $target->{'islnk'} || !$target->{'exists'} )
        || getmd5sum(
            $hr_MD5LIST, $root, $rfile, $hasmd5,
            'mtime'          => $target->{'mtime'},
            'size'           => $target->{'size'},
            'is_normal_file' => $target->{'isnormfile'}
        ) ne $rextra
      ) {
        my $dfile = $rfile;
        $dfile =~ s/^\.//g;

        my $trycount = 0;
        my $goodfile = 1;
        my $usebz2   = 1;

        my $pathtemp = $target->{'path'} . '-cpanelsync';
        while (1) {    ###
            $trycount++;
            if   ( $trycount % 2 == 0 ) { $usebz2 = 0; }
            else                        { $usebz2 = 1; }

            unlink($pathtemp);
            ### "http://${host}${url}${dfile}.bz2"  -or-
            ### "http://${host}${url}${dfile}"
            if ( !$skipbz2 && $usebz2 && $dfile !~ m/\.bz2$/ ) {
                downloadfile( $httpClient, "http://${host}${url}${dfile}.bz2", "$pathtemp.bz2" );
                my $size = ( stat("$pathtemp.bz2") )[7];
                if ( $size && $size > 0 ) {
                    unbzip2( $hasbzip2, "$pathtemp.bz2" );
                }
                if ( -e "$pathtemp.bz2" ) {
                    ## TODO: meaning what exactly? test with dashk
                    print "$pathtemp.bz2 still exists\n";
                    unlink "$pathtemp.bz2";
                    $skipbz2 = 1;
                    next;
                }
            }
            else {
                downloadfile( $httpClient, "http://${host}${url}${dfile}", $pathtemp );
            }

            my $check_md5sum = getmd5sum(
                $hr_MD5LIST, $root, $rfile . '-cpanelsync',
                $hasmd5, 'skipcache' => 1
            );
            last if ( $check_md5sum eq $rextra );
            my $size = ( stat( $rfile . '-cpanelsync' ) )[7] || 0;
            print "md5sum mismatch (actual: $check_md5sum) (expected: $rextra) (size: $size)\n";

            if ( $trycount > 1 ) {
                if ( $trycount % 3 == 0 ) { $httpClient->skiphost(); }
                if ( $trycount == 10 ) {
                    print "Tried to download the file $rfile 10 times and failed!\n";
                    if ($repomode) {
                        ## TODO: docuement exit codes
                        exit 16;
                    }
                    $goodfile = 0;
                    last;
                }

                ### "$url/.cpanelsync.version",
                if ($repomode) {
                    ## TODO: this does not read well.
                    $exit_code = check_repo_version( $repo, 0, $httpClient, $host, $url );
                    if ( $exit_code != 0 ) {
                        exit $exit_code;
                    }
                }

                downloadfailed($httpClient);
            }
        }
        if ($goodfile) {
            print "Got file $rfile ok (md5 matches)\n";
            _goodfile_handle_chmod(
                $ar_chmod_excludes, $root, $rfile, $target->{'perm'},
                $pathtemp,          $rperm
            );
            _goodfile_handle_rename( $target->{'path'} );
            $newfiles_ref->{ $target->{'path'} } = 1;
        }
        else {
            unlink $pathtemp;
        }
    }
    else {
        if (   !in_chmod_excludes( $ar_chmod_excludes, $root, $rfile )
            && $target->{'exists'}
            && ( sprintf( "%04o", ( $target->{'perm'} & 07777 ) ) ne $rperm ) ) {
            chmod( oct($rperm), $target->{'path'} );
        }
    }
}

sub _goodfile_handle_chmod {
    my ( $ar_chmod_excludes, $root, $rfile, $origperm, $pathtemp, $rperm ) = @_;
    ## if file matches the chmod exclude list, chmod the new temp file with
    ##   the mode from the old file
    if ( in_chmod_excludes( $ar_chmod_excludes, $root, $rfile ) ) {
        my $real_origperm = sprintf( "%04o", ( $origperm & 07777 ) );
        chmod( oct($real_origperm), $pathtemp );
    }
    else {
        chmod( oct($rperm), $pathtemp );
    }
}

sub _goodfile_handle_rename {
    my ($path) = @_;
    unlink $path;
    if ( -e $path ) {
        if ( rename( $path, $path . '.unlink' ) ) {
            unlink $path . '.unlink';
        }
        else {
            unlink $path;
        }
    }
    ## the "rename || unlink" clause ideally should warn the user that the file did not make it to
    ##   its production location. but this, as this runs as root, is hard-to-replicate.
    rename( $path . '-cpanelsync', $path ) || unlink( $path . '-cpanelsync' );
    return;
}

sub prune_OLDFILES {
    my ( $hr_OLDFILES, $rfile, $rtype ) = @_;
    if ( exists $hr_OLDFILES->{$rfile} ) {

        # Handle transition from directory to a symlink
        if ( $rtype eq 'l' && $hr_OLDFILES->{$rfile} ne 'l' ) {
            foreach my $old_file ( keys %$hr_OLDFILES ) {
                ## delete from hash all subdirs of $rfile, which is becoming a link
                if ( $old_file =~ m/^\Q$rfile\E\// ) {
                    delete $hr_OLDFILES->{$old_file};
                }
            }
        }
        delete $hr_OLDFILES->{$rfile};
    }
    return;
}

sub handle_deletes {
    my ( $ar_FILELIST, $hr_OLDFILES, $saferoot ) = @_;
    my @olddirectories;
    my %EXCLUDE_DELETE;

    if ( -e $saferoot . '/.cpanelsync.delete.exclude' && open( my $exc_fh, '<', $saferoot . '/.cpanelsync.delete.exclude' ) ) {
        %EXCLUDE_DELETE = map { chomp($_); $_ => undef } (<$exc_fh>);
        close($exc_fh);
    }

    ## note: loop on @FILELIST to prevent mass deletion on an inadvertantly empty .cpanelsync
    if ( scalar @$ar_FILELIST ) {
        ## adding 'sort' to guarantee an order for keys()
        foreach my $oldfile ( sort keys %$hr_OLDFILES ) {
            $oldfile =~ s/^\.\///g;

            my @BASEDIR = split( /\//, $saferoot . '/' . $oldfile );
            pop(@BASEDIR);
            my $basedir = join( '/', @BASEDIR );

            if ( -l $basedir ) {
                print "Skipping cleanse of $saferoot/$oldfile (within symlinked directory)\n";
                next;
            }

            if ( exists $EXCLUDE_DELETE{ $saferoot . '/' . $oldfile } ) {
                print "Excluding file removal from previous tree: $saferoot/$oldfile\n";
                next;
            }

            if ( -l $saferoot . '/' . $oldfile ) {
                ## ???: what sets the .keep files?
                next if -e $saferoot . '/' . $oldfile . '.keep';
                print "Removing symlink from previous tree: $saferoot/$oldfile\n";
                unlink $saferoot . '/' . $oldfile or print "Unable to remove deprecated symlink $saferoot/$oldfile: $!\n";
            }
            elsif ( -d $saferoot . '/' . $oldfile ) {
                push @olddirectories, $saferoot . '/' . $oldfile;
            }
            elsif ( -e _ ) {
                ## ???: what sets the .keep files?
                next if -e $saferoot . '/' . $oldfile . '.keep';
                print "Removing file from previous tree: $saferoot/$oldfile\n";
                unlink $saferoot . '/' . $oldfile or print "Unable to remove deprecated file $saferoot/$oldfile: $!\n";
            }
        }
    }
    foreach my $dir ( reverse sort @olddirectories ) {
        print "Removing directory from previous tree: $dir\n";
        rmdir $dir or print "Unable to remove deprecated directory $dir: $!\n";
    }

    return;
}

sub lstat_target {
    my ( $root, $rfile ) = @_;
    my %target;

    $target{'path'} = $root . '/' . $rfile;
    $target{'path'} =~ s/^\.\///;
    $target{'path'} =~ s/\/(?:\.\/)+/\//g;
    $target{'path'} =~ s/\/{2,}/\//g;

    my @_lstat = lstat( $target{'path'} );
    ## two slices to assign @_lstat indexes into %target
    @target{ 'perm', 'size', 'mtime' } = @_lstat[ 2, 7, 9 ];

    # if it was a symlink lstat didn't fall back to stat and we got the info on the
    # symlink instead of the info we wanted.  We call lstat as we know it will give the
    # information for the file if its not a symlink and -l will still work.  This saves us
    # from having to stat the file twice if its not a symlink which will be most of the time.

    $target{'isdir'}      = -d _;
    $target{'exists'}     = -e _;
    $target{'isnormfile'} = -f _;

    $target{'islnk'} = 0;
    ## note: I can not find a situation where "-l _" is true, where isdir and isnormfile are not
    ##   already false (namely '', as returned by stat)
    if ( -l _ ) {
        $target{'islnk'}      = 1;
        $target{'isdir'}      = 0;
        $target{'isnormfile'} = 0;
    }
    return \%target;
}

1;