Tuesday, December 01, 2009

Zip Files with PowerShell Script

Recently I have been working on backup files from a remote server in network to another server PC. I use SyncToy tool to sync files from the remote to the server. Each time when you run the SyncToy, it will generate a SyncToy.log file as in "C:\Documents and Settings\username\Local Settings\Application Data\Microsoft\SyncToy\2.0\SyncToyLog.log". What I need to do is to copy the SyncToy.log file from that location to a specified location and zip to a monthly file as my log, for example, "C:\synclog\synctoy_122009.zip".

This job can be easily done in a .Net project, but I was required to write a script instead of another program. As I know very little about PowerShell, I spent about 2-3 days to find out a solution. Basically, you can access to almost any .Net classes in PS. I have used DotNetZip library before, which provides a very simple and nice library class to zip files. What I need to access to this library, create an instance from its class and call its methods to detect and zip files. In PS, it is very easy to do that.

# ZIP dll library file in the local PC folder:
$ZIP_DLL = "C:\bin\Ionic.Zip\Ionic.Zip.dll"
$assemblyLoaded = [System.Reflection.Assembly]::LoadFrom($ZIP_DLL);
# Zip class
$zipClass = "Ionic.Zip.ZipFile";

Here I use a var to hold LoadFrom(...) is to prevent output of loading results. The var is not needed for reference use. In PS, if you want to prevent some output while calling some methods, this may be a strategy to do it.

To zip files, I created a function to do the job. The function will zip a group of files (source files as a string such as "C:\temp\*.log"), with a constrain of days for last modified date stamp within those days from now, to a destination folder. In addition to that, I pass one flag to the function to provide option to include path in zip or not.

#*============================================
# FUNCTION DEFINITIONS
#*============================================
function ZipUpFiles (
  [string]$p_Source = ${throw "Missing parameter source"},
  [string]$p_DestFolder = ${throw "Missing parameter destination folder"},
  [int]$p_days = ${throw "Missing parameter int days"},
  $p_zipFile,
  [bool]$p_PathInZip,
  $p_zipClass
  )
{
...
}
...
#*============================================
# END OF FUNCTION DEFINITIONS
#*============================================

In PS, actually, you don't need to define input parameters. You can define () empty list, and you can still call it with a list of parameters. Within the function, you can get parameters by $args. However, it is much clear by defining parameters. You can think them as var definitions.

The first thing to do in the function is to get a list of files:

  $checkFileDate = ($p_days -ne 0)
  # adjust timestamp by days for comparing
  $dateToCompare = (Get-date).AddDays(-$p_days)
  $zipCount = 0;
  # get all the files matched and timestamp > comparing date
    $fs = Get-Item -Path $p_source | Where-Object {!$_.PSIsContainer -and (!$checkFileDate -or ($checkFileDate -and $_.lastwritetime -gt $dateToCompare))}
  if ( $fs -ne $null )
  {
    ...

The codes are pretty much straightforward. Here Get-Item command to check path with pipe to check each items to meet requirements: not sub-direction, and file created date great than days if specified. The result is a collection of files to be zipped.

In PS, all the comparison and logical operators are literal with -. For example, -gt for great than, -eq for equal to, and -or. This very handy and easy to understand. It also makes the blog HTML tags much easier, no need to convert "<" to "&lt;".

Next continue to zip files in a for loop. The function takes one parameter as zip file name. If it is specified, all the files will be zipped to that file with {mmyyyy}.zip as suffix. If it is not specified, each file will be zipped with that suffix.
    $zipObj = $null
    if ( $p_zipFile -ne $null )
    {
      $zipFile = "{0}{1}" -f $p_DestFolder, $p_zipFile
      $zipObj = new-object $p_zipClass($zipFile);
    }
    foreach ($file in $fs)
    {
      $addFile = $file.Name
      if ( $p_zipFile -eq $null )
      {
        $zipFile = "{0}{1}.zip" -f $p_DestFolder, $addFile
        $zipObj = new-object $p_zipClass($zipFile);
      }
      # Trim drive name out as key to check if file already in zip?
      if ( ($zipObj.Count -eq 0) -or 
                (!$p_PathInZip -and ($zipObj[$file.Name] -eq $null)) -or
                ($p_PathInZip -and ($zipObj[$file.FullName.Substring(3)] -eq $null))
                )
      {
        Write-Output "Zipping file $addFile to $zipFile..."
        $pathInZip = ""
        if ( $p_PathInZip )
        {
          $pathInZip = $file.Directory
        }
        $e= $zipObj.AddFile($file.FullName, $pathInZip)
        $zipCount += 1
      }
      if ( $p_zipFile -eq $null -and $zipCount -gt 0 )
      {
        $zipObj.Save()
        $zipObj.Dispose()
        $zipObj = $null
        $zipFile = $null
      }
    }
    if ( $zipObj -ne $null -and $zipCount -gt 0 )
    {
      $zipObj.Save()
      $zipObj.Dispose()
      $zipObj = $null
    }

Here $zipObj is created from .Net class. All the methods then are available in PS. You may refer to class definition in Visual Studio or ReFlector to view class structure. Before I add a file to zip, I check if the file is already in the zip file (two cases: path in zip or not). If so, no zip will be done.

In the end of the function, the $zipObj has to be saved and cleared if there is any files added:

...
    }
    if ( $zipObj -ne $null -and $zipCount -gt 0 )
    {
      $zipObj.Save()
      $zipObj.Dispose()
      $zipObj = $null
    }
  }
  if ( $zipcount -eq 0 )
  {
    Write-Output "Nothing to zip"
  }
}


