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