1 #!powershell
2 
3 # Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #Requires -Module Ansible.ModuleUtils.Legacy
7 
8 # TODO: This module is not idempotent (it will always unzip and report change)
9 
10 $ErrorActionPreference = "Stop"
11 
12 $pcx_extensions = @('.bz2', '.gz', '.msu', '.tar', '.zip')
13 
14 $params = Parse-Args $args -supports_check_mode $true
15 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
16 
17 $src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty $true
18 $dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true
19 $creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
20 $recurse = Get-AnsibleParam -obj $params -name "recurse" -type "bool" -default $false
21 $delete_archive = Get-AnsibleParam -obj $params -name "delete_archive" -type "bool" -default $false -aliases 'rm'
22 $password = Get-AnsibleParam -obj $params -name "password" -type "str"
23 
24 # Fixes a fail error message (when the task actually succeeds) for a
25 # "Convert-ToJson: The converted JSON string is in bad format"
26 # This happens when JSON is parsing a string that ends with a "\",
27 # which is possible when specifying a directory to download to.
28 # This catches that possible error, before assigning the JSON $result
29 $result = @{
30     changed = $false
31     dest = $dest -replace '\$',''
32     removed = $false
33     src = $src -replace '\$',''
34 }
35 
Expand-Zip($src, $dest)36 Function Expand-Zip($src, $dest) {
37     $archive = [System.IO.Compression.ZipFile]::Open($src, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8)
38     foreach ($entry in $archive.Entries) {
39         $archive_name = $entry.FullName
40 
41         $entry_target_path = [System.IO.Path]::Combine($dest, $archive_name)
42         $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
43 
44         # Normalize paths for further evaluation
45         $full_target_path = [System.IO.Path]::GetFullPath($entry_target_path)
46         $full_dest_path = [System.IO.Path]::GetFullPath($dest + [System.IO.Path]::DirectorySeparatorChar)
47 
48         # Ensure file in the archive does not escape the extraction path
49         if (-not $full_target_path.StartsWith($full_dest_path)) {
50             Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'! Filename contains relative paths which would extract outside the destination: $entry_target_path"
51         }
52 
53         if (-not (Test-Path -LiteralPath $entry_dir)) {
54             New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
55             $result.changed = $true
56         }
57 
58         if ((-not ($entry_target_path.EndsWith("\") -or $entry_target_path.EndsWith("/"))) -and (-not $check_mode)) {
59             [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true)
60         }
61         $result.changed = $true
62     }
63     $archive.Dispose()
64 }
65 
Expand-ZipLegacy($src, $dest)66 Function Expand-ZipLegacy($src, $dest) {
67     # [System.IO.Compression.ZipFile] was only added in .net 4.5, this is used
68     # when .net is older than that.
69     $shell = New-Object -ComObject Shell.Application
70     $zip = $shell.NameSpace([IO.Path]::GetFullPath($src))
71     $dest_path = $shell.NameSpace([IO.Path]::GetFullPath($dest))
72 
73     $shell = New-Object -ComObject Shell.Application
74 
75     if (-not $check_mode) {
76         # https://msdn.microsoft.com/en-us/library/windows/desktop/bb787866.aspx
77         # From Folder.CopyHere documentation, 1044 means:
78         #  - 1024: do not display a user interface if an error occurs
79         #  -   16: respond with "yes to all" for any dialog box that is displayed
80         #  -    4: do not display a progress dialog box
81         $dest_path.CopyHere($zip.Items(), 1044)
82     }
83     $result.changed = $true
84 }
85 
86 If ($creates -and (Test-Path -LiteralPath $creates)) {
87     $result.skipped = $true
88     $result.msg = "The file or directory '$creates' already exists."
89     Exit-Json -obj $result
90 }
91 
92 If (-Not (Test-Path -LiteralPath $src)) {
93     Fail-Json -obj $result -message "File '$src' does not exist."
94 }
95 
96 $ext = [System.IO.Path]::GetExtension($src)
97 
98 If (-Not (Test-Path -LiteralPath $dest -PathType Container)){
99     Try{
100         New-Item -ItemType "directory" -path $dest -WhatIf:$check_mode | out-null
101     } Catch {
102         Fail-Json -obj $result -message "Error creating '$dest' directory! Msg: $($_.Exception.Message)"
103     }
104 }
105 
106 If ($ext -eq ".zip" -And $recurse -eq $false -And -Not $password) {
107     # TODO: PS v5 supports zip extraction, use that if available
108     $use_legacy = $false
109     try {
110         # determines if .net 4.5 is available, if this fails we need to fall
111         # back to the legacy COM Shell.Application to extract the zip
112         Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null
113         Add-Type -AssemblyName System.IO.Compression | Out-Null
114     } catch {
115         $use_legacy = $true
116     }
117 
118     if ($use_legacy) {
119         try {
120             Expand-ZipLegacy -src $src -dest $dest
121         } catch {
122             Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'!. Method: COM Shell.Application, Exception: $($_.Exception.Message)"
123         }
124     } else {
125         try {
126             Expand-Zip -src $src -dest $dest
127         } catch {
128             Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'!. Method: System.IO.Compression.ZipFile, Exception: $($_.Exception.Message)"
129         }
130     }
131 } Else {
132     # Check if PSCX is installed
133     $list = Get-Module -ListAvailable
134 
135     If (-Not ($list -match "PSCX")) {
136         Fail-Json -obj $result -message "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types."
137     } Else {
138         $result.pscx_status = "present"
139     }
140 
141     Try {
142         Import-Module PSCX
143     }
144     Catch {
145         Fail-Json $result "Error importing module PSCX"
146     }
147 
148     $expand_params = @{
149         OutputPath = $dest
150         WhatIf = $check_mode
151     }
152     if ($null -ne $password) {
153         $expand_params.Password = ConvertTo-SecureString -String $password -AsPlainText -Force
154     }
155     Try {
156         Expand-Archive -Path $src @expand_params
157     }
158     Catch {
159         Fail-Json -obj $result -message "Error expanding '$src' to '$dest'! Msg: $($_.Exception.Message)"
160     }
161 
162     If ($recurse) {
163         Get-ChildItem -LiteralPath $dest -recurse | Where-Object {$pcx_extensions -contains $_.extension} | ForEach-Object {
164             Try {
165                 Expand-Archive -Path $_.FullName -Force @expand_params
166             } Catch {
167                 Fail-Json -obj $result -message "Error recursively expanding '$src' to '$dest'! Msg: $($_.Exception.Message)"
168             }
169             If ($delete_archive) {
170                 Remove-Item -LiteralPath $_.FullName -Force -WhatIf:$check_mode
171                 $result.removed = $true
172             }
173         }
174     }
175 
176     $result.changed = $true
177 }
178 
179 If ($delete_archive){
180     try {
181         Remove-Item -LiteralPath $src -Recurse -Force -WhatIf:$check_mode
182     } catch {
183         Fail-Json -obj $result -message "failed to delete archive at '$src': $($_.Exception.Message)"
184     }
185     $result.removed = $true
186 }
187 Exit-Json $result
188