################################################################################    
##### Create/delete Windows Volume Snapshot Service (VSS) snapshots and adjust $Conf{ShareName2Path} accordingly
##### Version 0.5 (June 2020)
##### Jeff Kosowsky    
################################################################################    
####VARIABLES
    #Login name for windows host (use RsyncdUserName or if empty, defaults to BackupPCUser)
    $User = $Conf{RsyncdUserName} or $Conf{BackupPCUser}; #Use RsyncUserName as an alternative login user for Windows if set

    #Option to specify alternative ssh identity file for access to windows host
    $BackupPCsshID = `echo -n ~$Conf{BackupPCUser}` . "/.ssh/id_rsa";
    $BackupPCsshID = '-i ' . $BackupPCsshID  if defined $BackupPCsshID;;

    #Cygwin base:
    my $cygdrive = "/cygdrive";
    $cygdrive =~ s|/*$||; #Ensure that doesn't in slash

    #Location for storing shadow copies reparse-points
    my $shadowdir = "${cygdrive}/c/shadow"; #LOCATION for shadow copies
    $shadowdir =~ s|/*$|/|; #Ensure that ends in slash

    #Format of timestamp (allows for mulitple simultaneous backups of same host from different BackupPC servers)
    chomp(my $timestamp = `date +"%Y%m%d-%H%M%S"`);

    #Note a shadow copy will be created (and pointed to) for every 'letter' drive referenced in the values of the ClientShareName2Path hash
    #If you wish to skip some drive letters, add them to the following shadow exclude array;
    my @shadowexcludes = ();
    my %excludeshash = map { $_ => 1 } @shadowexcludes;
    
    #Pack shadow drive letters in string separated by spaces since can only pass in scalar context to $Conf{Dump...}
    my @pathvalues = values %{$Conf{ClientShareName2Path}}; #Get share paths
    s#^($cygdrive)?/([a-zA-Z])(/.*)?$|.*#$2# for @pathvalues; #Extract drive letters (note only match single drive letter [a-zA-Z])
    #Remove empties, excluded shadows, duplicates & sort (this is nice perl foo)
    @pathvalues = sort(grep($_ ne '' && ! $excludeshash{$_} && !$hash{$_}++, @pathvalues ));
    my $shadows = join(' ', @pathvalues); #Concatenate into space separated list for passing & use by bash scripts
   
#### Bash scripts to run on remote Windows client for Dump Pre/Post User Commands:
    #Load bash scripts into corresponding variables
    #NOTE: Put semicolon at the end of each line *EXCEPT* for final line
    #NOTE: Upper case variables are for bash evaluation on remote machine, 
    #      Lowercase are for perl evaluation locally on the BackupPC server
    #      Distinction is used later to determine what variables to escape (i.e., don't escape lowercase variables)

  #CREATE shadows based on space-separated list of drive letters, $shadows
    my $bashshadowcreate = <<'EOF';
[ -n "$shadows" ] && mkdir -p $shadowdir;
for I in $shadows; do
  ! [ -d "$(cygpath -u ${I}:)" ] && 
      echo "No such drive '${I}:' skipping corresponding shadow setup..." && continue;
    #Create shadow copy and capture shadow id
  { SHADOWID="$(wmic shadowcopy call create Volume=${I}:\\ | sed -ne 's|[ \t]*ShadowID = "\([^"]*\).*|\1|p')" ; } 2> >(tail +2);
       #Note: redirection removes extra new line from stderr
    #Get shadow GLOBALROOT path from shadow id
  SHADOWPATH="$(wmic shadowcopy | awk -v id=$SHADOWID 'id == $8 {print $3}')";
    #Create reparse-point link in shadowdir (since GLOBALROOT paths not readable by cygwin or rsync)
  SHADOWLINK="$(cygpath -w ${shadowdir})$I-$timestamp";
  cmd /c "mklink /j $SHADOWLINK $SHADOWPATH\\";
    #Unset in case error before fully set again
  unset SHADOWID SHADOWPATH SHADOWLINK;
done
EOF

  #UNWIND/DELETE successful shadows based on entries in $shadowdir
    my $bashunwindshadow = <<'EOF';
for II in $(ls -d ${shadowdir}*-${timestamp} 2>/dev/null); do
   SHADOWLINK="$(cygpath -w $II)";
     #Extract the drive letter
   DRIVE=${SHADOWLINK##*\\}; DRIVE=${DRIVE%%-*}; DRIVE="${DRIVE^^}:";
     #Fsutil used to get the target reparse point which is the GLOBALROOT path of the shadow copy
     #NOTE: '\r' is used to remove trailing '^M' in output of fsutil
   SHADOWPATH=$(fsutil reparsepoint query $SHADOWLINK | sed -ne "s|^Print Name:[[:space:]]*\(.*\)\\\\\r|\1|p");
     #Get the shadow id based on the shadowpath
   SHADOWID="$(wmic shadowcopy | awk -v path=${SHADOWPATH//\\/\\\\} 'path == $3 {print $8}')";
   echo "   Deleting shadow for '$DRIVE' PATH=$SHADOWPATH; ID=$SHADOWID; LINK=$SHADOWLINK";
     #Delete the shadow copy
   (vssadmin delete shadows /shadow=$SHADOWID /quiet || 
        echo "   ERROR: Couldn't delete shadow copy for '$DRIVE': $SHADOWLINK") | tail +4;
     #Delete the reparse point (note cygwin rmdir won't work)
   cmd /c rmdir $SHADOWLINK || 
        echo "   ERROR: Couldn't delete link for '$DRIVE': $SHADOWLINK";
done
EOF

  #TRAP script to run to unwind shadows & signal error if error creating shadows
    my $bashtrap = <<'EOF';
function errortrap  { #NOTE: Trap on error: unwind shadows and exit 1.
  echo "ERROR setting up shadows...";
    #First delete any partially created shadows
  if [ -n "$SHADOWID" ]; then
      unset ERROR;
      (vssadmin delete shadows /shadow=$SHADOWID /quiet || ERROR="ERROR ") | tail +4;			     
      echo "   ${ERROR}Deleting shadow copy for '${I^^}:' $SHADOWID";
  fi
  if [ -n "$SHADOWLINK" ]; then
      unset ERROR;
      cmd /c rmdir $SHADOWLINK || ERROR="ERROR ";
      echo "   ${ERROR}Deleting shadow link for '${I^^}:' $SHADOWLINK";
  fi
    #Then loop through to delete other previously created shadows
EOF
	
    $bashtrap .= $bashunwindshadow; #Add unwind script to delete any fully created shadows
    $bashtrap .= <<'EOF';
  exit 1; #Note perl code returns the exit code times 256 in $?
};
trap errortrap ERR;
EOF

    sub escape_bash { #Escape the bash scripts
	my $string = shift;
	$string =~  s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g;
	$string =~ s/\\\$(\\(\{))?([a-z][a-z0-9_]*)(\\(\}))?/\$$2$3$5/g; #Note we want to evaluate and pass lower case variables - so unescape them;
	return $string;
    };
    
    $bashprescript = &escape_bash($bashtrap . $bashshadowcreate);
    $bashpostscript = &escape_bash($bashunwindshadow);

    my $sshrunbashscript = qq(  #Run script \$bashscript on remote host via ssh
       open(my \$out_fh, "|-", "$Conf{SshPath} -q -x $BackupPCsshID -l $User \$args[0]->{hostIP} bash -s") or warn "Can't start ssh: \$!";
       print \$out_fh \"\$bashscript\";
       close \$out_fh; # or warn "Error flushing/closing pipe to ssh: \$!";
	);

#### DumpPreUserCmd
    $Conf{DumpPreUserCmd} = qq(&{sub {
       my \$timestamp = "$timestamp";
       my \$shadowdir = "$shadowdir";
       my \$shadows = "$shadows";

       my \$bashscript = "$bashprescript";
       $sshrunbashscript;

       my \$sharenameref=\$bpc->{Conf}{ClientShareName2Path};
       foreach my \$key (keys %{\$sharenameref}) { #Rewrite ClientShareName2Path
	  \$sharenameref->{\$key} = "\$shadowdir\$2-\$timestamp\$3" if 
                \$sharenameref->{\$key} =~ m#^($cygdrive)?/([a-zA-Z])(/.*)?\$#; #Add shadow if letter drive
       }
       print map { "   '\$_' => \$sharenameref->{\$_}\n" } sort(keys %{\$sharenameref}) unless \$?;
}});

##### DumpPostUserCmd
    $Conf{DumpPostUserCmd} = qq(&{sub {
       my \$timestamp = "$timestamp"; #Initialize variables
       my \$shadowdir = "$shadowdir";
       my \$shadows = "$shadows";

       my \$bashscript = "$bashpostscript";
       $sshrunbashscript;
}});

###############################################################################
#NOTE: the following bash 1-liner can be used to manually delete shadows and shadow junctions (links)
# (date=20200624; for I in $(\ls -d /c/shadow/*-${date}*); do echo $I; SHADOWLINK="$(cygpath -w $I)"; SHADOWPATH=$(fsutil reparsepoint query $SHADOWLINK | sed -ne "s|^Print Name:[[:space:]]*\(.*\)\\\\\r|\1|p"); SHADOWID="$(wmic shadowcopy | awk -v path=${SHADOWPATH//\\/\\\\} 'path == $3 {print $8}')"; vssadmin delete shadows /shadow=$SHADOWID /quiet; cmd /c rmdir $SHADOWLINK; done)

#NOTE: the following bash 1-liner can be used to delete shadows only
#(date=20200624; for id in $(wmic shadowcopy | awk -v date=$date '$10 ~ date {print $8}'); do echo $id; vssadmin delete shadows /shadow=$id /quiet; done)

#NOTE: the following bash 1-liner can be used to delete shadow junction (links) only
#(date=20200624; for I in $(\ls -d /c/shadow/*-${date}*); do echo $I; SHADOWLINK="$(cygpath -w $I)"; cmd /c rmdir $SHADOWLINK; done)
###############################################################################
