#!/usr/bin/perl
#============================================================= -*-perl-*-
#
# BackupPC_backupDelete: delete a V4 backup
#
# DESCRIPTION
#
#   BackupPC_backupDelete: delete a V4 backup
#
#   Usage:
#       BackupPC_backupDelete -h host -n num [-p] [-l] [-r] [-s shareName [dirs...]]
#
# AUTHOR
#   Craig Barratt  <cbarratt@users.sourceforge.net>
#
# COPYRIGHT
#   Copyright (C) 2001-2020  Craig Barratt
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#========================================================================
#
# Version 4.4.0, released 20 Jun 2020.
#
# See http://backuppc.sourceforge.net.
#
#========================================================================

use strict;
no utf8;

use lib "/usr/share/backuppc/lib";
use Getopt::Std;
use File::Copy;
use File::Path;
use Data::Dumper;

use BackupPC::Lib;
use BackupPC::XS qw( :all );
use BackupPC::DirOps qw( :BPC_DT_ALL );

my $Errors = 0;

die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );

my $TopDir = $bpc->TopDir();
my $BinDir = $bpc->BinDir();
my %Conf   = $bpc->Conf();
my $Hosts  = $bpc->HostInfoRead();

my $FileCnt     = 0;
my $FileCntNext = 100;
my $DirCnt      = 0;
my $DirCntNext  = 20;
my $doRefCountUpdate;

my %opts;

if (   !getopts("flLpmrh:n:s:", \%opts)
    || !defined($opts{h})
    || !defined($opts{n})
    || (!defined($opts{s}) && @ARGV >= 1) ) {
    print STDERR <<EOF;
usage: BackupPC_backupDelete -h host -n num [-f] [-l] [-L] [-m] [-p] [-r] [-s shareName [dirs...]]
  Options:
     -h host         host name
     -n num          backup number to delete
     -s shareName    don't delete the backup; delete just this share
                     (or only dirs below this share if specified)
     -f              force delete even if keep is set for this backup
     -L              log output to host's LOG file (instead of stdout)
     -l              don't remove XferLOG files
     -m              run even if a backup on this host is running
                     (specifically, don't take the server host mutex)
     -p              don't print progress information
     -r              do a ref count update (default: none)
  If a shareName is specified, just that share (or share/dirs) are deleted.
  The backup itself is not deleted, nor is the log file removed.
EOF
    exit(1);
}

if (   $opts{h} !~ /^([\w\.\s-]+)$/
    || $opts{h} =~ m{(^|/)\.\.(/|$)}
    || !defined($Hosts->{$opts{h}}) ) {
    print(STDERR "BackupPC_backupDelete: bad host name '$opts{h}'\n");
    exit(1);
}
my $Host = $opts{h};
if ( defined(my $error = $bpc->ConfigRead($Host)) ) {
    print(STDERR "BackupPC_backupDelete: Can't read $Host's config file: $error\n");
    exit(1);
}
%Conf = $bpc->Conf();

if (   !$opts{m}
    && !defined($bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}))
    && (my $status = $bpc->ServerMesg("hostMutex $Host -1 BackupPC_backupDelete")) =~ /fail/ ) {
    print(STDERR "$0: $status (use -m option to force running)\n");
    exit(1);
}

BackupPC::XS::Lib::logLevelSet($Conf{XferLogLevel});
my $LogLevel = $Conf{XferLogLevel};

#
# Write new-style attrib files (<= 4.0.0beta3 uses old-style), which are 0-length
# files with the digest encoded in the file name (eg: attrib_md5HexDigest). We
# can still read the old-style files, and we upgrade them as we go.
#
BackupPC::XS::Attrib::backwardCompat(0, 0);

if ( $opts{n} !~ /^(-?\d+)$/ ) {
    print(STDERR "BackupPC_backupDelete: bad backup number '$opts{n}'\n");
    exit(1);
}
$bpc->ChildInit();

my($idx, $idxMerge);

my @Backups = $bpc->BackupInfoRead($Host);

