417 lines
13 KiB
PowerShell
417 lines
13 KiB
PowerShell
|
#!powershell
|
||
|
|
||
|
# Copyright: (c) 2016, Ansible Project
|
||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||
|
|
||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||
|
#Requires -Module Ansible.ModuleUtils.LinkUtil
|
||
|
|
||
|
$spec = @{
|
||
|
options = @{
|
||
|
paths = @{ type = "list"; elements = "str"; required = $true }
|
||
|
age = @{ type = "str" }
|
||
|
age_stamp = @{ type = "str"; default = "mtime"; choices = "mtime", "ctime", "atime" }
|
||
|
file_type = @{ type = "str"; default = "file"; choices = "file", "directory" }
|
||
|
follow = @{ type = "bool"; default = $false }
|
||
|
hidden = @{ type = "bool"; default = $false }
|
||
|
patterns = @{ type = "list"; elements = "str"; aliases = "regex", "regexp" }
|
||
|
recurse = @{ type = "bool"; default = $false }
|
||
|
size = @{ type = "str" }
|
||
|
use_regex = @{ type = "bool"; default = $false }
|
||
|
get_checksum = @{ type = "bool"; default = $true }
|
||
|
checksum_algorithm = @{ type = "str"; default = "sha1"; choices = "md5", "sha1", "sha256", "sha384", "sha512" }
|
||
|
}
|
||
|
supports_check_mode = $true
|
||
|
}
|
||
|
|
||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||
|
|
||
|
$paths = $module.Params.paths
|
||
|
$age = $module.Params.age
|
||
|
$age_stamp = $module.Params.age_stamp
|
||
|
$file_type = $module.Params.file_type
|
||
|
$follow = $module.Params.follow
|
||
|
$hidden = $module.Params.hidden
|
||
|
$patterns = $module.Params.patterns
|
||
|
$recurse = $module.Params.recurse
|
||
|
$size = $module.Params.size
|
||
|
$use_regex = $module.Params.use_regex
|
||
|
$get_checksum = $module.Params.get_checksum
|
||
|
$checksum_algorithm = $module.Params.checksum_algorithm
|
||
|
|
||
|
$module.Result.examined = 0
|
||
|
$module.Result.files = @()
|
||
|
$module.Result.matched = 0
|
||
|
|
||
|
Load-LinkUtils
|
||
|
|
||
|
Function Assert-Age {
|
||
|
Param (
|
||
|
[System.IO.FileSystemInfo]$File,
|
||
|
[System.Int64]$Age,
|
||
|
[System.String]$AgeStamp
|
||
|
)
|
||
|
|
||
|
$actual_age = switch ($AgeStamp) {
|
||
|
mtime { $File.LastWriteTime.Ticks }
|
||
|
ctime { $File.CreationTime.Ticks }
|
||
|
atime { $File.LastAccessTime.Ticks }
|
||
|
}
|
||
|
|
||
|
if ($Age -ge 0) {
|
||
|
return $Age -ge $actual_age
|
||
|
} else {
|
||
|
return ($Age * -1) -le $actual_age
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Function Assert-FileType {
|
||
|
Param (
|
||
|
[System.IO.FileSystemInfo]$File,
|
||
|
[System.String]$FileType
|
||
|
)
|
||
|
|
||
|
$is_dir = $File.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
|
||
|
return ($FileType -eq 'directory' -and $is_dir) -or ($FileType -eq 'file' -and -not $is_dir)
|
||
|
}
|
||
|
|
||
|
Function Assert-FileHidden {
|
||
|
Param (
|
||
|
[System.IO.FileSystemInfo]$File,
|
||
|
[Switch]$IsHidden
|
||
|
)
|
||
|
|
||
|
$file_is_hidden = $File.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
|
||
|
return $IsHidden.IsPresent -eq $file_is_hidden
|
||
|
}
|
||
|
|
||
|
|
||
|
Function Assert-FileNamePattern {
|
||
|
Param (
|
||
|
[System.IO.FileSystemInfo]$File,
|
||
|
[System.String[]]$Patterns,
|
||
|
[Switch]$UseRegex
|
||
|
)
|
||
|
|
||
|
$valid_match = $false
|
||
|
foreach ($pattern in $Patterns) {
|
||
|
if ($UseRegex) {
|
||
|
if ($File.Name -match $pattern) {
|
||
|
$valid_match = $true
|
||
|
break
|
||
|
}
|
||
|
} else {
|
||
|
if ($File.Name -like $pattern) {
|
||
|
$valid_match = $true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return $valid_match
|
||
|
}
|
||
|
|
||
|
Function Assert-FileSize {
|
||
|
Param (
|
||
|
[System.IO.FileSystemInfo]$File,
|
||
|
[System.Int64]$Size
|
||
|
)
|
||
|
|
||
|
if ($Size -ge 0) {
|
||
|
return $File.Length -ge $Size
|
||
|
} else {
|
||
|
return $File.Length -le ($Size * -1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Function Get-FileChecksum {
|
||
|
Param (
|
||
|
[System.String]$Path,
|
||
|
[System.String]$Algorithm
|
||
|
)
|
||
|
|
||
|
$sp = switch ($algorithm) {
|
||
|
'md5' { New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
|
||
|
'sha1' { New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
|
||
|
'sha256' { New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
|
||
|
'sha384' { New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
|
||
|
'sha512' { New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
|
||
|
}
|
||
|
|
||
|
$fp = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||
|
try {
|
||
|
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
|
||
|
} finally {
|
||
|
$fp.Dispose()
|
||
|
}
|
||
|
|
||
|
return $hash
|
||
|
}
|
||
|
|
||
|
Function Search-Path {
|
||
|
[CmdletBinding()]
|
||
|
Param (
|
||
|
[Parameter(Mandatory=$true)]
|
||
|
[System.String]
|
||
|
$Path,
|
||
|
|
||
|
[Parameter(Mandatory=$true)]
|
||
|
[AllowEmptyCollection()]
|
||
|
[System.Collections.Generic.HashSet`1[System.String]]
|
||
|
$CheckedPaths,
|
||
|
|
||
|
[Parameter(Mandatory=$true)]
|
||
|
[Object]
|
||
|
$Module,
|
||
|
|
||
|
[System.Int64]
|
||
|
$Age,
|
||
|
|
||
|
[System.String]
|
||
|
$AgeStamp,
|
||
|
|
||
|
[System.String]
|
||
|
$FileType,
|
||
|
|
||
|
[Switch]
|
||
|
$Follow,
|
||
|
|
||
|
[Switch]
|
||
|
$GetChecksum,
|
||
|
|
||
|
[Switch]
|
||
|
$IsHidden,
|
||
|
|
||
|
[System.String[]]
|
||
|
$Patterns,
|
||
|
|
||
|
[Switch]
|
||
|
$Recurse,
|
||
|
|
||
|
[System.Int64]
|
||
|
$Size,
|
||
|
|
||
|
[Switch]
|
||
|
$UseRegex
|
||
|
)
|
||
|
|
||
|
$dir_obj = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $Path
|
||
|
if ([Int32]$dir_obj.Attributes -eq -1) {
|
||
|
$Module.Warn("Argument path '$Path' does not exist, skipping")
|
||
|
return
|
||
|
} elseif (-not $dir_obj.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||
|
$Module.Warn("Argument path '$Path' is a file not a directory, skipping")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
$dir_files = @()
|
||
|
try {
|
||
|
$dir_files = $dir_obj.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::TopDirectoryOnly)
|
||
|
} catch [System.IO.DirectoryNotFoundException] { # Broken ReparsePoint/Symlink, cannot enumerate
|
||
|
} catch [System.UnauthorizedAccessException] {} # No ListDirectory permissions, Get-ChildItem ignored this
|
||
|
|
||
|
foreach ($dir_child in $dir_files) {
|
||
|
if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -and $Recurse) {
|
||
|
if ($Follow -or -not $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
|
||
|
$PSBoundParameters.Remove('Path') > $null
|
||
|
Search-Path -Path $dir_child.FullName @PSBoundParameters
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Check to see if we've already encountered this path and skip if we have.
|
||
|
if (-not $CheckedPaths.Add($dir_child.FullName.ToLowerInvariant())) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
$Module.Result.examined++
|
||
|
|
||
|
if ($PSBoundParameters.ContainsKey('Age')) {
|
||
|
$age_match = Assert-Age -File $dir_child -Age $Age -AgeStamp $AgeStamp
|
||
|
} else {
|
||
|
$age_match = $true
|
||
|
}
|
||
|
|
||
|
$file_type_match = Assert-FileType -File $dir_child -FileType $FileType
|
||
|
$hidden_match = Assert-FileHidden -File $dir_child -IsHidden:$IsHidden
|
||
|
|
||
|
if ($PSBoundParameters.ContainsKey('Patterns')) {
|
||
|
$pattern_match = Assert-FileNamePattern -File $dir_child -Patterns $Patterns -UseRegex:$UseRegex.IsPresent
|
||
|
} else {
|
||
|
$pattern_match = $true
|
||
|
}
|
||
|
|
||
|
if ($PSBoundParameters.ContainsKey('Size')) {
|
||
|
$size_match = Assert-FileSize -File $dir_child -Size $Size
|
||
|
} else {
|
||
|
$size_match = $true
|
||
|
}
|
||
|
|
||
|
if (-not ($age_match -and $file_type_match -and $hidden_match -and $pattern_match -and $size_match)) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
# It passed all our filters so add it
|
||
|
$module.Result.matched++
|
||
|
|
||
|
# TODO: Make this generic so it can be shared with win_find and win_stat.
|
||
|
$epoch = New-Object -Type System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
|
||
|
$file_info = @{
|
||
|
attributes = $dir_child.Attributes.ToString()
|
||
|
checksum = $null
|
||
|
creationtime = (New-TimeSpan -Start $epoch -End $dir_child.CreationTime).TotalSeconds
|
||
|
exists = $true
|
||
|
extension = $null
|
||
|
filename = $dir_child.Name
|
||
|
isarchive = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Archive)
|
||
|
isdir = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
|
||
|
ishidden = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
|
||
|
isreadonly = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReadOnly)
|
||
|
isreg = $false
|
||
|
isshared = $false
|
||
|
lastaccesstime = (New-TimeSpan -Start $epoch -End $dir_child.LastAccessTime).TotalSeconds
|
||
|
lastwritetime = (New-TimeSpan -Start $epoch -End $dir_child.LastWriteTime).TotalSeconds
|
||
|
owner = $null
|
||
|
path = $dir_child.FullName
|
||
|
sharename = $null
|
||
|
size = $null
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$file_info.owner = $dir_child.GetAccessControl().Owner
|
||
|
} catch {} # May not have rights to get the Owner, historical behaviour is to ignore.
|
||
|
|
||
|
if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||
|
$share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($dir_child.FullName -replace '\\', '\\')'"
|
||
|
if ($null -ne $share_info) {
|
||
|
$file_info.isshared = $true
|
||
|
$file_info.sharename = $share_info.Name
|
||
|
}
|
||
|
} else {
|
||
|
$file_info.extension = $dir_child.Extension
|
||
|
$file_info.isreg = $true
|
||
|
$file_info.size = $dir_child.Length
|
||
|
|
||
|
if ($GetChecksum) {
|
||
|
try {
|
||
|
$file_info.checksum = Get-FileChecksum -Path $dir_child.FullName -Algorithm $checksum_algorithm
|
||
|
} catch {} # Just keep the checksum as $null in the case of a failure.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Append the link information if the path is a link
|
||
|
$link_info = @{
|
||
|
isjunction = $false
|
||
|
islnk = $false
|
||
|
nlink = 1
|
||
|
lnk_source = $null
|
||
|
lnk_target = $null
|
||
|
hlnk_targets = @()
|
||
|
}
|
||
|
$link_stat = Get-Link -link_path $dir_child.FullName
|
||
|
if ($null -ne $link_stat) {
|
||
|
switch ($link_stat.Type) {
|
||
|
"SymbolicLink" {
|
||
|
$link_info.islnk = $true
|
||
|
$link_info.isreg = $false
|
||
|
$link_info.lnk_source = $link_stat.AbsolutePath
|
||
|
$link_info.lnk_target = $link_stat.TargetPath
|
||
|
break
|
||
|
}
|
||
|
"JunctionPoint" {
|
||
|
$link_info.isjunction = $true
|
||
|
$link_info.isreg = $false
|
||
|
$link_info.lnk_source = $link_stat.AbsolutePath
|
||
|
$link_info.lnk_target = $link_stat.TargetPath
|
||
|
break
|
||
|
}
|
||
|
"HardLink" {
|
||
|
$link_info.nlink = $link_stat.HardTargets.Count
|
||
|
|
||
|
# remove current path from the targets
|
||
|
$hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $dir_child.FullName }
|
||
|
$link_info.hlnk_targets = @($hlnk_targets)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
foreach ($kv in $link_info.GetEnumerator()) {
|
||
|
$file_info.$($kv.Key) = $kv.Value
|
||
|
}
|
||
|
|
||
|
# Output the file_info object
|
||
|
$file_info
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$search_params = @{
|
||
|
CheckedPaths = [System.Collections.Generic.HashSet`1[System.String]]@()
|
||
|
GetChecksum = $get_checksum
|
||
|
Module = $module
|
||
|
FileType = $file_type
|
||
|
Follow = $follow
|
||
|
IsHidden = $hidden
|
||
|
Recurse = $recurse
|
||
|
}
|
||
|
|
||
|
if ($null -ne $age) {
|
||
|
$seconds_per_unit = @{'s'=1; 'm'=60; 'h'=3600; 'd'=86400; 'w'=604800}
|
||
|
$seconds_pattern = '^(-?\d+)(s|m|h|d|w)?$'
|
||
|
$match = $age -match $seconds_pattern
|
||
|
if ($Match) {
|
||
|
$specified_seconds = [Int64]$Matches[1]
|
||
|
if ($null -eq $Matches[2]) {
|
||
|
$chosen_unit = 's'
|
||
|
} else {
|
||
|
$chosen_unit = $Matches[2]
|
||
|
}
|
||
|
|
||
|
$total_seconds = $specified_seconds * ($seconds_per_unit.$chosen_unit)
|
||
|
|
||
|
if ($total_seconds -ge 0) {
|
||
|
$search_params.Age = (Get-Date).AddSeconds($total_seconds * -1).Ticks
|
||
|
} else {
|
||
|
# Make sure we add the positive value of seconds to current time then make it negative for later comparisons.
|
||
|
$age = (Get-Date).AddSeconds($total_seconds).Ticks
|
||
|
$search_params.Age = $age * -1
|
||
|
}
|
||
|
$search_params.AgeStamp = $age_stamp
|
||
|
} else {
|
||
|
$module.FailJson("Invalid age pattern specified")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($null -ne $patterns) {
|
||
|
$search_params.Patterns = $patterns
|
||
|
$search_params.UseRegex = $use_regex
|
||
|
}
|
||
|
|
||
|
if ($null -ne $size) {
|
||
|
$bytes_per_unit = @{'b'=1; 'k'=1KB; 'm'=1MB; 'g'=1GB;'t'=1TB}
|
||
|
$size_pattern = '^(-?\d+)(b|k|m|g|t)?$'
|
||
|
$match = $size -match $size_pattern
|
||
|
if ($Match) {
|
||
|
$specified_size = [Int64]$Matches[1]
|
||
|
if ($null -eq $Matches[2]) {
|
||
|
$chosen_byte = 'b'
|
||
|
} else {
|
||
|
$chosen_byte = $Matches[2]
|
||
|
}
|
||
|
|
||
|
$search_params.Size = $specified_size * ($bytes_per_unit.$chosen_byte)
|
||
|
} else {
|
||
|
$module.FailJson("Invalid size pattern specified")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$matched_files = foreach ($path in $paths) {
|
||
|
# Ensure we pass in an absolute path. We use the ExecutionContext as this is based on the PSProvider path not the
|
||
|
# process location which can be different.
|
||
|
$abs_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
|
||
|
Search-Path -Path $abs_path @search_params
|
||
|
}
|
||
|
|
||
|
# Make sure we sort the files in alphabetical order.
|
||
|
$module.Result.files = @() + ($matched_files | Sort-Object -Property {$_.path})
|
||
|
|
||
|
$module.ExitJson()
|
||
|
|