Monthly Archives: September 2011

Common solution to zipping files in PoSh

The code below, or very similar variations, seem to be a very widespread recommendation to adding files to a zip archive in PowerShell:

$fileName = 'C:\Temp\test.zip'
$source = 'C:\Temp\reallylargefile.txt'
Set-Content $fileName ('PK' + [char]5 + [char]6 + ("$([char]0)" * 18));
$oShell = New-Object -ComObject Shell.Application;
$zipPackage = $oShell.NameSpace($fileName);
$zipPackage.CopyHere($source);

There’s nothing particularly wrong with this code — it’s easy, it’s short, and it’s effective. However, a number of scripters out there have noticed that there’s a small problem: The CopyHere method returns immediately, rather than waiting for the zip process to complete. If your script wants to do something with that zip file, it’s got to find a way to know when the zip is free to be moved, renamed, copied, and so on.

Fortunately, there’s a way to do that. Here’s the solution I use:

function Test-FileLock {
 
    param (
		[parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
		[Alias("FullName")]
		[string]$Path,
		[switch]$PassThru
	)
 
	begin {}
 
	Process
	{
	    if ((Test-Path -Path $Path) -eq $false)
	    {
			throw [System.IO.FileNotFoundException] 
	    	return
	    }
 
	    $oFile = Get-Item $Path
 
	    if ($oFile.PSIsContainer)
	    {
    		return
	    }
 
	    try
	    {
		    $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
		    if ($oStream)
		    {
		        $oStream.Close()
		    }
		    if ($PassThru.IsPresent -eq $false)
		    {
		    	$false
		    }
		    else
		    {
		    	Get-Item $Path
		    }
	    }
	    catch
	    {
	    	# file is locked by a process.
		    if ($PassThru.IsPresent -eq $false)
		    {
		    	$true
		    }
	    }
	}
	end {}
}

The function above can be handy for any instance in which you need to know whether a file is being held open by another process. It just so happens that one of those instances is when waiting for Explorer to finish dumping files into a zip archive! Armed with the example above, you can easily wait for the zip file to become available:

$zipPackage.CopyHere($source);
 
Start-Sleep -Seconds 2
while (Test-FileLock $fileName)
{
	Start-Sleep -Seconds 2
}

Note that while the default for the Test-FileLock function is to return $true or $false, I added a -PassThru parameter that changes the output to either a FileInfo object or $null. This alternate behavior allows you pass unlocked files down the pipeline to be used by other cmdlets.

Helper function: Set-TimeZone

Getting time zone information in PowerShell is an exercise in simplicity. This single command:

[TimeZoneInfo]::Local

… yields about as much information as you’d generally care to know about the current system time zone.

Sample results:

Id                         : Mountain Standard Time
DisplayName                : (UTC-07:00) Mountain Time (US & Canada)
StandardName               : Mountain Standard Time
DaylightName               : Mountain Daylight Time
BaseUtcOffset              : -07:00:00
SupportsDaylightSavingTime : True

Setting the current time zone, unfortunately, isn’t quite as easy or intuitive. This is something I needed to do a while back for my desktop imaging process. Although the runtime portion of the Microsoft Deployment Tools (MDT) are largely built on a VBScript foundation, most of my deployment scripts are PowerShell-based, so I came up with a simple Posh function for setting the system time zone that works for both Windows XP and Windows 7.

There are a number of crazy ways to set time zone information, but rather than delving into methods that directly modify the registry and such, I decided upon documented, supported ways to change the current time zone:

function Set-TimeZone {
 
	param(
		[parameter(Mandatory=$true)]
		[string]$TimeZone
	)
 
	$osVersion = (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue("CurrentVersion")
	$proc = New-Object System.Diagnostics.Process
	$proc.StartInfo.WindowStyle = "Hidden"
 
	if ($osVersion -ge 6.0)
	{
		# OS is newer than XP
		$proc.StartInfo.FileName = "tzutil.exe"
		$proc.StartInfo.Arguments = "/s `"$TimeZone`""
	}
	else
	{
		# XP or earlier
		$proc.StartInfo.FileName = $env:comspec
		$proc.StartInfo.Arguments = "/c start /min control.exe TIMEDATE.CPL,,/z $TimeZone"
	}
 
	$proc.Start() | Out-Null
 
}

You can call this function using the friendly time zone name associated with the zone you want. For example,

Set-TimeZone "Pacific Standard Time"
Set-TimeZone "North Asia East Standard Time"
Set-TimeZone "GMT Standard Time"

… and so on.

Note that the function uses the .NET Process class to execute the necessary commands. The Windows 7 tzutil method doesn’t need this, but the control.exe command used for XP and earlier won’t work with the Start-Process cmdlet, so I went with the lowest common denominator.