for ( my $i = 0 ; $i < @Backups ; $i++ ) {
    $idx      = $i if ( $opts{n} == $Backups[$i]{num} );
    $idxMerge = $i
      if ( $opts{n} > $Backups[$i]{num}
        && (!defined($idxMerge) || $Backups[$i]{num} > $Backups[$idxMerge]{num}) );
}
if ( !defined($idx) ) {
    print(STDERR "BackupPC_backupDelete: can't find backup number $opts{n} on host $Host\n");
    exit(1);
}
if ( $Backups[$idx]{keep} && !$opts{f} ) {
    print(STDERR
          "BackupPC_backupDelete: host $Host backup #$Backups[$idx]{num} has keep set; not deleting (use -f to override)\n"
    );
    exit(1);
}

my($LogFd);
if ( $opts{L} ) {
    my($delPid, $delFd);
    #
    # Fork into a parent which reads stdout and puts the output into
    # the client's LOG file, while the child runs BackupPC_backupDelete.
    #
    if ( !defined($delPid = open($delFd, "-|")) ) {
        print(STDERR "BackupPC_backupDelete: can't fork to -L logging\n");
        exit(1);
    }
    binmode($delFd);
    if ( !$delPid ) {
        #
        # This is the child: clone STDERR to STDOUT then continue
        #
        setpgrp 0, 0;
        close(STDERR);
        open(STDERR, ">&STDOUT");
    } else {
        #
        # This is the parent; open the $Host LOG file and write $delFd
        # there
        #
        my($LogPath, $pids);
        ($LogFd, $LogPath) = $bpc->openPCLogFile($Host);
        if ( !defined($LogFd) ) {
            print(STDERR "BackupPC_backupDelete: unable to open/create $LogPath; exiting\n");
            exit(1);
        }
        $pids->{$$}      = 1;
        $pids->{$delPid} = 1;
        pidHandler(keys(%$pids));
        while ( <$delFd> ) {
            if ( $_ =~ /^__bpc_progress_/ ) {
                print($_);
            } elsif ( $_ =~ /^__bpc_pidStart__ (\d+)/ ) {
                $pids->{$1} = 1;
                pidHandler(keys(%$pids));
            } elsif ( $_ =~ /^__bpc_pidEnd__ (\d+)/ ) {
                delete($pids->{$1});
                pidHandler(keys(%$pids));
            } else {
                logMsg($_);
            }
        }
        close($delFd);
        exit(0);
    }
} else {
    print("__bpc_pidStart__ $$\n") if ( !$opts{p} );
}

my($MergeCompress, $MergeDir, $MergeFilled);
my($AttrDel, $DeltaDel, $AttrMerge, $DeltaMerge);
my $HostDir     = "$TopDir/pc/$Host";
my $DelCompress = $Backups[$idx]{compress};
my $DelTopDir   = "$HostDir/$Backups[$idx]{num}";
my $DelPaths    = @ARGV ? [@ARGV] : [undef];
my $ShareName   = $opts{s};
my $ShareNameM  = $bpc->fileNameEltMangle($ShareName);

