#!/usr/bin/perl
#============================================================= -*-perl-*-
#
# BackupPC_backupDelete: delete a V4 backup
#
# DESCRIPTION
#
#   BackupPC_backupDelete: delete a V4 backup
#
#   Usage:
#       BackupPC_backupDelete -h host -n num
#
# AUTHOR
#   Craig Barratt  <cbarratt@users.sourceforge.net>
#
# COPYRIGHT
#   Copyright (C) 2001-2017  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.1.3, released 3 Jun 2017.
#
# 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("lprh:n:s:", \%opts) || !defined($opts{h}) || !defined($opts{n}) || (!defined($opts{s}) && @ARGV >= 1) ) {
    print STDERR <<EOF;
usage: BackupPC_backupDelete -h host -n num [-p] [-l] [-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)
     -p              don't print progress information
     -l              don't remove XferLOG files
     -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();

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);
}

print("__bpc_pidStart__ $$\n") if ( !$opts{p} );

my($MergeCompress, $MergeDir);
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 ) {
            print("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 {
        print("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);
    } 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" if ( $delPath !~ m{^/} );
            $delPathM    = $bpc->fileNameMangle($delPath);
            $fullDelPath = "/$ShareName$fullDelPath";
            $delPathM    = "/$ShareNameM$delPathM";
            $fullDelPath =~ s{//+}{/};
            $delPathM    =~ s{//+}{/};
        }
        print("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");
            }
            print("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);
                print("AttrDel($delPath) returns $ret\n") if ( $LogLevel >= 5 );
                $AttrDel->flush(1);
                $bpc->flushXSLibMesgs();
            }
            if ( $DeltaDel ) {
                print("Deltas for $Backups[$idx]{num}:\n");
                $DeltaDel->print();
                $DeltaDel->flush();
            }
        } else {
            if ( !$opts{p} ) {
                print("__bpc_progress_state__ merge #$Backups[$idx]{num}$fullDelPath -> #$Backups[$idxMerge]{num}$fullDelPath\n");
            }
            print("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}";
            #
            # 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)) ) {
                print("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
            #
            print("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);
                print("AttrDel($delPath) returns $ret\n") if ( $LogLevel >= 5 );
            }

            $AttrDel->flush(1);
            $bpc->flushXSLibMesgs();
            if ( $DeltaDel ) {
                print("Deltas for $Backups[$idx]{num}:\n");
                $DeltaDel->print();
                $DeltaDel->flush();
            }
            $AttrMerge->flush(1);
            $bpc->flushXSLibMesgs();
            print("Deltas for $Backups[$idxMerge]{num}:\n");
            $DeltaMerge->print();
            $DeltaMerge->flush();
        }
    }
    #
    # 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 );

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

print("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) = @_;

    print("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) ) {
        print("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 $attrAll = $attr->get();
    foreach my $fileUM ( keys(%$attrAll) ) {
        my $a = $attrAll->{$fileUM};
        $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 );
        #
        # Update inode link count in del backup, and copy/update inode in merge
        #
        my $aInode = $AttrDel->getInode($a->{inode});
        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);
            }
        }
        if ( !(my $inodeMerge = $AttrMerge->getInode($a->{inode})) ) {
            #
            # Copy the inode if it doesn't exist in Merge.
            #
            $aInode->{nlinks} = 1;
            $AttrMerge->setInode($a->{inode}, $aInode);
            $DeltaMerge->update($aInode->{compress}, $aInode->{digest}, 1)
                                                if ( length($aInode->{digest}) );
        } else {
            #
            # increase link count on exiting inode
            #
            $inodeMerge->{nlinks}++;
            $AttrMerge->setInode($a->{inode}, $inodeMerge);
        }
    }
    printProgress() if ( $FileCnt >= $FileCntNext );
}

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

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

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

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

    print("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 );
                }
                print("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,
            };
        }
    }
    #print "MergeDir $mergeDir contents:\n", Dumper($attrAll);
    #print "DelTopDir $delDir contents:\n", Dumper($attrDelAll);

    foreach my $fileUM ( keys(%$attrDelAll) ) {
        my $a = $attrDelAll->{$fileUM};
        $FileCnt++;
        if ( defined(my $aMerge = $attrAll->{$fileUM}) ) {
            print("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} ) {
                    print("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.
            #
            print("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);
            print("removing directory tree $delDir/$fileUM\n") if ( $LogLevel >= 5 );
            BackupPC::DirOps::RmTreeQuiet($bpc, "$delDir/$file", $DelCompress, $DeltaDel, $AttrDel, \&progressUpdate);
            next;
        }
        print("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});
                print("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});
                    print("got del $fileUM 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});
                            print("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});
                            print("updated del 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.
                    #
                    $aInode->{nlinks} = 1;
                    $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});
                        print("setting merge inode $a->{inode} (type = $aInode->{type}, size = $aInode->{size}, nlinks = $aInode->{nlinks}, digest = $digestStr)\n")
                    }
                } else {
                    #
                    # increase link count on exiting inode
                    #
                    $inodeMerge->{nlinks}++;
                    $AttrMerge->setInode($a->{inode}, $inodeMerge);
                    if ( $LogLevel >= 7 ) {
                        my $digestStr = unpack("H*", $inodeMerge->{digest});
                        print("updating merge inode $a->{inode} (type = $inodeMerge->{type}, size = $inodeMerge->{size}, nlinks = $inodeMerge->{nlinks}, digest = $digestStr)\n")
                    }
                }
            }
            $dirty = 1;
        }
        next if ( $a->{type} != BPC_FTYPE_DIR );
        #
        # Since it's a directory, 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);
        print("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") ) {
                print("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 );

    if ( $dirty ) {
        my $oldDigest = $attr->digest();
        print("rewriting attrib file $mergeDir/$name; ") if ( $LogLevel >= 5 );
        if ( !$attr->write($mergeDir, $name, $oldDigest, $DeltaMerge) ) {
            my $digestStr = unpack("H*", $oldDigest);
            print("mergeDir: attr write to $mergeDir/$name failed (digest was $digestStr)\n");
        }
        printf("digest now %s, was %s\n", 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");
    }
}
