Using PowerShell Class to Deploy Zip files


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:


#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

view raw

Copy-code.ps1

hosted with ❤ by GitHub

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s