if ( $Backups[$idx]{version} < 4 ) {
    if ( defined($ShareName) ) {
        foreach my $delPath ( @$DelPaths ) {
            logMsg("BackupPC_backupDelete: removing pre-v4 path #$Backups[$idx]{num}/$ShareName/$delPath\n")
              if ( $LogLevel >= 1 );
            if ( !$opts{p} ) {
                print("__bpc_progress_state__ delete #$Backups[$idx]{num}/$ShareName/$delPath\n");
            }
            #
            # no reference counting for pre V4 - just remove the tree(s)
            #
            my $delPathM = $bpc->fileNameMangle($delPath);
            BackupPC::DirOps::RmTreeQuiet($bpc, "$DelTopDir/$ShareNameM/$delPathM", -2, undef, undef, \&progressUpdate);
        }
    } else {
        logMsg("BackupPC_backupDelete: removing pre-v4 backup #$Backups[$idx]{num}\n") if ( $LogLevel >= 1 );
        if ( !$opts{p} ) {
            print("__bpc_progress_state__ delete #$Backups[$idx]{num}\n");
        }
        #
        # no reference counting for pre V4 - just remove the tree(s)
        #
        BackupPC::DirOps::RmTreeQuiet($bpc, $DelTopDir, -2, undef, undef, \&progressUpdate);
    }
} else {
    if ( defined($ShareName) ) {
        #
        # Since we are only deleting portions of the backup tree, we
        # need to update reference counts
        #
        $DeltaDel = BackupPC::XS::DeltaRefCnt::new($DelTopDir);
        $AttrDel  = BackupPC::XS::AttribCache::new($Host, $Backups[$idx]{num}, $ShareName, $Backups[$idx]{compress});
        $AttrDel->setDeltaInfo($DeltaDel);
        #
        # Create a needFsck file, so if we are killed and can't recover, we can
        # make sure an fsck is run next time.
        #
        mkdir("$DelTopDir/refCnt", 0770) if ( !-d "$DelTopDir/refCnt" );
        my $needFsckFH;
        if ( !(open($needFsckFH, ">", "$DelTopDir/refCnt/needFsck.del") && close($needFsckFH)) ) {
            logMsg("BackupPC_backupDelete: can't create $DelTopDir/refCnt/needFsck.del ($?)\n");
        }
    } else {
        $AttrDel = BackupPC::XS::AttribCache::new($Host, $Backups[$idx]{num}, "", $Backups[$idx]{compress});
    }
    foreach my $delPath ( @$DelPaths ) {
        my($delPathM, $fullDelPath);
        if ( defined($delPath) ) {
            $fullDelPath = $delPath;
            $fullDelPath = "/$fullDelPath" if ( $fullDelPath !~ m{^/} );
            $delPathM    = $bpc->fileNameMangle($delPath);
            $fullDelPath = "/$ShareName$fullDelPath";
            $delPathM    = "/$ShareNameM$delPathM";
            $fullDelPath =~ s{//+}{/};
            $delPathM    =~ s{//+}{/};
        }
        logMsg("BackupPC_backupDelete: removing #$Backups[$idx]{num}$fullDelPath\n") if ( $LogLevel >= 1 );
        if ( !defined($idxMerge) || $Backups[$idxMerge]{version} < 4 || !$Backups[$idxMerge]{noFill} ) {
            #
            # Either this is oldest backup or prior is non-V4 or prior
            # is filled.  There is no need to merge into the prior backup,
            # so just delete the tree.
            #
            if ( !$opts{p} ) {
                print("__bpc_progress_state__ delete #$Backups[$idx]{num}$fullDelPath\n");
            }
            logMsg("BackupPC_backupDelete: No prior backup for merge\n") if ( $LogLevel >= 1 );
            BackupPC::DirOps::RmTreeQuiet($bpc, "$DelTopDir$delPathM", $DelCompress, $DeltaDel, $AttrDel,
                \&progressUpdate);
            if ( defined($delPath) ) {
                my $ret = $AttrDel->delete($delPath);
                logMsg("AttrDel($delPath) returns $ret\n") if ( $LogLevel >= 5 );
                $AttrDel->flush(1);
                $bpc->flushXSLibMesgs();
            }
        } else {
            if ( !$opts{p} ) {
                print(
                    "__bpc_progress_state__ merge #$Backups[$idx]{num}$fullDelPath -> #$Backups[$idxMerge]{num}$fullDelPath\n"
                );
            }
            logMsg("BackupPC_backupDelete: Merge into backup $Backups[$idxMerge]{num}$fullDelPath\n")
              if ( $LogLevel >= 1 );
            $Backups[$idxMerge]{noFill} = $Backups[$idx]{noFill};

            if (   ($Backups[$idx]{compress} == 0 && $Backups[$idxMerge]{compress} != 0)
                || ($Backups[$idx]{compress} != 0 && $Backups[$idxMerge]{compress} == 0) ) {
                printf(STDERR "BackupPC_backupDelete: backups #%d and #%d have different compression - cannot merge\n",
                    $Backups[$idx]{num},
                    $Backups[$idxMerge]{num}
                );
                print("__bpc_pidEnd__ $$\n") if ( !$opts{p} );
                exit(1);
            }
            $MergeCompress = $Backups[$idxMerge]{compress};
            $MergeDir      = "$HostDir/$Backups[$idxMerge]{num}";
            $MergeFilled   = !$Backups[$idxMerge]{noFill};
            #
            # Create a needFsck file, so if we are killed and can't recover, we can
            # make sure an fsck is run next time.
            #
            mkdir("$MergeDir/refCnt", 0770) if ( !-d "$MergeDir/refCnt" );
            my $needFsckFH;
            if ( !(open($needFsckFH, ">", "$MergeDir/refCnt/needFsck.del") && close($needFsckFH)) ) {
                logMsg("BackupPC_backupDelete: can't create $MergeDir/refCnt/needFsck.del ($?)\n");
            }

            if ( !chdir($DelTopDir) ) {
                print(STDERR "BackupPC_backupDelete: cannot chdir to $DelTopDir\n");
                print("__bpc_pidEnd__ $$\n") if ( !$opts{p} );
                exit(1);
            }
            $AttrMerge = BackupPC::XS::AttribCache::new($Host, $Backups[$idxMerge]{num},
                $ShareName, $Backups[$idxMerge]{compress});
            $DeltaMerge = BackupPC::XS::DeltaRefCnt::new($MergeDir);
            $AttrMerge->setDeltaInfo($DeltaMerge);

            my $dir = defined($delPath) ? ".$delPathM" : ".";
            mergeDir(".", $dir);
            BackupPC::DirOps::find($bpc, {wanted => \&mergeDir}, $dir, 1);

            $AttrDel->flush(1);
            $bpc->flushXSLibMesgs();

            #
            # Now delete the Del backup tree
            #
            logMsg("removing remaining directory tree $DelTopDir$delPathM\n") if ( $LogLevel >= 5 );
            BackupPC::DirOps::RmTreeQuiet($bpc, "$DelTopDir$delPathM", $DelCompress, $DeltaDel, $AttrDel,
                \&progressUpdate);
            if ( defined($delPath) ) {
                if ( defined(my $attr = $AttrDel->get($delPath)) && !defined($AttrMerge->get($delPath)) ) {
                    $AttrMerge->set($delPath, $attr);
                }
                my $ret = $AttrDel->delete($delPath);
                logMsg("AttrDel($delPath) returns $ret\n") if ( $LogLevel >= 5 );
            }

            $AttrDel->flush(1);
            $DeltaDel->flush() if ( $DeltaDel );
            $bpc->flushXSLibMesgs();
            $AttrMerge->flush(1);
            $DeltaMerge->flush();
            $bpc->flushXSLibMesgs();
        }
    }
    #
    # Make sure we update the reference counts below since there are (likely)
    # deltas in the merge backup and/or original backup (if $DelPaths)
    #
    $doRefCountUpdate = 1;
}
if ( !defined($ShareName) ) {
    if ( !$opts{l} ) {
        unlink("$HostDir/SmbLOG.$Backups[$idx]{num}")
          if ( -f "$HostDir/SmbLOG.$Backups[$idx]{num}" );
        unlink("$HostDir/SmbLOG.$Backups[$idx]{num}.z")
          if ( -f "$HostDir/SmbLOG.$Backups[$idx]{num}.z" );
        unlink("$HostDir/XferLOG.$Backups[$idx]{num}")
          if ( -f "$HostDir/XferLOG.$Backups[$idx]{num}" );
        unlink("$HostDir/XferLOG.$Backups[$idx]{num}.z")
          if ( -f "$HostDir/XferLOG.$Backups[$idx]{num}.z" );
    }
    splice(@Backups, $idx, 1);
    $bpc->BackupInfoWrite($Host, @Backups);
}

unlink("$MergeDir/refCnt/needFsck.del")
  if ( $Errors == 0 && $Conf{RefCntFsck} == 0 && -f "$MergeDir/refCnt/needFsck.del" );
unlink("$DelTopDir/refCnt/needFsck.del")
  if ( $Errors == 0 && $Conf{RefCntFsck} == 0 && -f "$DelTopDir/refCnt/needFsck.del" );

if ( $doRefCountUpdate || $opts{r} ) {
    my $optsp = " -p" if ( $opts{p} );
    $Errors++ if ( system("$BinDir/BackupPC_refCountUpdate -h $Host -o 0$optsp") != 0 );
}

logMsg("BackupPC_backupDelete: got $Errors errors\n") if ( $Errors > 0 || $LogLevel >= 2 );

print("__bpc_pidEnd__ $$\n") if ( !$opts{p} );
exit($Errors ? 1 : 0);

sub copyInodes
{
    my($name, $path) = @_;

    logMsg("copyInodes: name = $name, path = $path\n") if ( $LogLevel >= 6 );

    return if ( $path !~ m{(.*)/(attrib[^/]*)$} );

    $path = $1;
    $name = $2;

    my $attr = BackupPC::XS::Attrib::new($MergeCompress);
    if ( -f "$path/$name" && !$attr->read($path, $name) ) {
        logMsg("Can't read attribute file $path/$name\n");
        $Errors++;
        return;
    }
    my $digest = $attr->digest();
    if ( length($digest) ) {
        $DeltaMerge->update($MergeCompress, $digest, 1);
        $DeltaDel->update($DelCompress, $digest, -1) if ( $DeltaDel );
    }
    my $idx = 0;
    my $a;
    while ( 1 ) {
        ($a, $idx) = $attr->iterate($idx);
        last if ( !defined($a) );
        my $fileUM = $a->{name};
        $FileCnt++;
        $DeltaMerge->update($a->{compress}, $a->{digest}, 1) if ( length($a->{digest}) );
        $DeltaDel->update($a->{compress}, $a->{digest}, -1)  if ( $DeltaDel && length($a->{digest}) );

        next if ( $a->{nlinks} == 0 );
        #
        # copy/update inode in merge, and reduce link count in delete backup
        #
        my $aInode = $AttrDel->getInode($a->{inode});
        if ( !(my $inodeMerge = $AttrMerge->getInode($a->{inode})) ) {
            #
            # Copy the inode if it doesn't exist in Merge.
            #
            $AttrMerge->setInode($a->{inode}, $aInode);
            $DeltaMerge->update($aInode->{compress}, $aInode->{digest}, 1)
              if ( length($aInode->{digest}) );
        }
        if ( $DeltaDel && $aInode ) {
            $aInode->{nlinks}--;
            if ( $aInode->{nlinks} <= 0 ) {
                $DeltaDel->update($DelCompress, $aInode->{digest}, -1) if ( $DeltaDel && length($aInode->{digest}) );
                $AttrDel->deleteInode($a->{inode});
            } else {
                $AttrDel->setInode($a->{inode}, $aInode);
            }
        }
    }
    printProgress() if ( $FileCnt >= $FileCntNext );
}

sub mergeDir
{
    my($name, $path) = @_;

    if ( !-d $path || $path =~ m{^\./refCnt} || $path =~ m{^\./inode} ) {
        logMsg("mergeDir: skipping name = $name, path = $path\n") if ( $LogLevel >= 5 );
        return;
    }
    $name = "attrib";

    logMsg("mergeDir: name = $name, path = $path\n") if ( $LogLevel >= 5 );

    my $d = $path;
    $d =~ s{^\./+}{};
    my $delDir   = "$DelTopDir/$d";
    my $mergeDir = "$MergeDir/$d";

    logMsg("mergeDir: delDir = $delDir, mergeDir = $mergeDir\n") if ( $LogLevel >= 6 );

    my $attr = BackupPC::XS::Attrib::new($MergeCompress);
    $attr->read($mergeDir, $name) if ( -d $mergeDir );
    my $attrAll = $attr->get();

    my($attrDelAll);
    my $dirty   = 0;
    my $attrDel = BackupPC::XS::Attrib::new($DelCompress);
    if ( -d $delDir ) {
        $attrDel->read($delDir, $name);
        $attrDelAll = $attrDel->get();
        $DeltaDel->update($DelCompress, $attrDel->digest(), -1) if ( $DeltaDel && length($attrDel->digest()) );
    }
    $bpc->flushXSLibMesgs();

    #
    # Add non-attrib directories (ie: directories that were created
    # to store attributes in deeper directories), since these
    # directories may not appear in the attrib file at this level.
    #
    if ( defined(my $entries = BackupPC::DirOps::dirRead($bpc, $delDir)) ) {
        foreach my $e ( @$entries ) {
            next if ( $e->{name} eq "." || $e->{name} eq ".." || $e->{name} eq "inode" );
            if ( $e->{name} =~ /^attrib/ ) {
                if ( $e->{name} =~ /attrib_(.{16,})/ ) {
                    my $digest = pack("H*", $1);
                    $DeltaDel->update($DelCompress, $digest, -1) if ( $DeltaDel && $attrDel->digest() ne $digest );
                }
                logMsg("Removing attrib file $delDir/$e->{name}\n") if ( $LogLevel >= 5 );
                unlink("$delDir/$e->{name}");
            } elsif ( -d "$delDir/$e->{name}" ) {
                my $fileUM = $bpc->fileNameUnmangle($e->{name});
                next if ( $attrDelAll && defined($attrDelAll->{$fileUM}) );
                $attrDelAll->{$fileUM} = {
                    type     => BPC_FTYPE_DIR,
                    noAttrib => 1,
                };
            }
        }
    }
    if ( defined(my $entries = BackupPC::DirOps::dirRead($bpc, $mergeDir)) ) {
        foreach my $e ( @$entries ) {
            next if ( $e->{name} eq "."
                || $e->{name} eq ".."
                || $e->{name} eq "inode"
                || !-d "$mergeDir/$e->{name}" );
            my $fileUM = $bpc->fileNameUnmangle($e->{name});
            next if ( $attrAll && defined($attrAll->{$fileUM}) );
            $attrAll->{$fileUM} = {
                type     => BPC_FTYPE_DIR,
                noAttrib => 1,
            };
        }
    }

    #logMsg "MergeDir $mergeDir contents:\n", Dumper($attrAll);
    #logMsg "DelTopDir $delDir contents:\n", Dumper($attrDelAll);

    foreach my $fileUM ( keys(%$attrDelAll) ) {
        my $a = $attrDelAll->{$fileUM};
        $FileCnt++;
        if ( defined(my $aMerge = $attrAll->{$fileUM}) ) {
            logMsg("Got del file $delDir/$fileUM, type $a->{type}; mergeDir has type $aMerge->{type}\n")
              if ( $LogLevel >= 7 );
            #
            # The file exists in both the previous and
            # deleted backups.
            #
            # If they are both directories, then don't do
            # anything for now
            #
            if ( $a->{type} == BPC_FTYPE_DIR && $aMerge->{type} == BPC_FTYPE_DIR ) {
                #
                # if the deleted directory has real attributes, and the
                # merge doesn't, then copy the attributes to merge
                #
                if ( $aMerge->{noAttrib} && !$a->{noAttrib} ) {
                    logMsg("Copying del attributes to merge for $delDir/$fileUM\n") if ( $LogLevel >= 7 );
                    $attr->set($fileUM, $a);
                    $dirty = 1;
                }
                next;
            }
            #
            # The deleted version will get deleted.
            #
            logMsg("removing $delDir/$fileUM\n") if ( $LogLevel >= 7 );
            if ( $DeltaDel ) {
                $DeltaDel->update($a->{compress}, $a->{digest}, -1);
                if ( $a->{nlinks} > 0 && (my $aInode = $AttrDel->getInode($a->{inode})) ) {
                    $aInode->{nlinks}--;
                    if ( $aInode->{nlinks} <= 0 ) {
                        $DeltaDel->update($DelCompress, $aInode->{digest}, -1) if ( $DeltaDel );
                        $AttrDel->deleteInode($a->{inode});
                    } else {
                        $AttrDel->setInode($a->{inode}, $aInode);
                    }
                }
            }
            next if ( $a->{type} != BPC_FTYPE_DIR );
            #
            # Since this is a directory and the merge file is not,
            # we need to remove the tree.
            #
            my $file = $bpc->fileNameMangle($fileUM);
            logMsg("removing directory tree $delDir/$fileUM\n") if ( $LogLevel >= 5 );
            BackupPC::DirOps::RmTreeQuiet($bpc, "$delDir/$file", $DelCompress, $DeltaDel, $AttrDel, \&progressUpdate);
            next;
        }
        logMsg("Got del file $delDir/$fileUM, type $a->{type}; mergeDir doesn't exist\n") if ( $LogLevel >= 7 );

        #
        # The file doesn't exist in the previous backup, so move
        # the deleted file version to the previous backup.  Update
        # the reference counts for the merge backup.
        #
        if ( !$a->{noAttrib} ) {
            $attr->set($fileUM, $a);
            if ( length($a->{digest}) ) {
                $DeltaMerge->update($a->{compress}, $a->{digest}, 1);
                $DeltaDel->update($a->{compress}, $a->{digest}, -1) if ( $DeltaDel );
            }
            if ( $LogLevel >= 7 ) {
                my $digestStr = unpack("H*", $a->{digest});
                logMsg(
                    "set $fileUM attrib (type = $a->{type}, size = $a->{size}, nlinks = $a->{nlinks}, inode = $a->{inode}, digest = $digestStr)\n"
                );
            }
            if ( $a->{nlinks} > 0 ) {
                my $aInode = $AttrDel->getInode($a->{inode});
                if ( $LogLevel >= 7 ) {
                    my $digestStr = unpack("H*", $aInode->{digest});
                    logMsg(
                        "got del $fileUM inode $a->{inode} (type = $aInode->{type}, size = $aInode->{size}, nlinks = $aInode->{nlinks}, digest = $digestStr)\n"
                    );
                }
                if ( !(my $inodeMerge = $AttrMerge->getInode($a->{inode})) ) {
                    #
                    # Copy the inode if it doesn't exist in Merge.
                    #
                    $AttrMerge->setInode($a->{inode}, $aInode);
                    $DeltaMerge->update($aInode->{compress}, $aInode->{digest}, 1)
                      if ( length($aInode->{digest}) );
                    if ( $LogLevel >= 7 ) {
                        my $digestStr = unpack("H*", $aInode->{digest});
                        logMsg(
                            "setting merge inode $a->{inode} (type = $aInode->{type}, size = $aInode->{size}, nlinks = $aInode->{nlinks}, digest = $digestStr)\n"
                        );
                    }
                }
                if ( $DeltaDel && $aInode ) {
                    $aInode->{nlinks}--;
                    if ( $aInode->{nlinks} <= 0 ) {
                        $DeltaDel->update($DelCompress, $aInode->{digest}, -1) if ( $DeltaDel );
                        $AttrDel->deleteInode($a->{inode});
                        if ( $LogLevel >= 7 ) {
                            my $digestStr = unpack("H*", $aInode->{digest});
                            logMsg(
                                "deleted del inode $a->{inode} (type = $aInode->{type}, size = $aInode->{size}, nlinks = $aInode->{nlinks}, digest = $digestStr)\n"
                            );
                        }
                    } else {
                        $AttrDel->setInode($a->{inode}, $aInode);
                        if ( $LogLevel >= 7 ) {
                            my $digestStr = unpack("H*", $aInode->{digest});
                            logMsg(
                                "updated del inode $a->{inode} (type = $aInode->{type}, size = $aInode->{size}, nlinks = $aInode->{nlinks}, digest = $digestStr)\n"
                            );
                        }
                    }
                }
            }
            $dirty = 1;
        }
        next if ( $a->{type} != BPC_FTYPE_DIR );
        #
        # Since it's a directory, if it exists, move the whole thing over.
        # We need to traverse the moved directory to make sure any referenced
        # inodes get moved over too and to update the reference counts added
        # to the merge backup.
        #
        my $file = $bpc->fileNameEltMangle($fileUM);
        if ( -d "$delDir/$file" ) {
            logMsg("renaming $delDir/$file to $mergeDir/$file\n") if ( $LogLevel >= 7 );
            if ( !rename("$delDir/$file", "$mergeDir/$file") ) {
                #
                # try again if the target directory doesn't exist
                #
                mkpath($mergeDir, 0, 0777) if ( !-d $mergeDir );
                if ( !rename("$delDir/$file", "$mergeDir/$file") ) {
                    logMsg("Can't move $delDir/$file to $mergeDir/$file; removing tree\n");
                    #
                    # need to remove this tree.
                    #
                    BackupPC::DirOps::RmTreeQuiet($bpc, "$delDir/$file", $DelCompress, $DeltaDel, \&progressUpdate);
                    $Errors++;
                    next;
                }
            }
        }
        BackupPC::DirOps::find($bpc, {wanted => \&copyInodes}, "$mergeDir/$file", 1);
    }
    printProgress() if ( $FileCnt >= $FileCntNext );

    $bpc->flushXSLibMesgs();
    if ( $MergeFilled ) {
        #
        # if the merge directory is filled, actually delete any files that have
        # BPC_FTYPE_DELETED types
        #
        my $idx = 0;
        my $a;
        while ( 1 ) {
            ($a, $idx) = $attr->iterate($idx);
            last if ( !defined($a) );
            my $fileUM = $a->{name};
            next if ( $a->{type} != BPC_FTYPE_DELETED );

            logMsg("removing attrib for deleted file $mergeDir/$fileUM\n") if ( $LogLevel >= 7 );
            $attr->delete($fileUM);
            $dirty = 1;
        }
    }
    if ( $dirty ) {
        my $oldDigest = $attr->digest();
        if ( !$attr->write($mergeDir, $name, $oldDigest, $DeltaMerge) ) {
            my $digestStr = unpack("H*", $oldDigest);
            logMsg("mergeDir: attr write to $mergeDir/$name failed (digest was $digestStr)\n");
        }
        logMsg(sprintf(
            "rewriting attrib file %s; digest is %s was %s\n",
            "$mergeDir/$name",
            unpack("H*", $attr->digest()),
            unpack("H*", $oldDigest)
        ))
          if ( $LogLevel >= 5 );
    }
    $bpc->flushXSLibMesgs();
}

sub progressUpdate
{
    my($newCnt) = @_;

    if ( $DeltaDel ) {
        $FileCnt += $newCnt;
        printProgress() if ( $FileCnt >= $FileCntNext );
    } else {
        $DirCnt += $newCnt;
        if ( $DirCnt >= $DirCntNext ) {
            printProgress();
            $DirCntNext += 20;
        }
    }
}

sub printProgress
{
    $FileCntNext = $FileCnt + 100 if ( $FileCnt >= $FileCntNext );
    return if ( $opts{p} );
    if ( $FileCnt > 0 && $DirCnt > 0 ) {
        print("__bpc_progress_fileCnt__ $FileCnt files / $DirCnt dirs\n");
    } elsif ( $DirCnt == 0 ) {
        print("__bpc_progress_fileCnt__ $FileCnt\n");
    } else {
        print("__bpc_progress_fileCnt__ $DirCnt dirs\n");
    }
}

#
# print a log/debug message to stdout (default) or the per-client LOG
# file (-L) with a timestamp
#
sub logMsg
{
    my $str = join("", @_);

    if ( $LogFd ) {
        print($LogFd $bpc->timeStamp, $str);
    } else {
        print($str);
    }
}

#
# print the list of pids
#
sub pidHandler
{
    my @pids = sort { $a <=> $b } @_;
    printf("xferPids %s\n", join(",", @pids));
}
