1 #!powershell
2 
3 # Copyright: (c) 2016, Ansible Project
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #AnsibleRequires -CSharpUtil Ansible.Basic
7 #Requires -Module Ansible.ModuleUtils.LinkUtil
8 
9 $spec = @{
10     options = @{
11         paths = @{ type = "list"; elements = "str"; required = $true }
12         age = @{ type = "str" }
13         age_stamp = @{ type = "str"; default = "mtime"; choices = "mtime", "ctime", "atime" }
14         file_type = @{ type = "str"; default = "file"; choices = "file", "directory" }
15         follow = @{ type = "bool"; default = $false }
16         hidden = @{ type = "bool"; default = $false }
17         patterns = @{ type = "list"; elements = "str"; aliases = "regex", "regexp" }
18         recurse = @{ type = "bool"; default = $false }
19         size = @{ type = "str" }
20         use_regex = @{ type = "bool"; default = $false }
21         get_checksum = @{ type = "bool"; default = $true }
22         checksum_algorithm = @{ type = "str"; default = "sha1"; choices = "md5", "sha1", "sha256", "sha384", "sha512" }
23     }
24     supports_check_mode = $true
25 }
26 
27 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
28 
29 $paths = $module.Params.paths
30 $age = $module.Params.age
31 $age_stamp = $module.Params.age_stamp
32 $file_type = $module.Params.file_type
33 $follow = $module.Params.follow
34 $hidden = $module.Params.hidden
35 $patterns = $module.Params.patterns
36 $recurse = $module.Params.recurse
37 $size = $module.Params.size
38 $use_regex = $module.Params.use_regex
39 $get_checksum = $module.Params.get_checksum
40 $checksum_algorithm = $module.Params.checksum_algorithm
41 
42 $module.Result.examined = 0
43 $module.Result.files = @()
44 $module.Result.matched = 0
45 
46 Load-LinkUtils
47 
48 Function Assert-Age {
49     Param (
50         [System.IO.FileSystemInfo]$File,
51         [System.Int64]$Age,
52         [System.String]$AgeStamp
53     )
54 
55     $actual_age = switch ($AgeStamp) {
56         mtime { $File.LastWriteTime.Ticks }
57         ctime { $File.CreationTime.Ticks }
58         atime { $File.LastAccessTime.Ticks }
59     }
60 
61     if ($Age -ge 0) {
62         return $Age -ge $actual_age
63     } else {
64         return ($Age * -1) -le $actual_age
65     }
66 }
67 
68 Function Assert-FileType {
69     Param (
70         [System.IO.FileSystemInfo]$File,
71         [System.String]$FileType
72     )
73 
74     $is_dir = $File.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
75     return ($FileType -eq 'directory' -and $is_dir) -or ($FileType -eq 'file' -and -not $is_dir)
76 }
77 
78 Function Assert-FileHidden {
79     Param (
80         [System.IO.FileSystemInfo]$File,
81         [Switch]$IsHidden
82     )
83 
84     $file_is_hidden = $File.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
85     return $IsHidden.IsPresent -eq $file_is_hidden
86 }
87 
88 
89 Function Assert-FileNamePattern {
90     Param (
91         [System.IO.FileSystemInfo]$File,
92         [System.String[]]$Patterns,
93         [Switch]$UseRegex
94     )
95 
96     $valid_match = $false
97     foreach ($pattern in $Patterns) {
98         if ($UseRegex) {
99             if ($File.Name -match $pattern) {
100                 $valid_match = $true
101                 break
102             }
103         } else {
104             if ($File.Name -like $pattern) {
105                 $valid_match = $true
106                 break
107             }
108         }
109     }
110     return $valid_match
111 }
112 
113 Function Assert-FileSize {
114     Param (
115         [System.IO.FileSystemInfo]$File,
116         [System.Int64]$Size
117     )
118 
119     if ($Size -ge 0) {
120         return $File.Length -ge $Size
121     } else {
122         return $File.Length -le ($Size * -1)
123     }
124 }
125 
126 Function Get-FileChecksum {
127     Param (
128         [System.String]$Path,
129         [System.String]$Algorithm
130     )
131 
132     $sp = switch ($algorithm) {
133         'md5' { New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
134         'sha1' { New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
135         'sha256' { New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
136         'sha384' { New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
137         'sha512' { New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
138     }
139 
140     $fp = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
141     try {
142         $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
143     } finally {
144         $fp.Dispose()
145     }
146 
147     return $hash
148 }
149 
150 Function Search-Path {
151     [CmdletBinding()]
152     Param (
153         [Parameter(Mandatory=$true)]
154         [System.String]
155         $Path,
156 
157         [Parameter(Mandatory=$true)]
158         [AllowEmptyCollection()]
159         [System.Collections.Generic.HashSet`1[System.String]]
160         $CheckedPaths,
161 
162         [Parameter(Mandatory=$true)]
163         [Object]
164         $Module,
165 
166         [System.Int64]
167         $Age,
168 
169         [System.String]
170         $AgeStamp,
171 
172         [System.String]
173         $FileType,
174 
175         [Switch]
176         $Follow,
177 
178         [Switch]
179         $GetChecksum,
180 
181         [Switch]
182         $IsHidden,
183 
184         [System.String[]]
185         $Patterns,
186 
187         [Switch]
188         $Recurse,
189 
190         [System.Int64]
191         $Size,
192 
193         [Switch]
194         $UseRegex
195     )
196 
197     $dir_obj = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $Path
198     if ([Int32]$dir_obj.Attributes -eq -1) {
199         $Module.Warn("Argument path '$Path' does not exist, skipping")
200         return
201     } elseif (-not $dir_obj.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
202         $Module.Warn("Argument path '$Path' is a file not a directory, skipping")
203         return
204     }
205 
206     $dir_files = @()
207     try {
208         $dir_files = $dir_obj.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::TopDirectoryOnly)
209     } catch [System.IO.DirectoryNotFoundException] { # Broken ReparsePoint/Symlink, cannot enumerate
210     } catch [System.UnauthorizedAccessException] {}  # No ListDirectory permissions, Get-ChildItem ignored this
211 
212     foreach ($dir_child in $dir_files) {
213         if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -and $Recurse) {
214             if ($Follow -or -not $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
215                 $PSBoundParameters.Remove('Path') > $null
216                 Search-Path -Path $dir_child.FullName @PSBoundParameters
217             }
218         }
219 
220         # Check to see if we've already encountered this path and skip if we have.
221         if (-not $CheckedPaths.Add($dir_child.FullName.ToLowerInvariant())) {
222             continue
223         }
224 
225         $Module.Result.examined++
226 
227         if ($PSBoundParameters.ContainsKey('Age')) {
228             $age_match = Assert-Age -File $dir_child -Age $Age -AgeStamp $AgeStamp
229         } else {
230             $age_match = $true
231         }
232 
233         $file_type_match = Assert-FileType -File $dir_child -FileType $FileType
234         $hidden_match = Assert-FileHidden -File $dir_child -IsHidden:$IsHidden
235 
236         if ($PSBoundParameters.ContainsKey('Patterns')) {
237             $pattern_match = Assert-FileNamePattern -File $dir_child -Patterns $Patterns -UseRegex:$UseRegex.IsPresent
238         } else {
239             $pattern_match = $true
240         }
241 
242         if ($PSBoundParameters.ContainsKey('Size')) {
243             $size_match = Assert-FileSize -File $dir_child -Size $Size
244         } else {
245             $size_match = $true
246         }
247 
248         if (-not ($age_match -and $file_type_match -and $hidden_match -and $pattern_match -and $size_match)) {
249             continue
250         }
251 
252         # It passed all our filters so add it
253         $module.Result.matched++
254 
255         # TODO: Make this generic so it can be shared with win_find and win_stat.
256         $epoch = New-Object -Type System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
257         $file_info = @{
258             attributes = $dir_child.Attributes.ToString()
259             checksum = $null
260             creationtime = (New-TimeSpan -Start $epoch -End $dir_child.CreationTime).TotalSeconds
261             exists = $true
262             extension = $null
263             filename = $dir_child.Name
264             isarchive = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Archive)
265             isdir = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
266             ishidden = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
267             isreadonly = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReadOnly)
268             isreg = $false
269             isshared = $false
270             lastaccesstime = (New-TimeSpan -Start $epoch -End $dir_child.LastAccessTime).TotalSeconds
271             lastwritetime = (New-TimeSpan -Start $epoch -End $dir_child.LastWriteTime).TotalSeconds
272             owner = $null
273             path = $dir_child.FullName
274             sharename = $null
275             size = $null
276         }
277 
278         try {
279             $file_info.owner = $dir_child.GetAccessControl().Owner
280         } catch {}  # May not have rights to get the Owner, historical behaviour is to ignore.
281 
282         if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
283             $share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($dir_child.FullName -replace '\\', '\\')'"
284             if ($null -ne $share_info) {
285                 $file_info.isshared = $true
286                 $file_info.sharename = $share_info.Name
287             }
288         } else {
289             $file_info.extension = $dir_child.Extension
290             $file_info.isreg = $true
291             $file_info.size = $dir_child.Length
292 
293             if ($GetChecksum) {
294                 try {
295                     $file_info.checksum = Get-FileChecksum -Path $dir_child.FullName -Algorithm $checksum_algorithm
296                 } catch {}  # Just keep the checksum as $null in the case of a failure.
297             }
298         }
299 
300         # Append the link information if the path is a link
301         $link_info = @{
302             isjunction = $false
303             islnk = $false
304             nlink = 1
305             lnk_source = $null
306             lnk_target = $null
307             hlnk_targets = @()
308         }
309         $link_stat = Get-Link -link_path $dir_child.FullName
310         if ($null -ne $link_stat) {
311             switch ($link_stat.Type) {
312                 "SymbolicLink" {
313                     $link_info.islnk = $true
314                     $link_info.isreg = $false
315                     $link_info.lnk_source = $link_stat.AbsolutePath
316                     $link_info.lnk_target = $link_stat.TargetPath
317                     break
318                 }
319                 "JunctionPoint" {
320                     $link_info.isjunction = $true
321                     $link_info.isreg = $false
322                     $link_info.lnk_source = $link_stat.AbsolutePath
323                     $link_info.lnk_target = $link_stat.TargetPath
324                     break
325                 }
326                 "HardLink" {
327                     $link_info.nlink = $link_stat.HardTargets.Count
328 
329                     # remove current path from the targets
330                     $hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $dir_child.FullName }
331                     $link_info.hlnk_targets = @($hlnk_targets)
332                     break
333                 }
334             }
335         }
336         foreach ($kv in $link_info.GetEnumerator()) {
337             $file_info.$($kv.Key) = $kv.Value
338         }
339 
340         # Output the file_info object
341         $file_info
342     }
343 }
344 
345 $search_params = @{
346     CheckedPaths = [System.Collections.Generic.HashSet`1[System.String]]@()
347     GetChecksum = $get_checksum
348     Module = $module
349     FileType = $file_type
350     Follow = $follow
351     IsHidden = $hidden
352     Recurse = $recurse
353 }
354 
355 if ($null -ne $age) {
356     $seconds_per_unit = @{'s'=1; 'm'=60; 'h'=3600; 'd'=86400; 'w'=604800}
357     $seconds_pattern = '^(-?\d+)(s|m|h|d|w)?$'
358     $match = $age -match $seconds_pattern
359     if ($Match) {
360         $specified_seconds = [Int64]$Matches[1]
361         if ($null -eq $Matches[2]) {
362             $chosen_unit = 's'
363         } else {
364             $chosen_unit = $Matches[2]
365         }
366 
367         $total_seconds = $specified_seconds * ($seconds_per_unit.$chosen_unit)
368 
369         if ($total_seconds -ge 0) {
370             $search_params.Age = (Get-Date).AddSeconds($total_seconds * -1).Ticks
371         } else {
372             # Make sure we add the positive value of seconds to current time then make it negative for later comparisons.
373             $age = (Get-Date).AddSeconds($total_seconds).Ticks
374             $search_params.Age = $age * -1
375         }
376         $search_params.AgeStamp = $age_stamp
377     } else {
378         $module.FailJson("Invalid age pattern specified")
379     }
380 }
381 
382 if ($null -ne $patterns) {
383     $search_params.Patterns = $patterns
384     $search_params.UseRegex = $use_regex
385 }
386 
387 if ($null -ne $size) {
388     $bytes_per_unit = @{'b'=1; 'k'=1KB; 'm'=1MB; 'g'=1GB;'t'=1TB}
389     $size_pattern = '^(-?\d+)(b|k|m|g|t)?$'
390     $match = $size -match $size_pattern
391     if ($Match) {
392         $specified_size = [Int64]$Matches[1]
393         if ($null -eq $Matches[2]) {
394             $chosen_byte = 'b'
395         } else {
396             $chosen_byte = $Matches[2]
397         }
398 
399         $search_params.Size = $specified_size * ($bytes_per_unit.$chosen_byte)
400     } else {
401         $module.FailJson("Invalid size pattern specified")
402     }
403 }
404 
405 $matched_files = foreach ($path in $paths) {
406     # Ensure we pass in an absolute path. We use the ExecutionContext as this is based on the PSProvider path not the
407     # process location which can be different.
408     $abs_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
409     Search-Path -Path $abs_path @search_params
410 }
411 
412 # Make sure we sort the files in alphabetical order.
413 $module.Result.files = @() + ($matched_files | Sort-Object -Property {$_.path})
414 
415 $module.ExitJson()
416 
417