Finally, in my PS script, after the function definition, which has to be declared before it is called, here is my main entrance:

#*============================================
#* SCRIPT BODY
#*============================================
# Example parameters:
# E:\Temp\*.bak E:\Temp\BackupZips\ 50 backup.zip
Write-Debug "Starting ZipFiles.ps1"
# check input arguments
$argsLen = 0 
if ($args -ne $null )
{
  $argsLen = $args.length
}
if ( $argsLen -lt 2 -or $argsLen -gt 5 )
{
  HelpInfo
  return
}
$i = 0;
# Get input parameters
$sourcePath = $args[$i++]
$destPath = $args[$i++]
if ( !$destPath.EndsWith("\") )
{
   $destPath += "\"
}
[int]$numOfDays = 0
$zipFile = $null
[bool]$pathInZip = $true
if ( $argsLen -gt $i )
{
  $r = [int]::TryParse($args[$i++], [ref]$numOfDays)
  if ( $argsLen -gt $i )
  {
    $zipFile = $args[$i++]
    if ( $zipFile -eq $null -or $zipFile.length -eq 0 )
    {
      $zipFile = $null
    }
    if ( $argsLen -gt $i )
    {
      $pathInZip = ($args[$i++] -eq 1)
    }
  }
}

# Test source & destiantion
if ( !(Test-Path $sourcePath) -or !(Test-Path $destPath) )
{
  Write-Output "Nothing to do. Either ""$sourcePath"" or ""$destPath"" is empty or does not exist."
  return
}

# ZIP library is from http://www.codeplex.com/DotNetZip
# ZIP dll library file in the local PC folder:
$ZIP_DLL = "C:\bin\Ionic.Zip\Ionic.Zip.dll"
$assemblyLoaded = [System.Reflection.Assembly]::LoadFrom($ZIP_DLL);
# Zip class
$zipClass = "Ionic.Zip.ZipFile";

Write-Debug "Start zip process ($sourcePath > $destPath)..."
ZipUpFiles $sourcePath $destPath $numOfDays $zipFile $pathInZip $zipClass

$assemblyLoaded = $null

#*============================================
#* END OF SCRIPT BODY
#*============================================

The first section of main body is to parse input parameters. As I mentioned, $args is a PS variable for arguments. If there is less or more required parameters, function HelpInfo is called, which just output the usage of the script and it is omitted. When all the required parameters are parsed, the function ZipUpFiles is called.

0 comments: