Recently I have been working with @developermj on a class that he wrote for deploying code to a server from a zip file. This blog article is about how that code works.
To Start this off we need to gain access to the dot net classes that have the features for zipping and unzipping files in them:
System.IO.Compression & System.IO.Compression.FileSystem
These will get added with two statements using and Add-type
#requires -version 5.0 using namespace System.IO using namespace System.IO.Compression param( [Parameter(Mandatory=$true)][string]$sourceZip, [Parameter(Mandatory=$true)][string]$destPath ) add-type -assemblyname 'System.IO.Compression' add-type -assemblyname 'System.IO.Compression.FileSystem'
Then we’ll build the first part of our utility which is our function to deploy the files. This function is where all the magic is:
function Deploy-Files { param( [ValidateNotNullOrEmpty()][FileInfo]$sourceZip, [ValidateNotNullOrEmpty()][DirectoryInfo]$destFolder ) if (-not $sourceZip.Exists) { throw "Zip $($sourceZip.Name) does not exist" } [ZipArchive]$archive = [ZipFile]::Open($sourceZip, "Read") [DeployFile[]]$files = $archive.Entries | where-object {$_.Length -gt 0} ` | %{[ArchiveFile]::new($_)} if ($files.Length -eq 0) { Write-Information "No files to copy" } $hasWritten = $false foreach ($file in $files) { [FileInfo]$destFile = "$destFolder$($file.GetName())" $copied = $file.TryCopy($destFile) if ($copied) { $hasWritten = $true } } Write-Information "Done" if (-not $hasWritten) { Write-Information "...Nothing copied" } }
Since the incoming object is of type Fileinfo we can find out if the file exists with this statement: if (-not $sourceZip.Exists) . If the sourcezip exists then we progress on through our function. Else we throw an exception.
Since we’ve imported the dot net classes for filecompression we now have an available type we can cast our $archive variable to [ZipArchive]. Since ZipArchive requires a stream we can open the zip file with the ZipFile class and stream it to the ZipArchive object.
Now that we have the entire contents for the archive in a variable $archive we can use apply our class to the variable. Below is what the value of my $archive looks like.
[DBG]: PS ps:\>> $archive Entries Mode ------- ---- {Code/, Code/Lib/, Code/Lib/ICSharpCode.SharpZipLib.dll, Code/Mindscape.Samples.Powershell.ZipProvider.csproj...} Read [DBG]: PS ps:\>> $archive.entries.count 11
The next line in the code is where we’ll start using the Class we’ve defined in our script.
[DeployFile[]]$files = $archive.Entries ` | where-object {$_.Length -gt 0} | %{[ArchiveFile]::new($_)}
Since we are creating a new object of type [deployFile[]] Powershell will see this and instantiate a new object from our Class. In the example above we are taking each archive entry and creating a new [ArchiveFile]. If we follow the code through this loop we’ll find the first data element that’s length is greater than 0 will be defined as a [Archivefile].
class ArchiveFile : DeployFile { hidden [ZipArchiveEntry]$entry ArchiveFile([ZipArchiveEntry]$entry) { $this.entry = $entry } [DateTime] GetModifiedDate() { return $this.entry.LastWriteTime.UtcDateTime } [void] Copy([FileInfo]$file) { [ZipFileExtensions]::ExtractToFile($this.entry, $file.FullName, $true) } [string] GetName() { return "\$($this.entry.FullName)" } }
As you can see from the declaration for this class [ArchiveFile] inherits the [DeployFile] class. PowerShell will hit the constructor that matches what was passed to the class. We passed a [ZipArchiveEntry]
Since this is now defined a new object it inherits all the methods that are declared in the class for this object type. This object type has The following methods defined:
GetModifiedDate, Copy, GetName
It then inherits from the [DeployFile] from this inheritance it gets the following methods:
ShouldCopy, Copy, TryCopy, ToString
ArchiveFile([ZipArchiveEntry]$entry) { $this.entry = $entry }
If we continue to loop through each item in our intial $archive variable we’ll notice that we end up with a new Variable of type [DeployFile]. This $files variable is now of that type if we pipe the variable to get member we’ll see that we have a class name of [ArchiveFile]. if we look at the members of the $files of the array we’ll see the [Archivefile] class and the methods that were inherited from the other class [DeployFiles].
DBG]: PS ps:\>> $files[0] | gm TypeName: ArchiveFile Name MemberType Definition ---- ---------- ---------- Copy Method void Copy(System.IO.FileInfo file) Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetModifiedDate Method datetime GetModifiedDate() GetName Method string GetName() GetType Method type GetType() ShouldCopy Method bool ShouldCopy(System.IO.FileInfo file) ToString Method string ToString() TryCopy Method bool TryCopy(System.IO.FileInfo file)
Now that we have our class we can move onto deploying these files to the intended target. Which is what this next line of code does.
foreach ($file in $files) { [FileInfo]$destFile = "$destFolder$($file.GetName())" $copied = $file.TryCopy($destFile) if ($copied) { $hasWritten = $true } }
The Foreach loops through each file and gets the destingation location plus the name of the file by calling the classes method getname().
[DBG]: PS ps:\>> $file \Code/Lib/ICSharpCode.SharpZipLib.dll [DBG]: PS ps:\>> $file.getname() \Code/Lib/ICSharpCode.SharpZipLib.dll
Now that we have a [Fileinfo] object we can now call the TryCopy method on our $file. TryCopy Expects a type of [Fileinfo]
$copied = $file.TryCopy($destFile)
Which takes us to our class for file into it’s method TryCopy
[bool] TryCopy([FileInfo]$file) { if ($this.ShouldCopy($file)) { [DeployFile]::CreateFolderIfNeeded($file) Write-Verbose "Copying to $($file.Name)" $this.Copy($file) return $true }
The first this is we are going to test to see if we should copy this file with the should Copy method on the same object ($this).
[bool] ShouldCopy([FileInfo]$file) { if (-not $file.Exists) { return $true } if ($this.GetModifiedDate() -gt $file.LastWriteTimeUtc) { return $true } return $false }
This function will check to see if the file doesn’t exist with -not $file.exists. Then it checks to see what the modified date is. if the Modified date is greater than the files last writetime in UTC. Then we are going to return true. Which means that this file is newer and should be copied. Hence the function name should copy. If both those tests fail then we’ll return false because the file exists and its timestamp is less than the lastwritetimeutc.
Now we return back to the TryCopy. Provided the return results of the try copy is true we’ll next check to see if we need to create a directory through a call to the class [DeployFile]::CreateFolderifNeeded([fileinfo]). This function is part of the deployfile class and will create a folder if it isn’t present for the file in question.
Now that the folder is created. We can now call the copy function from the $file object.
This will copy the file to the destination filename based on the $file object.
Note:
I haven’t been able to get this script to run on it’s own without writing a wrapper script to then call this one. I’ve posted an article about this on Powershell.org.
https://powershell.org/forums/topic/system-io-compression-in-powershell-class/
Here is what I have in my wrapper Script:
#requires -version 5.0 using namespace System.IO using namespace System.IO.Compression param( [Parameter(Mandatory=$true)][string]$sourceZip, [Parameter(Mandatory=$true)][string]$destPath ) add-type -assemblyname 'System.IO.Compression' add-type -assemblyname 'System.IO.Compression.FileSystem' & .\copy-code.ps1 -sourceZip $sourceZip -destpath $destpath
I hope this helps someone.
Until then keep scripting
Thom
Full copy of the script is in this Gist:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#requires -version 5.0 | |
using namespace System.IO | |
using namespace System.IO.Compression | |
param( | |
[Parameter(Mandatory=$true)][string]$sourceZip, | |
[Parameter(Mandatory=$true)][string]$destPath | |
) | |
add-type –assemblyname 'System.IO.Compression' | |
add-type –assemblyname 'System.IO.Compression.FileSystem' | |
#region Functions | |
function Deploy-Files { | |
param( | |
[ValidateNotNullOrEmpty()][FileInfo]$sourceZip, | |
[ValidateNotNullOrEmpty()][DirectoryInfo]$destFolder | |
) | |
if (-not $sourceZip.Exists) { | |
throw "Zip $($sourceZip.Name) does not exist" | |
} | |
[ZipArchive]$archive = [ZipFile]::Open($sourceZip, "Read") | |
[DeployFile[]]$files = $archive.Entries | where-object {$_.Length -gt 0} | %{[ArchiveFile]::new($_)} | |
if ($files.Length -eq 0) { | |
Write-Information "No files to copy" | |
} | |
$hasWritten = $false | |
foreach ($file in $files) { | |
[FileInfo]$destFile = "$destFolder$($file.GetName())" | |
$copied = $file.TryCopy($destFile) | |
if ($copied) { $hasWritten = $true } | |
} | |
Write-Information "Done" | |
if (-not $hasWritten) { | |
Write-Information "…Nothing copied" | |
} | |
} | |
class DeployFile { | |
static [void] CreateFolderIfNeeded([FileInfo]$file) { | |
if (-not $file.Directory.Exists) { | |
Write-Verbose "Creating folder $($file.Directory.FullName)" | |
$file.Directory.Create() | |
} | |
} | |
[string] GetName() { throw "Name must be specified" } | |
[bool] ShouldCopy([FileInfo]$file) { | |
if (-not $file.Exists) { | |
return $true | |
} | |
if ($this.GetModifiedDate() -gt $file.LastWriteTimeUtc) { | |
return $true | |
} | |
return $false | |
} | |
[void] Copy([FileInfo]$file) { | |
throw "copy method not overridden" | |
} | |
[bool] TryCopy([FileInfo]$file) { | |
if ($this.ShouldCopy($file)) { | |
[DeployFile]::CreateFolderIfNeeded($file) | |
Write-Verbose "Copying to $($file.Name)" | |
$this.Copy($file) | |
return $true | |
} | |
return $false | |
} | |
[string] ToString() { | |
return $this.GetName() | |
} | |
} | |
class ArchiveFile : DeployFile { | |
hidden [ZipArchiveEntry]$entry | |
ArchiveFile([ZipArchiveEntry]$entry) { | |
$this.entry = $entry | |
} | |
[DateTime] GetModifiedDate() { | |
return $this.entry.LastWriteTime.UtcDateTime | |
} | |
[void] Copy([FileInfo]$file) { | |
[ZipFileExtensions]::ExtractToFile($this.entry, $file.FullName, $true) | |
} | |
[string] GetName() { | |
return "\$($this.entry.FullName)" | |
} | |
} | |
# For physical file support if wanted in the future | |
class PhysicalFile : DeployFile { | |
hidden [FileInfo]$entry | |
hidden [DirectoryInfo]$root | |
PhysicalFile([FileInfo]$entry, [DirectoryInfo]$rootFolder) { | |
$this.entry = $entry | |
$this.root = $rootFolder | |
} | |
[DateTime] GetModifiedDate() { | |
return $this.entry.LastWriteTimeUtc | |
} | |
[void] Copy([FileInfo]$file) { | |
$this.entry.CopyTo($file, $true) | |
} | |
[string] GetName() { | |
return $this.entry.FullName.Substring($this.root.FullName.Length) | |
} | |
} | |
#endregion Functions | |
$dotnetversion = [Environment]::Version | |
if(!($dotnetversion.Major -ge 4 -and $dotnetversion.Build -ge 30319)) { | |
write-error "Version of DotNet must be greater than or equal to $($dotnetversion.Major).30319" | |
exit(1) | |
} | |
Deploy-files –sourceZip $sourcezip –destFolder $destPath | |