├── Functions └── GenXdev.FileSystem │ ├── AssurePester.ps1 │ ├── Expand-Path.ps1 │ ├── Find-DuplicateFiles.ps1 │ ├── Find-Item.ps1 │ ├── Invoke-Fasti.ps1 │ ├── Move-ItemWithTracking.ps1 │ ├── Move-ToRecycleBin.ps1 │ ├── Remove-AllItems.ps1 │ ├── Remove-ItemWithFallback.ps1 │ ├── Remove-OnReboot.ps1 │ ├── Rename-InProject.ps1 │ ├── Start-RoboCopy.ps1 │ └── _AssureTypes.ps1 ├── GenXdev.FileSystem.psd1 ├── GenXdev.FileSystem.psm1 ├── LICENSE ├── README.md ├── Tests └── GenXdev.FileSystem │ ├── AssurePester.Tests.ps1 │ ├── Expand-Path.Tests.ps1 │ ├── Find-DuplicateFiles.Tests.ps1 │ ├── Find-Item.Tests.ps1 │ ├── Invoke-Fasti.Tests.ps1 │ ├── Move-ItemWithTracking.Tests.ps1 │ ├── Move-ToRecycleBin.Tests.ps1 │ ├── Remove-AllItems.Tests.ps1 │ ├── Remove-ItemWithFallback.Tests.ps1 │ ├── Remove-OnReboot.Tests.ps1 │ ├── Rename-InProject.Tests.ps1 │ ├── Start-RoboCopy.Tests.ps1 │ └── _AssureTypes.Tests.ps1 ├── license.txt └── powershell.jpg /Functions/GenXdev.FileSystem/AssurePester.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Ensures Pester testing framework is available for use. 5 | 6 | .DESCRIPTION 7 | This function verifies if the Pester module is installed in the current 8 | PowerShell environment. If not found, it automatically installs it from the 9 | PowerShell Gallery and imports it into the current session. This ensures that 10 | Pester testing capabilities are available when needed. 11 | 12 | .EXAMPLE 13 | AssurePester 14 | # This ensures Pester is installed and ready for use 15 | #> 16 | function AssurePester { 17 | 18 | [CmdletBinding()] 19 | param() 20 | 21 | begin { 22 | 23 | # inform user that we're checking pester installation 24 | Microsoft.PowerShell.Utility\Write-Verbose "Checking for Pester module installation..." 25 | } 26 | 27 | 28 | process { 29 | 30 | # attempt silent import of pester to check if it's available 31 | Microsoft.PowerShell.Core\Import-Module -Name Pester -ErrorAction SilentlyContinue 32 | 33 | $found = (Microsoft.PowerShell.Core\Get-Module -Name Pester -ErrorAction SilentlyContinue); 34 | 35 | # verify if pester module is now loaded in the current session 36 | if ((-not $found) -or ($found.Version -lt '5.7.0')) { 37 | 38 | # notify about installation attempt through verbose and regular output 39 | Microsoft.PowerShell.Utility\Write-Verbose "Pester module not found, attempting installation..." 40 | Microsoft.PowerShell.Utility\Write-Host "Pester not found. Installing Pester..." 41 | 42 | try { 43 | # install pester module from the powershell gallery 44 | $null = PowerShellGet\Install-Module -Name Pester ` 45 | -Force ` 46 | -SkipPublisherCheck 47 | 48 | # load the newly installed pester module 49 | $null = Microsoft.PowerShell.Core\Import-Module -Name Pester -Force 50 | 51 | # confirm successful installation 52 | Microsoft.PowerShell.Utility\Write-Host "Pester installed successfully." 53 | Microsoft.PowerShell.Utility\Write-Verbose "Pester module installation and import completed." 54 | } 55 | catch { 56 | # report any installation failures 57 | Microsoft.PowerShell.Utility\Write-Error "Failed to install Pester. Error: $PSItem" 58 | Microsoft.PowerShell.Utility\Write-Verbose "Pester installation failed with error." 59 | } 60 | } 61 | else { 62 | # inform that pester is already available 63 | Microsoft.PowerShell.Utility\Write-Verbose "Pester module already installed and imported." 64 | } 65 | } 66 | 67 | end { 68 | } 69 | } 70 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Expand-Path.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Expands any given file reference to a full pathname. 5 | 6 | .DESCRIPTION 7 | Expands any given file reference to a full pathname, with respect to the user's 8 | current directory. Can optionally assure that directories or files exist. 9 | 10 | .PARAMETER FilePath 11 | The file path to expand to a full path. 12 | 13 | .PARAMETER CreateDirectory 14 | Will create directory if it does not exist. 15 | 16 | .PARAMETER CreateFile 17 | Will create an empty file if it does not exist. 18 | 19 | .EXAMPLE 20 | Expand-Path -FilePath ".\myfile.txt" -CreateFile 21 | 22 | .EXAMPLE 23 | ep ~\documents\test.txt -CreateFile 24 | #> 25 | function Expand-Path { 26 | 27 | [CmdletBinding()] 28 | [Alias("ep")] 29 | 30 | param( 31 | ######################################################################## 32 | [Parameter( 33 | Mandatory = $true, 34 | Position = 0, 35 | ValueFromPipeline = $true, 36 | ValueFromPipelineByPropertyName = $true, 37 | HelpMessage = "Path to expand" 38 | )] 39 | [ValidateNotNullOrEmpty()] 40 | [string] $FilePath, 41 | ######################################################################## 42 | [Parameter( 43 | Mandatory = $false, 44 | HelpMessage = "Will create directory if it does not exist" 45 | )] 46 | [switch] $CreateDirectory, 47 | ######################################################################## 48 | [Parameter( 49 | Mandatory = $false, 50 | HelpMessage = "Will create an empty file if it does not exist" 51 | )] 52 | [switch] $CreateFile, 53 | ######################################################################## 54 | [Parameter( 55 | Mandatory = $false, 56 | HelpMessage = "Will delete the file if it already exists" 57 | )] 58 | [switch] $DeleteExistingFile, 59 | ######################################################################## 60 | [Parameter( 61 | Mandatory = $false, 62 | HelpMessage = "Will force the use of a specific drive" 63 | )] 64 | [char] $ForceDrive = '*', 65 | ######################################################################## 66 | [Parameter( 67 | Mandatory = $false, 68 | HelpMessage = "Will throw if file does not exist" 69 | )] 70 | [switch] $FileMustExist, 71 | ######################################################################## 72 | [Parameter( 73 | Mandatory = $false, 74 | HelpMessage = "Will throw if directory does not exist" 75 | )] 76 | [switch] $DirectoryMustExist 77 | ######################################################################## 78 | ) 79 | 80 | begin { 81 | 82 | # normalize path separators and remove double separators 83 | [string] $normalizedPath = $FilePath.Trim().Replace("\", [IO.Path]::DirectorySeparatorChar). 84 | Replace("/", [IO.Path]::DirectorySeparatorChar); 85 | 86 | if ($normalizedPath.StartsWith([IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar)) { 87 | 88 | $normalizedPath = [IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar + 89 | $normalizedPath.Substring(2).Replace( 90 | [IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar, 91 | [IO.Path]::DirectorySeparatorChar 92 | ) 93 | 94 | if (($ForceDrive -ne '*') -and 95 | ("ABCDEFGHIJKLMNOPQRSTUVWXYZ".IndexOf(($ForceDrive -as [string]).ToUpperInvariant()) -ge 0)) { 96 | 97 | $i = $normalizedPath.IndexOf([IO.Path]::DirectorySeparatorChar, 2); 98 | $normalizedPath = $ForceDrive + ":" + ( 99 | 100 | $i -lt 0 ? ([IO.Path]::DirectorySeparatorChar) : $normalizedPath.Substring($i) 101 | ) 102 | } 103 | } 104 | else { 105 | 106 | $normalizedPath = $normalizedPath.Replace( 107 | [IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar, 108 | [IO.Path]::DirectorySeparatorChar 109 | ) 110 | } 111 | 112 | # check if path ends with a directory separator 113 | $hasTrailingSeparator = $normalizedPath.EndsWith( 114 | [System.IO.Path]::DirectorySeparatorChar) -or 115 | $normalizedPath.EndsWith([System.IO.Path]::AltDirectorySeparatorChar) 116 | } 117 | 118 | 119 | process { 120 | 121 | # expand home directory if path starts with ~ 122 | if ($normalizedPath.StartsWith("~")) { 123 | 124 | if (($ForceDrive -ne '*') -and 125 | ("ABCDEFGHIJKLMNOPQRSTUVWXYZ".IndexOf(($ForceDrive -as [string]).ToUpperInvariant()) -ge 0)) { 126 | 127 | $i = $normalizedPath.IndexOf([IO.Path]::DirectorySeparatorChar, 1); 128 | $normalizedPath = $ForceDrive + ":" + ( 129 | 130 | $i -lt 0 ? [IO.Path]::DirectorySeparatorChar + "**" + [IO.Path]::DirectorySeparatorChar : ("\**" + $normalizedPath.Substring($i)) 131 | ) 132 | } 133 | else { 134 | 135 | $normalizedPath = Microsoft.PowerShell.Management\Join-Path (Microsoft.PowerShell.Management\Convert-Path ~) ` 136 | $normalizedPath.Substring(1) 137 | } 138 | } 139 | 140 | if ((($normalizedPath.Length -gt 1) -and 141 | ($normalizedPath.Substring(1, 1) -eq ":"))) { 142 | 143 | if (($ForceDrive -ne '*') -and 144 | ("ABCDEFGHIJKLMNOPQRSTUVWXYZ".IndexOf(($ForceDrive -as [string]).ToUpperInvariant()) -ge 0)) { 145 | $i = $normalizedPath.IndexOf([IO.Path]::DirectorySeparatorChar); 146 | $normalizedPath = $ForceDrive + ":" + [IO.Path]::DirectorySeparatorChar + (($i -eq -1 -and $normalizedPath.Length -gt 2) -or $i -eq 2 ? "**" + [IO.Path]::DirectorySeparatorChar : "") + $normalizedPath.Substring(2) 147 | } 148 | else { 149 | 150 | if (($normalizedPath.Length -lt 3) -or ($normalizedPath.Substring(2, 1) -ne [System.IO.Path]::DirectorySeparatorChar)) { 151 | 152 | Microsoft.PowerShell.Management\Push-Location $normalizedPath.Substring(0, 2) 153 | try { 154 | $normalizedPath = "$(Microsoft.PowerShell.Management\Get-Location)$([IO.Path]::DirectorySeparatorChar)$($normalizedPath.Substring(2))" 155 | $normalizedPath = [System.IO.Path]::GetFullPath($normalizedPath) 156 | } 157 | finally { 158 | Microsoft.PowerShell.Management\Pop-Location 159 | } 160 | } 161 | } 162 | } 163 | 164 | # handle absolute paths (drive letter or UNC) 165 | if ($normalizedPath.StartsWith([IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar)) { 166 | 167 | try { 168 | $normalizedPath = [System.IO.Path]::GetFullPath($normalizedPath) 169 | } 170 | catch { 171 | Microsoft.PowerShell.Utility\Write-Verbose "Failed to normalize path, keeping original" 172 | } 173 | } 174 | else { 175 | 176 | if (($ForceDrive -ne '*') -and 177 | ("ABCDEFGHIJKLMNOPQRSTUVWXYZ".IndexOf(($ForceDrive -as [string]).ToUpperInvariant()) -ge 0)) { 178 | 179 | if ($normalizedPath.Length -lt 2 -or $normalizedPath.Substring(1, 1) -ne ":") { 180 | 181 | $newPath = $ForceDrive + ":" + [IO.Path]::DirectorySeparatorChar; 182 | 183 | while ($normalizedPath.StartsWith(".")) { 184 | 185 | $i = $normalizedPath.IndexOf([IO.Path]::DirectorySeparatorChar); 186 | if ($i -lt 0) { 187 | 188 | $normalizedPath = "" 189 | } 190 | else { 191 | 192 | $normalizedPath = $normalizedPath.Substring($i + 1) 193 | } 194 | } 195 | 196 | if ($normalizedPath.StartsWith([IO.Path]::DirectorySeparatorChar)) { 197 | 198 | $newPath += $normalizedPath 199 | } 200 | else { 201 | 202 | $newPath += "**" + [IO.Path]::DirectorySeparatorChar + $normalizedPath 203 | } 204 | 205 | $normalizedPath = $newPath 206 | } 207 | } 208 | 209 | # handle relative paths 210 | try { 211 | $normalizedPath = [System.IO.Path]::GetFullPath( 212 | [System.IO.Path]::Combine($pwd, $normalizedPath)) 213 | } 214 | catch { 215 | $normalizedPath = Microsoft.PowerShell.Management\Convert-Path $normalizedPath 216 | } 217 | } 218 | 219 | # handle directory/file creation if requested 220 | if ($DirectoryMustExist -or $FileMustExist) { 221 | 222 | # get directory path accounting for trailing separator 223 | $directoryPath = if ($hasTrailingSeparator) { 224 | [IO.Path]::TrimEndingDirectorySeparator($normalizedPath) 225 | } 226 | else { 227 | [IO.Path]::TrimEndingDirectorySeparator( 228 | [System.IO.Path]::GetDirectoryName($normalizedPath)) 229 | } 230 | 231 | # create directory if it doesn't exist 232 | if ($DirectoryMustExist -and (-not [IO.Directory]::Exists($directoryPath))) { 233 | 234 | throw "Directory does not exist: $directoryPath" 235 | } 236 | 237 | if ($FileMustExist -and (-not [IO.File]::Exists($normalizedPath))) { 238 | 239 | throw "File does not exist: $normalizedPath" 240 | } 241 | } 242 | 243 | # handle directory/file creation if requested 244 | if ($CreateDirectory -or $CreateFile) { 245 | 246 | # get directory path accounting for trailing separator 247 | $directoryPath = if ($hasTrailingSeparator) { 248 | [IO.Path]::TrimEndingDirectorySeparator($normalizedPath) 249 | } 250 | else { 251 | [IO.Path]::TrimEndingDirectorySeparator( 252 | [System.IO.Path]::GetDirectoryName($normalizedPath)) 253 | } 254 | 255 | # create directory if it doesn't exist 256 | if (-not [IO.Directory]::Exists($directoryPath)) { 257 | $null = [IO.Directory]::CreateDirectory($directoryPath) 258 | Microsoft.PowerShell.Utility\Write-Verbose "Created directory: $directoryPath" 259 | } 260 | } 261 | 262 | # delete existing file if requested 263 | if ($DeleteExistingFile -and [IO.File]::Exists($normalizedPath)) { 264 | 265 | # verify path doesn't point to existing directory 266 | if ([IO.Directory]::Exists($normalizedPath)) { 267 | throw "Cannot create file: Path refers to an existing directory" 268 | } 269 | 270 | if (-not (GenXdev.FileSystem\Remove-ItemWithFallback -Path $normalizedPath)) { 271 | 272 | throw "Failed to delete existing file: $normalizedPath" 273 | } 274 | 275 | Microsoft.PowerShell.Utility\Write-Verbose "Deleted existing file: $normalizedPath" 276 | } 277 | 278 | 279 | # clean up trailing separators except for root paths 280 | while ([IO.Path]::EndsInDirectorySeparator($normalizedPath) -and 281 | $normalizedPath.Length -gt 4) { 282 | $normalizedPath = [IO.Path]::TrimEndingDirectorySeparator($normalizedPath) 283 | } 284 | 285 | # handle file creation if requested 286 | if ($CreateFile) { 287 | 288 | # verify path doesn't point to existing directory 289 | if ([IO.Directory]::Exists($normalizedPath)) { 290 | throw "Cannot create file: Path refers to an existing directory" 291 | } 292 | 293 | 294 | # create empty file if it doesn't exist 295 | if (-not [IO.File]::Exists($normalizedPath)) { 296 | $null = [IO.File]::WriteAllText($normalizedPath, "") 297 | Microsoft.PowerShell.Utility\Write-Verbose "Created empty file: $normalizedPath" 298 | } 299 | } 300 | 301 | return $normalizedPath 302 | } 303 | 304 | end { 305 | } 306 | } 307 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Find-DuplicateFiles.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Find duplicate files across multiple directories based on configurable criteria. 5 | 6 | .DESCRIPTION 7 | Recursively searches specified directories for duplicate files. Files are 8 | considered duplicates if they share the same name and optionally match on size 9 | and modification date. Returns groups of duplicate files for further processing. 10 | 11 | .PARAMETER Paths 12 | Array of directory paths to recursively search for duplicate files. Accepts 13 | pipeline input and wildcard paths. 14 | 15 | .PARAMETER DontCompareSize 16 | When specified, file size is not used as a comparison criterion, only names 17 | are matched. 18 | 19 | .PARAMETER DontCompareModifiedDate 20 | When specified, file modification dates are not used as a comparison criterion. 21 | 22 | .EXAMPLE 23 | Find-DuplicateFiles -Paths "C:\Photos","D:\Backup\Photos" 24 | 25 | .EXAMPLE 26 | "C:\Photos","D:\Backup\Photos" | fdf -DontCompareSize 27 | #> 28 | function Find-DuplicateFiles { 29 | 30 | [CmdletBinding()] 31 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] 32 | [Alias("fdf")] 33 | 34 | param( 35 | ############################################################################### 36 | [Parameter( 37 | Mandatory = $true, 38 | Position = 0, 39 | ValueFromPipeline = $true, 40 | ValueFromPipelineByPropertyName = $true, 41 | HelpMessage = "One or more directory paths to search for duplicates" 42 | )] 43 | [ValidateNotNullOrEmpty()] 44 | [string[]] $Paths, 45 | ############################################################################### 46 | [Parameter( 47 | Mandatory = $false, 48 | Position = 1, 49 | HelpMessage = "Skip file size comparison when grouping duplicates" 50 | )] 51 | [switch] $DontCompareSize, 52 | ############################################################################### 53 | [Parameter( 54 | Mandatory = $false, 55 | Position = 2, 56 | HelpMessage = "Skip last modified date comparison when grouping duplicates" 57 | )] 58 | [switch] $DontCompareModifiedDate 59 | ############################################################################### 60 | ) 61 | 62 | begin { 63 | # convert all input paths to full filesystem paths 64 | $normalizedPaths = @() 65 | $Paths | Microsoft.PowerShell.Core\ForEach-Object { 66 | $normalizedPaths += (GenXdev.FileSystem\Expand-Path $_) 67 | } 68 | 69 | # internal helper function to generate unique comparison key for each file 70 | function Get-FileKey([System.IO.FileInfo]$file) { 71 | 72 | # start with filename as the base identifier 73 | $key = $file.Name 74 | 75 | # include file size in comparison key if enabled 76 | if (-not $DontCompareSize) { 77 | $key += "|$($file.Length)" 78 | } 79 | 80 | # include modification date in comparison key if enabled 81 | if (-not $DontCompareModifiedDate) { 82 | $key += "|$($file.LastWriteTimeUtc.ToString('o'))" 83 | } 84 | 85 | return $key 86 | } 87 | 88 | # initialize high-performance collection for gathering files 89 | $allFiles = [System.Collections.Generic.List[System.IO.FileInfo]]::new() 90 | } 91 | 92 | 93 | process { 94 | 95 | foreach ($path in $normalizedPaths) { 96 | 97 | # verify directory exists before attempting to process 98 | if ([System.IO.Directory]::Exists($path)) { 99 | 100 | Microsoft.PowerShell.Utility\Write-Verbose "Scanning directory for duplicates: $path" 101 | 102 | # use direct .NET IO for faster recursive file enumeration 103 | [System.IO.Directory]::GetFiles($path, "*.*", 104 | [System.IO.SearchOption]::AllDirectories) | 105 | Microsoft.PowerShell.Core\ForEach-Object { 106 | $null = $allFiles.Add([System.IO.FileInfo]::new($_)) 107 | } 108 | } 109 | else { 110 | Microsoft.PowerShell.Utility\Write-Warning "Skipping non-existent directory: $path" 111 | } 112 | } 113 | } 114 | 115 | end { 116 | 117 | # group files by composite key and return only groups with duplicates 118 | $allFiles | 119 | Microsoft.PowerShell.Utility\Group-Object -Property { Get-FileKey $_ } | 120 | Microsoft.PowerShell.Core\Where-Object { $_.Count -gt 1 } | 121 | Microsoft.PowerShell.Core\ForEach-Object { 122 | # create result object for each duplicate group 123 | [PSCustomObject]@{ 124 | FileName = $_.Group[0].Name 125 | Files = $_.Group 126 | } 127 | } 128 | } 129 | } 130 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Invoke-Fasti.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Extracts archive files in the current directory and deletes the originals. 5 | 6 | .DESCRIPTION 7 | Automatically extracts common archive formats (zip, 7z, tar, etc.) found in the 8 | current directory into individual folders named after each archive. After 9 | successful extraction, the original archive files are deleted. Requires 7-Zip 10 | to be installed on the system. 11 | 12 | .EXAMPLE 13 | PS C:\Downloads> Invoke-Fasti 14 | 15 | .EXAMPLE 16 | PS C:\Downloads> fasti 17 | 18 | .NOTES 19 | Supported formats: 7z, zip, rar, tar, iso and many others. 20 | Requires 7-Zip installation (will attempt auto-install via winget if missing). 21 | #> 22 | function Invoke-Fasti { 23 | 24 | [CmdletBinding()] 25 | [Alias("fasti")] 26 | param( 27 | [Parameter( 28 | Mandatory = $false, 29 | HelpMessage = "Enter the password for the encrypted archive(s)" 30 | )] 31 | [string] $Password 32 | ) 33 | 34 | begin { 35 | 36 | # list of supported archive extensions 37 | $extensions = @("*.7z", "*.7z.001", "*.xz", "*.bzip2", "*.gzip", "*.tar", "*.zip", "*.zip.001", 38 | "*.wim", "*.ar", "*.arj", "*.cab", "*.chm", "*.cpio", "*.cramfs", 39 | "*.dmg", "*.ext", "*.fat", "*.gpt", "*.hfs", "*.ihex", "*.iso", 40 | "*.lzh", "*.lzma", "*.mbr", "*.msi", "*.nsis", "*.ntfs", "*.qcow2", 41 | "*.rar", "*.rpm", "*.squashfs", "*.udf", "*.uefi", "*.vdi", "*.vhd", 42 | "*.vmdk", "*.wim", "*.xar", "*.z") 43 | } 44 | 45 | 46 | process { 47 | 48 | # process each archive file found in current directory 49 | Microsoft.PowerShell.Management\Get-ChildItem $extensions -File -ErrorAction SilentlyContinue | 50 | Microsoft.PowerShell.Core\ForEach-Object { 51 | 52 | Microsoft.PowerShell.Utility\Write-Verbose "Processing archive: $($PSItem.Name)" 53 | 54 | # initialize 7zip executable path 55 | $sevenZip = "7z" 56 | 57 | # get archive details 58 | $zipFile = $PSItem.fullname 59 | $name = [system.IO.Path]::GetFileNameWithoutExtension($zipFile) 60 | $path = [System.IO.Path]::GetDirectoryName($zipFile) 61 | $extractPath = [system.Io.Path]::Combine($path, $name) 62 | 63 | # create extraction directory if it doesn't exist 64 | if ([System.IO.Directory]::exists($extractPath) -eq $false) { 65 | 66 | Microsoft.PowerShell.Utility\Write-Verbose "Creating directory: $extractPath" 67 | [System.IO.Directory]::CreateDirectory($extractPath) 68 | } 69 | 70 | # verify 7zip installation or attempt to install it 71 | if ((Microsoft.PowerShell.Core\Get-Command $sevenZip -ErrorAction SilentlyContinue).Length -eq 0) { 72 | 73 | $sevenZip = "${env:ProgramFiles}\7-Zip\7z.exe" 74 | 75 | if (![IO.File]::Exists($sevenZip)) { 76 | 77 | if ((Microsoft.PowerShell.Core\Get-Command winget -ErrorAction SilentlyContinue).Length -eq 0) { 78 | 79 | throw "You need to install 7zip or winget first" 80 | } 81 | 82 | Microsoft.PowerShell.Utility\Write-Verbose "Installing 7-Zip via winget..." 83 | winget install 7zip 84 | 85 | if (![IO.File]::Exists($sevenZip)) { 86 | 87 | throw "You need to install 7-zip" 88 | } 89 | } 90 | } 91 | 92 | # extract archive contents 93 | Microsoft.PowerShell.Utility\Write-Verbose "Extracting to: $extractPath" 94 | $pwparam = if ($Password) { "-p$Password" } else { "" } 95 | if ([string]::IsNullOrWhiteSpace($Password)) { 96 | 97 | & $sevenZip x -y "-o$extractPath" $zipFile 98 | } 99 | else { 100 | 101 | & $sevenZip x -y $pwparam "-o$extractPath" $zipFile 102 | } 103 | 104 | # delete original archive if extraction succeeded 105 | if ($?) { 106 | 107 | try { 108 | Microsoft.PowerShell.Utility\Write-Verbose "Removing original archive: $zipFile" 109 | Microsoft.PowerShell.Management\Remove-Item "$zipFile" -Force -ErrorAction silentlycontinue 110 | } 111 | catch { 112 | Microsoft.PowerShell.Utility\Write-Verbose "Failed to remove original archive" 113 | } 114 | } 115 | } 116 | } 117 | 118 | end { 119 | } 120 | } 121 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Move-ItemWithTracking.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Moves files and directories while preserving filesystem links and references. 5 | 6 | .DESCRIPTION 7 | Uses the Windows MoveFileEx API to move files and directories with link tracking 8 | enabled. This ensures that filesystem references, symbolic links, and hardlinks 9 | are maintained. The function is particularly useful for tools like Git that need 10 | to track file renames. 11 | 12 | .OUTPUTS 13 | System.Boolean 14 | Returns $true if the move operation succeeds, $false otherwise. 15 | 16 | .PARAMETER Path 17 | The source path of the file or directory to move. Accepts pipeline input and 18 | aliases to FullName for compatibility with Get-ChildItem output. 19 | 20 | .PARAMETER Destination 21 | The target path where the file or directory should be moved to. Must be a valid 22 | filesystem path. 23 | 24 | .PARAMETER Force 25 | If specified, allows overwriting an existing file or directory at the 26 | destination path. 27 | 28 | .EXAMPLE 29 | Move-ItemWithTracking -Path "C:\temp\oldfile.txt" -Destination "D:\newfile.txt" 30 | # Moves a file while preserving any existing filesystem links 31 | 32 | .EXAMPLE 33 | "C:\temp\olddir" | Move-ItemWithTracking -Destination "D:\newdir" -Force 34 | # Moves a directory, overwriting destination if it exists 35 | #> 36 | function Move-ItemWithTracking { 37 | 38 | [CmdletBinding(SupportsShouldProcess)] 39 | [OutputType([System.Boolean])] 40 | param( 41 | ######################################################################## 42 | [Parameter( 43 | Mandatory = $true, 44 | Position = 0, 45 | ValueFromPipeline = $true, 46 | ValueFromPipelineByPropertyName = $true, 47 | HelpMessage = "Source path of file/directory to move" 48 | )] 49 | [ValidateNotNullOrEmpty()] 50 | [Alias("FullName")] 51 | [string]$Path, 52 | ######################################################################## 53 | [Parameter( 54 | Mandatory = $true, 55 | Position = 1, 56 | HelpMessage = "Destination path to move to" 57 | )] 58 | [ValidateNotNullOrEmpty()] 59 | [string]$Destination, 60 | ######################################################################## 61 | [Parameter( 62 | HelpMessage = "Overwrite destination if it exists" 63 | )] 64 | [switch]$Force 65 | ######################################################################## 66 | ) 67 | 68 | begin { 69 | 70 | # define the native windows api function signature for moving files 71 | $signature = @" 72 | [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 73 | public static extern bool MoveFileEx( 74 | string lpExistingFileName, 75 | string lpNewFileName, 76 | int dwFlags); 77 | "@ 78 | 79 | try { 80 | # load the native windows api function into the current session 81 | $win32 = Microsoft.PowerShell.Utility\Add-Type -MemberDefinition $signature ` 82 | -Name "MoveFileExUtils" ` 83 | -Namespace Win32 ` 84 | -PassThru 85 | } 86 | catch { 87 | Microsoft.PowerShell.Utility\Write-Error "Failed to load Win32 API: $_" 88 | return $false 89 | } 90 | 91 | # configure move operation flags for link tracking and overwrite handling 92 | $moveFileWriteThrough = 0x8 # ensures the move completes before returning 93 | $moveFileReplaceExisting = 0x1 # allows overwriting existing files 94 | 95 | # combine flags based on whether Force parameter was specified 96 | $flags = $moveFileWriteThrough 97 | if ($Force) { 98 | $flags = $flags -bor $moveFileReplaceExisting 99 | } 100 | } 101 | 102 | 103 | process { 104 | try { 105 | # convert relative paths to absolute filesystem paths 106 | $fullSourcePath = GenXdev.FileSystem\Expand-Path $Path 107 | $fullDestPath = GenXdev.FileSystem\Expand-Path $Destination 108 | 109 | # verify the source path exists before attempting move 110 | if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $fullSourcePath) { 111 | 112 | # check if user wants to proceed with the operation 113 | if ($PSCmdlet.ShouldProcess($fullSourcePath, 114 | "Move to $fullDestPath")) { 115 | 116 | Microsoft.PowerShell.Utility\Write-Verbose "Moving $fullSourcePath to $fullDestPath" 117 | 118 | # perform the move operation with link tracking 119 | $result = $win32::MoveFileEx($fullSourcePath, 120 | $fullDestPath, $flags) 121 | 122 | if (-not $result) { 123 | # get detailed error information on failure 124 | $errorCode = [System.Runtime.InteropServices.Marshal]:: ` 125 | GetLastWin32Error() 126 | throw "Move failed from '$fullSourcePath' to " + 127 | "'$fullDestPath'. Error: $errorCode" 128 | } 129 | 130 | Microsoft.PowerShell.Utility\Write-Verbose "Move completed successfully with link tracking" 131 | return $true 132 | } 133 | } 134 | else { 135 | Microsoft.PowerShell.Utility\Write-Warning "Source path not found: $fullSourcePath" 136 | return $false 137 | } 138 | } 139 | catch { 140 | Microsoft.PowerShell.Utility\Write-Error $_ 141 | return $false 142 | } 143 | } 144 | 145 | end { 146 | } 147 | } 148 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Move-ToRecycleBin.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Moves files and directories to the Windows Recycle Bin safely. 5 | 6 | .DESCRIPTION 7 | Safely moves files or directories to the recycle bin using the Windows Shell API, 8 | even if they are currently in use. The function uses the Shell.Application COM 9 | object to perform the operation, ensuring proper recycling behavior and undo 10 | capability. 11 | 12 | .PARAMETER Path 13 | One or more paths to files or directories that should be moved to the recycle 14 | bin. Accepts pipeline input and wildcards. The paths must exist and be 15 | accessible. 16 | 17 | .EXAMPLE 18 | Move-ToRecycleBin -Path "C:\temp\old-report.txt" 19 | # Moves a single file to the recycle bin 20 | 21 | .EXAMPLE 22 | "file1.txt","file2.txt" | recycle 23 | # Moves multiple files using pipeline and alias 24 | #> 25 | function Move-ToRecycleBin { 26 | 27 | [CmdletBinding(SupportsShouldProcess)] 28 | [OutputType([bool])] 29 | [Alias("recycle")] 30 | param( 31 | ######################################################################## 32 | [Parameter( 33 | Mandatory = $true, 34 | Position = 0, 35 | ValueFromPipeline = $true, 36 | ValueFromPipelineByPropertyName = $true, 37 | HelpMessage = "Specify the path(s) to move to the recycle bin" 38 | )] 39 | [ValidateNotNullOrEmpty()] 40 | [Alias("FullName")] 41 | [string[]]$Path 42 | ######################################################################## 43 | ) 44 | 45 | begin { 46 | # track overall success of operations 47 | $success = $true 48 | 49 | # initialize shell automation object for recycle bin operations 50 | $shellObj = $null 51 | try { 52 | $shellObj = Microsoft.PowerShell.Utility\New-Object -ComObject Shell.Application 53 | Microsoft.PowerShell.Utility\Write-Verbose "Created Shell.Application COM object for recycle operations" 54 | } 55 | catch { 56 | Microsoft.PowerShell.Utility\Write-Error "Failed to create Shell.Application COM object: $_" 57 | return $false 58 | } 59 | } 60 | 61 | 62 | process { 63 | 64 | foreach ($itemPath in $Path) { 65 | 66 | # convert relative or shorthand paths to full filesystem paths 67 | $fullPath = GenXdev.FileSystem\Expand-Path $itemPath 68 | Microsoft.PowerShell.Utility\Write-Verbose "Processing path: $fullPath" 69 | 70 | try { 71 | # check if the target path actually exists before attempting to recycle 72 | if ([System.IO.File]::Exists($fullPath) -or ` 73 | [System.IO.Directory]::Exists($fullPath)) { 74 | 75 | # confirm the recycle operation with the user 76 | if ($PSCmdlet.ShouldProcess($fullPath, "Move to Recycle Bin")) { 77 | 78 | # split the path into directory and filename for shell operation 79 | $dirName = [System.IO.Path]::GetDirectoryName($fullPath) 80 | $fileName = [System.IO.Path]::GetFileName($fullPath) 81 | 82 | # get shell folder object for the directory containing the item 83 | $folderObj = $shellObj.Namespace($dirName) 84 | $fileObj = $folderObj.ParseName($fileName) 85 | 86 | # perform the recycle operation using shell verbs 87 | $fileObj.InvokeVerb("delete") 88 | Microsoft.PowerShell.Utility\Write-Verbose "Successfully moved to recycle bin: $fullPath" 89 | } 90 | } 91 | else { 92 | Microsoft.PowerShell.Utility\Write-Warning "Path not found: $fullPath" 93 | $success = $false 94 | } 95 | } 96 | catch { 97 | Microsoft.PowerShell.Utility\Write-Error "Failed to recycle $fullPath : $_" 98 | $success = $false 99 | } 100 | } 101 | } 102 | 103 | end { 104 | 105 | # cleanup the COM object to prevent resource leaks 106 | try { 107 | [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shellObj) | ` 108 | Microsoft.PowerShell.Core\Out-Null 109 | Microsoft.PowerShell.Utility\Write-Verbose "Released Shell.Application COM object" 110 | } 111 | catch { 112 | Microsoft.PowerShell.Utility\Write-Warning "Failed to release COM object: $_" 113 | } 114 | 115 | return $success 116 | } 117 | } 118 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Remove-AllItems.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Recursively removes all content from a directory with advanced error handling. 5 | 6 | .DESCRIPTION 7 | Safely removes all files and subdirectories within a specified directory using 8 | a reverse-order deletion strategy to handle deep paths. Includes WhatIf support, 9 | verbose logging, and fallback deletion methods for locked files. 10 | 11 | .PARAMETER Path 12 | The directory path to clear. Can be relative or absolute path. Will be normalized 13 | and expanded before processing. 14 | 15 | .PARAMETER DeleteFolder 16 | When specified, also removes the root directory specified by Path after clearing 17 | its contents. 18 | 19 | .PARAMETER WhatIf 20 | Shows what would happen if the cmdlet runs. The cmdlet is not run. 21 | 22 | .EXAMPLE 23 | Remove-AllItems -Path "C:\Temp\BuildOutput" -DeleteFolder -Verbose 24 | 25 | .EXAMPLE 26 | sdel ".\temp" -DeleteFolder 27 | #> 28 | function Remove-AllItems { 29 | 30 | [CmdletBinding(SupportsShouldProcess)] 31 | [Alias("sdel")] 32 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] 33 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] 34 | 35 | param( 36 | ############################################################################### 37 | [Parameter( 38 | Mandatory = $true, 39 | Position = 0, 40 | ValueFromPipeline = $true, 41 | ValueFromPipelineByPropertyName = $true, 42 | HelpMessage = "The directory path to clear" 43 | )] 44 | [ValidateNotNullOrEmpty()] 45 | [Alias("FullName")] 46 | [string] $Path, 47 | ############################################################################### 48 | [Parameter( 49 | Mandatory = $false, 50 | Position = 1, 51 | HelpMessage = "Also delete the root folder supplied with the Path parameter" 52 | )] 53 | [switch] $DeleteFolder 54 | ############################################################################### 55 | ) 56 | 57 | begin { 58 | 59 | # preserve original preference settings for restoration in end block 60 | $originalVerbosePreference = $VerbosePreference 61 | $originalWhatIfPreference = $WhatIfPreference 62 | 63 | try { 64 | # convert relative or shorthand paths to full filesystem paths 65 | $Path = GenXdev.FileSystem\Expand-Path $Path 66 | Microsoft.PowerShell.Utility\Write-Verbose "Normalized path: $Path" 67 | 68 | # ensure verbose output is enabled during WhatIf operations 69 | if ($WhatIfPreference -or $WhatIf) { 70 | $VerbosePreference = "Continue" 71 | } 72 | } 73 | catch { 74 | # restore original whatif setting before propagating error 75 | $WhatIfPreference = $originalWhatIfPreference 76 | throw 77 | } 78 | } 79 | 80 | 81 | process { 82 | try { 83 | # skip processing if target directory doesn't exist 84 | if (![System.IO.Directory]::Exists($Path)) { 85 | Microsoft.PowerShell.Utility\Write-Verbose "Directory does not exist: $Path" 86 | return 87 | } 88 | 89 | Microsoft.PowerShell.Utility\Write-Verbose "Processing directory: $Path" 90 | 91 | # delete files first, in reverse order to handle nested paths 92 | [System.IO.Directory]::GetFiles($Path, "*.*", ` 93 | [System.IO.SearchOption]::AllDirectories) | 94 | Microsoft.PowerShell.Utility\Sort-Object -Descending | 95 | Microsoft.PowerShell.Core\ForEach-Object { 96 | $filePath = $_ 97 | if ($PSCmdlet.ShouldProcess($filePath, "Remove file")) { 98 | $null = GenXdev.FileSystem\Remove-ItemWithFallback -Path $filePath 99 | } 100 | } 101 | 102 | # delete directories after files, also in reverse order 103 | [System.IO.Directory]::GetDirectories($Path, "*", ` 104 | [System.IO.SearchOption]::AllDirectories) | 105 | Microsoft.PowerShell.Utility\Sort-Object -Descending | 106 | Microsoft.PowerShell.Core\ForEach-Object { 107 | $dirPath = $_ 108 | if ($PSCmdlet.ShouldProcess($dirPath, "Remove directory")) { 109 | try { 110 | [System.IO.Directory]::Delete($dirPath, $true) 111 | Microsoft.PowerShell.Utility\Write-Verbose "Removed directory: $dirPath" 112 | } 113 | catch { 114 | Microsoft.PowerShell.Utility\Write-Warning "Failed to delete directory: $dirPath" 115 | } 116 | } 117 | } 118 | 119 | # optionally remove the root directory itself 120 | if ($DeleteFolder) { 121 | if ($PSCmdlet.ShouldProcess($Path, "Remove root directory")) { 122 | try { 123 | [System.IO.Directory]::Delete($Path, $true) 124 | Microsoft.PowerShell.Utility\Write-Verbose "Removed root directory: $Path" 125 | } 126 | catch { 127 | try { 128 | $null = GenXdev.FileSystem\Remove-ItemWithFallback -Path $Path 129 | } 130 | catch {} 131 | } 132 | } 133 | } 134 | } 135 | catch { 136 | # restore original whatif setting before propagating error 137 | $WhatIfPreference = $originalWhatIfPreference 138 | throw 139 | } 140 | } 141 | 142 | end { 143 | # restore original preference settings 144 | $WhatIfPreference = $originalWhatIfPreference 145 | } 146 | } 147 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Remove-ItemWithFallback.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Removes files or directories with multiple fallback mechanisms for reliable deletion. 5 | 6 | .DESCRIPTION 7 | This function provides a robust way to delete files and directories by attempting 8 | multiple deletion methods in sequence: 9 | 1. Direct deletion via System.IO methods for best performance 10 | 2. PowerShell provider-aware Remove-Item cmdlet as fallback 11 | 3. Mark for deletion on next system reboot if other methods fail 12 | This ensures maximum reliability when removing items across different providers. 13 | 14 | .PARAMETER Path 15 | The file or directory path to remove. Can be a filesystem path or provider path. 16 | Accepts pipeline input and wildcards. Must be a valid, non-empty path. 17 | 18 | .PARAMETER CountRebootDeletionAsSuccess 19 | If specified, the function returns $true when a file is successfully marked for deletion on reboot. 20 | By default ($false), the function returns $false in this scenario. 21 | 22 | .EXAMPLE 23 | Remove-ItemWithFallback -Path "C:\temp\myfile.txt" 24 | Attempts to remove the file using all available methods. 25 | 26 | .EXAMPLE 27 | "C:\temp\mydir" | rif 28 | Uses the alias 'rif' to remove a directory through the pipeline. 29 | #> 30 | function Remove-ItemWithFallback { 31 | 32 | [CmdletBinding(SupportsShouldProcess = $true)] 33 | [OutputType([bool])] 34 | [Alias("rmf")] 35 | 36 | param( 37 | ######################################################################## 38 | [Parameter( 39 | Mandatory = $true, 40 | Position = 0, 41 | ValueFromPipeline = $true, 42 | ValueFromPipelineByPropertyName = $true, 43 | HelpMessage = "The path to the item to remove" 44 | )] 45 | [ValidateNotNullOrEmpty()] 46 | [Alias("FullName")] 47 | [string]$Path, 48 | ######################################################################## 49 | [Parameter( 50 | Mandatory = $false 51 | )] 52 | [switch]$CountRebootDeletionAsSuccess = $false 53 | ######################################################################## 54 | ) 55 | 56 | begin { 57 | 58 | # convert relative or provider paths to full filesystem paths 59 | $Path = GenXdev.FileSystem\Expand-Path $Path 60 | } 61 | 62 | process { 63 | try { 64 | # verify item exists and get its provider information 65 | $item = Microsoft.PowerShell.Management\Get-Item -LiteralPath $Path ` 66 | -ErrorAction Stop 67 | 68 | # handle filesystem items with direct IO methods for best performance 69 | if ($item.PSProvider.Name -eq 'FileSystem') { 70 | 71 | if ($PSCmdlet.ShouldProcess($Path, "Remove item")) { 72 | 73 | # try fastest method first - direct file deletion 74 | if ([System.IO.File]::Exists($Path)) { 75 | try { 76 | [System.IO.File]::Delete($Path) 77 | Microsoft.PowerShell.Utility\Write-Verbose "Successfully removed file using IO: $Path" 78 | return $true 79 | } 80 | catch { 81 | # If ErrorAction Stop was specified, immediately rethrow 82 | if (($PSBoundParameters.ContainsKey('ErrorAction') -and $PSBoundParameters['ErrorAction'] -eq 'Stop') -or 83 | $ErrorActionPreference -eq 'Stop') { 84 | throw 85 | } 86 | # Otherwise, fall through to next deletion method 87 | Microsoft.PowerShell.Utility\Write-Verbose "Direct file deletion failed: $_" 88 | # Don't rethrow here - let the code flow to the next deletion method 89 | } 90 | } 91 | 92 | # handle directory deletion with recursive option 93 | if ([System.IO.Directory]::Exists($Path)) { 94 | try { 95 | [System.IO.Directory]::Delete($Path, $true) 96 | Microsoft.PowerShell.Utility\Write-Verbose "Successfully removed directory using IO: $Path" 97 | return $true 98 | } 99 | catch { 100 | # If ErrorAction Stop was specified, immediately rethrow 101 | if (($PSBoundParameters.ContainsKey('ErrorAction') -and $PSBoundParameters['ErrorAction'] -eq 'Stop') -or 102 | $ErrorActionPreference -eq 'Stop') { 103 | throw 104 | } 105 | # Otherwise, fall through to next deletion method 106 | Microsoft.PowerShell.Utility\Write-Verbose "Direct directory deletion failed: $_" 107 | # Don't rethrow here - let the code flow to the next deletion method 108 | } 109 | } 110 | } 111 | } 112 | else { 113 | # non-filesystem items need provider-specific handling 114 | if ($PSCmdlet.ShouldProcess($Path, "Remove via provider")) { 115 | Microsoft.PowerShell.Management\Remove-Item -LiteralPath $Path ` 116 | -Force 117 | Microsoft.PowerShell.Utility\Write-Verbose "Removed item via provider: $Path" 118 | return $true 119 | } 120 | } 121 | } 122 | catch { 123 | Microsoft.PowerShell.Utility\Write-Verbose "Standard deletion failed, attempting boot-time removal..." 124 | 125 | # Check if ErrorAction Stop was specified via parameter or preference variable 126 | if (($PSBoundParameters.ContainsKey('ErrorAction') -and $PSBoundParameters['ErrorAction'] -eq 'Stop') -or 127 | $ErrorActionPreference -eq 'Stop') { 128 | # Rethrow the original exception immediately without trying fallback methods 129 | throw 130 | } 131 | 132 | # Only try boot-time deletion for filesystem items and verify path exists first 133 | if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $Path -ErrorAction SilentlyContinue) { 134 | $providerInfo = (Microsoft.PowerShell.Management\Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue).PSProvider 135 | 136 | if ($null -ne $providerInfo -and $providerInfo.Name -eq 'FileSystem') { 137 | # last resort - mark for deletion on next boot 138 | if (GenXdev.FileSystem\Remove-OnReboot -Path $Path) { 139 | Microsoft.PowerShell.Utility\Write-Verbose "Marked for deletion on next reboot: $Path" 140 | return [bool]$CountRebootDeletionAsSuccess 141 | } 142 | } 143 | } 144 | 145 | Microsoft.PowerShell.Utility\Write-Warning "All deletion methods failed for: $Path" 146 | Microsoft.PowerShell.Utility\Write-Error $_.Exception.Message 147 | return $false 148 | } 149 | } 150 | 151 | end { 152 | } 153 | } 154 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Remove-OnReboot.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | 3 | <# 4 | .SYNOPSIS 5 | Marks files or directories for deletion during the next system boot. 6 | 7 | .DESCRIPTION 8 | This function uses the Windows API to mark files for deletion on next boot. 9 | It handles locked files by first attempting to rename them to temporary names 10 | and tracks all moves to maintain file system integrity. If renaming fails, 11 | the -MarkInPlace parameter can be used to mark files in their original location. 12 | 13 | .PARAMETER Path 14 | One or more file or directory paths to mark for deletion. Accepts pipeline input. 15 | 16 | .PARAMETER MarkInPlace 17 | If specified, marks files for deletion in their original location when renaming 18 | fails. This is useful for locked files that cannot be renamed. 19 | 20 | .EXAMPLE 21 | Remove-OnReboot -Path "C:\temp\locked-file.txt" 22 | 23 | .EXAMPLE 24 | "file1.txt","file2.txt" | Remove-OnReboot -MarkInPlace 25 | #> 26 | function Remove-OnReboot { 27 | 28 | [CmdletBinding(SupportsShouldProcess)] 29 | [OutputType([bool])] 30 | param( 31 | ############################################################################### 32 | [Parameter( 33 | Mandatory = $true, 34 | Position = 0, 35 | ValueFromPipeline = $true, 36 | HelpMessage = "Path(s) to files/directories to mark for deletion" 37 | )] 38 | [ValidateNotNullOrEmpty()] 39 | [Alias("FullName")] 40 | [string[]]$Path, 41 | ############################################################################### 42 | [Parameter( 43 | Mandatory = $false, 44 | HelpMessage = "Marks files for deletion without renaming" 45 | )] 46 | [switch]$MarkInPlace 47 | ############################################################################### 48 | ) 49 | 50 | begin { 51 | # registry location storing pending file operations 52 | $regKey = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" 53 | $regName = "PendingFileRenameOperations" 54 | 55 | # get existing pending renames or initialize empty array 56 | try { 57 | $pendingRenames = @(Microsoft.PowerShell.Management\Get-ItemProperty -Path $regKey ` 58 | -Name $regName -ErrorAction SilentlyContinue).$regName 59 | } 60 | catch { 61 | $pendingRenames = @() 62 | } 63 | 64 | if ($null -eq $pendingRenames) { 65 | $pendingRenames = @() 66 | } 67 | } 68 | 69 | 70 | process { 71 | 72 | try { 73 | foreach ($item in $Path) { 74 | $fullPath = GenXdev.FileSystem\Expand-Path $item 75 | 76 | if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $fullPath) { 77 | if ($PSCmdlet.ShouldProcess($fullPath, "Mark for deletion on reboot")) { 78 | 79 | try { 80 | # attempt immediate deletion first 81 | Microsoft.PowerShell.Management\Remove-Item -LiteralPath $fullPath -Force -ErrorAction Stop 82 | Microsoft.PowerShell.Utility\Write-Verbose "Successfully deleted: $fullPath" 83 | continue 84 | } 85 | catch { 86 | Microsoft.PowerShell.Utility\Write-Verbose "Direct deletion failed, attempting rename..." 87 | 88 | try { 89 | # create hidden temporary file name 90 | $newName = "." + [System.Guid]::NewGuid().ToString() 91 | $newPath = [System.IO.Path]::Combine($dir, $newName) 92 | 93 | # rename and hide the file 94 | Microsoft.PowerShell.Management\Rename-Item -Path $fullPath -NewName $newName -Force ` 95 | -ErrorAction Stop 96 | $file = Microsoft.PowerShell.Management\Get-Item -LiteralPath $newPath -Force 97 | $file.Attributes = $file.Attributes -bor ` 98 | [System.IO.FileAttributes]::Hidden -bor ` 99 | [System.IO.FileAttributes]::System 100 | 101 | Microsoft.PowerShell.Utility\Write-Verbose "Renamed to hidden system file: $newPath" 102 | 103 | # add to pending renames with windows api path format 104 | $sourcePath = "\??\" + $newPath 105 | $pendingRenames += $sourcePath 106 | $pendingRenames += "" 107 | 108 | Microsoft.PowerShell.Utility\Write-Verbose "Marked for deletion on reboot: $newPath" 109 | } 110 | catch { 111 | if ($MarkInPlace) { 112 | Microsoft.PowerShell.Utility\Write-Verbose "Marking original file for deletion" 113 | $sourcePath = "\??\" + $fullPath 114 | $pendingRenames += $sourcePath 115 | $pendingRenames += "" 116 | } 117 | else { 118 | Microsoft.PowerShell.Utility\Write-Error "Failed to rename $($fullPath): $_" 119 | continue 120 | } 121 | } 122 | } 123 | } 124 | } 125 | else { 126 | Microsoft.PowerShell.Utility\Write-Warning "Path not found: $fullPath" 127 | } 128 | } 129 | 130 | if ($pendingRenames.Count -gt 0) { 131 | # save pending operations to registry 132 | Microsoft.PowerShell.Management\Set-ItemProperty -Path $regKey -Name $regName ` 133 | -Value $pendingRenames -Type MultiString 134 | return $true 135 | } 136 | } 137 | catch { 138 | Microsoft.PowerShell.Utility\Write-Error "Failed to set pending file operations: $_" 139 | return $false 140 | } 141 | 142 | return $true 143 | } 144 | 145 | end { 146 | } 147 | } 148 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Rename-InProject.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | <# 3 | .SYNOPSIS 4 | Performs case-sensitive text replacement throughout a project directory. 5 | 6 | .DESCRIPTION 7 | Recursively searches through files and directories in a project to perform text 8 | replacements. Handles both file/directory names and file contents. Skips common 9 | binary files and repository folders (.git, .svn) to avoid corruption. Uses UTF-8 10 | encoding without BOM for file operations. 11 | 12 | .PARAMETER Source 13 | The directory, filepath, or directory+searchmask to process. Defaults to current 14 | directory if not specified. 15 | 16 | .PARAMETER FindText 17 | The case-sensitive text pattern to search for in filenames and content. 18 | 19 | .PARAMETER ReplacementText 20 | The text to replace all instances of FindText with. 21 | 22 | .PARAMETER WhatIf 23 | Shows what changes would occur without actually making them. 24 | 25 | .EXAMPLE 26 | Rename-InProject -Source .\src\*.js -FindText "oldName" ` 27 | -ReplacementText "newName" 28 | 29 | .EXAMPLE 30 | rip . "MyClass" "MyNewClass" -WhatIf 31 | #> 32 | function Rename-InProject { 33 | 34 | [CmdletBinding(SupportsShouldProcess = $true)] 35 | [Alias("rip")] 36 | param( 37 | ######################################################################## 38 | [Parameter( 39 | Mandatory = $false, 40 | Position = 0, 41 | ValueFromPipeline = $false, 42 | HelpMessage = "The directory, filepath, or directory+searchmask" 43 | )] 44 | [Alias("src", "s")] 45 | [PSDefaultValue(Value = ".\")] 46 | [string] $Source, 47 | ######################################################################## 48 | [Parameter( 49 | Mandatory = $true, 50 | Position = 1, 51 | ValueFromPipeline = $false, 52 | HelpMessage = "The text to find (case sensitive)" 53 | )] 54 | [Alias("find", "what", "from")] 55 | [ValidateNotNullOrEmpty()] 56 | [string] $FindText, 57 | ######################################################################## 58 | [Parameter( 59 | Mandatory = $true, 60 | Position = 2, 61 | ValueFromPipeline = $false, 62 | HelpMessage = "The text to replace matches with" 63 | )] 64 | [Alias("into", "txt", "to")] 65 | [ValidateNotNull()] 66 | [string] $ReplacementText 67 | ######################################################################## 68 | ) 69 | 70 | begin { 71 | 72 | try { 73 | # normalize path and extract search pattern if specified 74 | $sourcePath = GenXdev.FileSystem\Expand-Path $Source 75 | $searchPattern = "*" 76 | 77 | # split source into path and pattern if not a directory 78 | if (![System.IO.Directory]::Exists($sourcePath)) { 79 | 80 | $searchPattern = [System.IO.Path]::GetFileName($sourcePath) 81 | $sourcePath = [System.IO.Path]::GetDirectoryName($sourcePath) 82 | 83 | if (![System.IO.Directory]::Exists($sourcePath)) { 84 | throw "Source directory not found: $sourcePath" 85 | } 86 | } 87 | 88 | Microsoft.PowerShell.Utility\Write-Verbose "Processing source path: $sourcePath" 89 | Microsoft.PowerShell.Utility\Write-Verbose "Using search pattern: $searchPattern" 90 | 91 | # extensions to skip to avoid corrupting binary files 92 | $skipExtensions = @( 93 | ".jpg", ".jpeg", ".gif", ".bmp", ".png", ".tiff", 94 | ".exe", ".dll", ".pdb", ".so", 95 | ".wav", ".mp3", ".avi", ".mkv", ".wmv", 96 | ".tar", ".7z", ".zip", ".rar", ".apk", ".ipa", 97 | ".cer", ".crt", ".pkf", ".db", ".bin" 98 | ) 99 | } 100 | catch { 101 | throw 102 | } 103 | } 104 | 105 | 106 | process { 107 | 108 | try { 109 | # recursive function to get all project files excluding repos 110 | function Get-ProjectFiles { 111 | 112 | [CmdletBinding()] 113 | [OutputType([System.Collections.Generic.List[string]])] 114 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( 115 | "PSUseSingularNouns", 116 | "Get-ProjectFiles" 117 | )] 118 | param( 119 | [string] $Dir, 120 | [string] $Mask 121 | ) 122 | 123 | $result = [System.Collections.Generic.List[string]]::new() 124 | 125 | # skip version control directories 126 | if ([IO.Path]::GetFileName($Dir) -in @(".svn", ".git")) { 127 | return $result 128 | } 129 | 130 | # collect matching files in current directory 131 | [IO.Directory]::GetFiles($Dir, $Mask) | Microsoft.PowerShell.Core\ForEach-Object { 132 | $null = $result.Add($_) 133 | } 134 | 135 | # recursively process subdirectories 136 | [IO.Directory]::GetDirectories($Dir, "*") | Microsoft.PowerShell.Core\ForEach-Object { 137 | if ([IO.Path]::GetFileName($_) -notin @(".svn", ".git")) { 138 | $null = Get-ProjectFiles $_ $Mask | Microsoft.PowerShell.Core\ForEach-Object { 139 | $null = $result.Add($_) 140 | } 141 | } 142 | } 143 | 144 | return $result 145 | } 146 | 147 | # process files in reverse order to handle renames safely 148 | Get-ProjectFiles -dir $sourcePath -mask $searchPattern | 149 | Microsoft.PowerShell.Utility\Sort-Object -Descending | 150 | Microsoft.PowerShell.Core\ForEach-Object { 151 | 152 | $filePath = $_ 153 | $extension = [IO.Path]::GetExtension($filePath).ToLower() 154 | 155 | # only process text files 156 | if ($extension -notin $skipExtensions) { 157 | 158 | try { 159 | Microsoft.PowerShell.Utility\Write-Verbose "Processing file: $filePath" 160 | 161 | # replace text in file contents 162 | $content = [IO.File]::ReadAllText($filePath, 163 | [Text.Encoding]::UTF8) 164 | $newContent = $content.Replace($FindText, $ReplacementText) 165 | 166 | if ($content -ne $newContent) { 167 | if ($PSCmdlet.ShouldProcess($filePath, 168 | "Replace content")) { 169 | 170 | $utf8 = [Text.UTF8Encoding]::new($false) 171 | [IO.File]::WriteAllText($filePath, $newContent, 172 | $utf8) 173 | 174 | Microsoft.PowerShell.Utility\Write-Verbose "Updated content in: $filePath" 175 | } 176 | } 177 | } 178 | catch { 179 | Microsoft.PowerShell.Utility\Write-Warning "Failed to update content in: $filePath`n$_" 180 | } 181 | 182 | # handle filename changes 183 | $oldName = [IO.Path]::GetFileName($filePath) 184 | $newName = $oldName.Replace($FindText, $ReplacementText) 185 | 186 | if ($oldName -ne $newName) { 187 | $newPath = [IO.Path]::Combine( 188 | [IO.Path]::GetDirectoryName($filePath), 189 | $newName) 190 | 191 | if ($PSCmdlet.ShouldProcess($filePath, "Rename file")) { 192 | try { 193 | 194 | if ("$filePath".ToLowerInvariant() -eq "$newPath".ToLowerInvariant()) { 195 | 196 | $newPath = "$newPath.$([DateTime]::Now.Ticks).tmp" 197 | $null = GenXdev.FileSystem\Move-ItemWithTracking -Path $filePath ` 198 | -Destination "$newPath.tmp12389" 199 | Microsoft.PowerShell.Utility\Write-Verbose "Renamed file: $filePath -> $newPath" 200 | $filePath = $newPath 201 | } 202 | 203 | $null = GenXdev.FileSystem\Move-ItemWithTracking -Path $filePath ` 204 | -Destination $newPath 205 | Microsoft.PowerShell.Utility\Write-Verbose "Renamed file: $filePath -> $newPath" 206 | } 207 | catch { 208 | Microsoft.PowerShell.Utility\Write-Warning "Failed to rename file: $filePath`n$_" 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | # process directories in reverse order 216 | Microsoft.PowerShell.Management\Get-ChildItem -Path $sourcePath -Directory -Recurse | 217 | Microsoft.PowerShell.Utility\Sort-Object -Descending | 218 | Microsoft.PowerShell.Core\Where-Object { 219 | $_.FullName -notlike "*\.git\*" -and 220 | $_.FullName -notlike "*\.svn\*" 221 | } | 222 | Microsoft.PowerShell.Core\ForEach-Object { 223 | 224 | $dir = $_ 225 | $oldName = $dir.Name 226 | $newName = $oldName.Replace($FindText, $ReplacementText) 227 | 228 | if ($oldName -ne $newName) { 229 | $newPath = GenXdev.FileSystem\Expand-Path ( 230 | [IO.Path]::Combine($dir.Parent.FullName, $newName)) 231 | 232 | if ($PSCmdlet.ShouldProcess($dir.FullName, 233 | "Rename directory")) { 234 | 235 | if ([IO.Directory]::Exists($newPath)) { 236 | # merge directories if target exists 237 | GenXdev.FileSystem\Start-RoboCopy -Source $dir.FullName ` 238 | -DestinationDirectory $newPath -Move 239 | $null = GenXdev.FileSystem\Remove-AllItems ($dir.FullName) -DeleteFolder 240 | Microsoft.PowerShell.Utility\Write-Verbose "Merged directory: $($dir.FullName) -> $newPath" 241 | } 242 | else { 243 | try { 244 | $null = GenXdev.FileSystem\Move-ItemWithTracking -Path $dir.FullName ` 245 | -Destination $newPath 246 | Microsoft.PowerShell.Utility\Write-Verbose "Renamed directory: $($dir.FullName) -> $newPath" 247 | } 248 | catch { 249 | Microsoft.PowerShell.Utility\Write-Warning "Failed to rename directory: $($dir.FullName)`n$_" 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | catch { 257 | throw 258 | } 259 | } 260 | 261 | end { 262 | } 263 | } 264 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/Start-RoboCopy.ps1: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | <# 3 | .SYNOPSIS 4 | Provides a PowerShell wrapper for Microsoft's Robust Copy (RoboCopy) utility. 5 | 6 | .DESCRIPTION 7 | A comprehensive wrapper for the RoboCopy command-line utility that provides 8 | robust file and directory copying capabilities. This function exposes RoboCopy's 9 | extensive feature set through PowerShell-friendly parameters while maintaining 10 | most of its powerful functionality. 11 | 12 | Key Features: 13 | - Directory synchronization with mirror options 14 | - Support for extra long pathnames (>256 characters) 15 | - Restartable mode for resilient copying 16 | - Security settings preservation 17 | - Advanced file attribute handling 18 | - Symbolic link and junction point management 19 | - Monitor mode for continuous synchronization 20 | - Performance optimization for large files 21 | - Network compression support 22 | - Recovery mode for failing devices 23 | 24 | .PARAMETER Source 25 | The source directory, file path, or directory with search mask to copy from. 26 | 27 | .PARAMETER DestinationDirectory 28 | The destination directory to place the copied files and directories into. 29 | If this directory does not exist yet, all missing directories will be created. 30 | Default value = `.\` 31 | 32 | .PARAMETER FileMask 33 | Optional searchmask for selecting the files that need to be copied. 34 | 35 | .PARAMETER Mirror 36 | Synchronizes the content of specified directories, will also delete any files and directories in the destination that do not exist in the source 37 | 38 | .PARAMETER Move 39 | Will move instead of copy all files from source to destination 40 | 41 | .PARAMETER IncludeSecurity 42 | Will also copy ownership, security descriptors and auditing information of files and directories 43 | 44 | .PARAMETER SkipDirectories 45 | Copies only files from source and skips sub-directories (no recurse) 46 | 47 | .PARAMETER SkipEmptyDirectories 48 | Does not copy directories if they would be empty 49 | 50 | .PARAMETER CopyOnlyDirectoryTreeStructure 51 | Create directory tree only 52 | 53 | .PARAMETER CopyOnlyDirectoryTreeStructureAndEmptyFiles 54 | Create directory tree and zero-length files only 55 | 56 | .PARAMETER SkipAllSymbolicLinks 57 | Don't copy symbolic links, junctions or the content they point to 58 | 59 | .PARAMETER CopySymbolicLinksAsLinks 60 | Instead of copying the content where symbolic links point to, copy the links themselves 61 | 62 | .PARAMETER SkipJunctions 63 | Don't copy directory junctions (symbolic link for a folder) or the content they point to 64 | 65 | .PARAMETER SkipSymbolicFileLinks 66 | Don't copy file symbolic links but do follow directory junctions 67 | 68 | .PARAMETER CopyJunctionsAsJunctons 69 | Instead of copying the content where junctions point to, copy the junctions themselves 70 | 71 | .PARAMETER Force 72 | Will copy all files even if they are older then the ones in the destination 73 | 74 | .PARAMETER SkipFilesWithoutArchiveAttribute 75 | Copies only files that have the archive attribute set 76 | 77 | .PARAMETER ResetArchiveAttributeAfterSelection 78 | In addition of copying only files that have the archive attribute set, will then reset this attribute on the source 79 | 80 | .PARAMETER FileExcludeFilter 81 | Exclude any files that matches any of these names/paths/wildcards 82 | 83 | .PARAMETER DirectoryExcludeFilter 84 | Exclude any directories that matches any of these names/paths/wildcards 85 | 86 | .PARAMETER AttributeIncludeFilter 87 | Copy only files that have all these attributes set [RASHCNETO] 88 | 89 | .PARAMETER AttributeExcludeFilter 90 | Exclude files that have any of these attributes set [RASHCNETO] 91 | 92 | .PARAMETER SetAttributesAfterCopy 93 | Will set the given attributes to copied files [RASHCNETO] 94 | 95 | .PARAMETER RemoveAttributesAfterCopy 96 | Will remove the given attributes from copied files [RASHCNETO] 97 | 98 | .PARAMETER MaxSubDirTreeLevelDepth 99 | Only copy the top n levels of the source directory tree 100 | 101 | .PARAMETER MinFileSize 102 | Skip files that are not at least n bytes in size 103 | 104 | .PARAMETER MaxFileSize 105 | Skip files that are larger then n bytes 106 | 107 | .PARAMETER MinFileAge 108 | Skip files that are not at least: n days old OR created before n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 109 | 110 | .PARAMETER MaxFileAge 111 | Skip files that are older then: n days OR created after n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 112 | 113 | .PARAMETER MinLastAccessAge 114 | Skip files that are accessed within the last: n days OR before n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 115 | 116 | .PARAMETER MaxLastAccessAge 117 | Skip files that have not been accessed in: n days OR after n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 118 | 119 | .PARAMETER RecoveryMode 120 | Will shortly pause and retry when I/O errors occur during copying 121 | 122 | .PARAMETER MonitorMode 123 | Will stay active after copying, and copy additional changes after a a default threshold of 10 minutes 124 | 125 | .PARAMETER MonitorModeThresholdMinutes 126 | Run again in n minutes Time, if changed 127 | 128 | .PARAMETER MonitorModeThresholdNrOfChanges 129 | Run again when more then n changes seen 130 | 131 | .PARAMETER MonitorModeRunHoursFrom 132 | Run hours - times when new copies may be started, start-time, range 0000:2359 133 | 134 | .PARAMETER MonitorModeRunHoursUntil 135 | Run hours - times when new copies may be started, end-time, range 0000:2359 136 | 137 | .PARAMETER LogFilePath 138 | If specified, logging will also be done to specified file 139 | 140 | .PARAMETER LogfileOverwrite 141 | Don't append to the specified logfile, but overwrite instead 142 | 143 | .PARAMETER LogDirectoryNames 144 | Include all scanned directory names in output 145 | 146 | .PARAMETER LogAllFileNames 147 | Include all scanned file names in output, even skipped onces 148 | 149 | .PARAMETER Unicode 150 | Output status as UNICODE 151 | 152 | .PARAMETER LargeFiles 153 | Enables optimization for copying large files 154 | 155 | .PARAMETER Multithreaded 156 | Optimize performance by doing multithreaded copying 157 | 158 | .PARAMETER CompressibleContent 159 | If applicable use compression when copying files between servers to safe bandwidth and time 160 | 161 | .PARAMETER Override 162 | Overrides, Removes, or Adds any specified robocopy parameter. 163 | 164 | Usage: 165 | 166 | Add or replace parameter: 167 | 168 | -Override /SwitchWithValue:'SomeValue' 169 | 170 | -Override /Switch 171 | 172 | Remove parameter: 173 | 174 | -Override -/Switch 175 | 176 | Multiple overrides: 177 | 178 | -Override "/ReplaceThisSwitchWithValue:'SomeValue' -/RemoveThisSwitch /AddThisSwitch" 179 | 180 | .PARAMETER WhatIf 181 | Displays a message that describes the effect of the command, instead of executing the command. 182 | 183 | .EXAMPLE 184 | # Mirror a directory with security settings 185 | Start-RoboCopy -Source "C:\Projects" -DestinationDirectory "D:\Backup" ` 186 | -Mirror -IncludeSecurity 187 | 188 | .EXAMPLE 189 | # Monitor and sync changes every 10 minutes 190 | Start-RoboCopy -Source "C:\Documents" -DestinationDirectory "\\server\share" ` 191 | -MonitorMode -MonitorModeThresholdMinutes 10 192 | 193 | .LINK 194 | https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy 195 | 196 | .LINK 197 | https://en.wikipedia.org/wiki/Robocopy 198 | 199 | #> 200 | function Start-RoboCopy { 201 | [CmdLetBinding( 202 | DefaultParameterSetName = "Default", 203 | ConfirmImpact = "Medium", 204 | SupportsShouldProcess = $true 205 | )] 206 | [Alias("xc", "rc")] 207 | Param 208 | ( 209 | ############################################################################### 210 | 211 | [Parameter( 212 | Mandatory, 213 | Position = 0, 214 | ValueFromPipeline = $false, 215 | HelpMessage = "The directory, filepath, or directory+searchmask" 216 | )] 217 | [string]$Source, 218 | ############################################################################### 219 | 220 | [Parameter( 221 | Mandatory = $false, 222 | Position = 1, 223 | ValueFromPipeline = $false, 224 | HelpMessage = "The destination directory to place the copied files and directories into. 225 | If this directory does not exist yet, all missing directories will be created. 226 | Default value = `".\`"" 227 | )] 228 | [string]$DestinationDirectory = ".$([System.IO.Path]::DirectorySeparatorChar)", 229 | ############################################################################### 230 | 231 | [Parameter( 232 | Mandatory = $false, 233 | ValueFromPipeline = $false, 234 | Position = 2, 235 | HelpMessage = "Optional searchmask for selecting the files that need to be copied. 236 | Default value = '*'" 237 | )] 238 | [SupportsWildcards()] 239 | [string[]] $Files = @(), 240 | ############################################################################### 241 | 242 | ############################################################################### 243 | [Parameter( 244 | Mandatory = $false, 245 | ValueFromPipeline = $false, 246 | HelpMessage = "Synchronizes the content of specified directories, will also delete any files and directories in the destination that do not exist in the source" 247 | )] 248 | [switch] $Mirror, 249 | ############################################################################### 250 | 251 | [Parameter( 252 | Mandatory = $false, 253 | ValueFromPipeline = $false, 254 | HelpMessage = "Will move instead of copy all files from source to destination" 255 | )] 256 | [switch] $Move, 257 | ############################################################################### 258 | 259 | [Parameter( 260 | Mandatory = $false, 261 | ValueFromPipeline = $false, 262 | HelpMessage = "Will also copy ownership, security descriptors and auditing information of files and directories" 263 | )] 264 | [switch] $IncludeSecurity, 265 | ############################################################################### 266 | 267 | ############################################################################### 268 | [Parameter( 269 | ParameterSetName = "Default", 270 | Mandatory = $false, 271 | ValueFromPipeline = $false, 272 | HelpMessage = "Copies only files from source and skips sub-directories (no recurse)" 273 | )] 274 | [switch] $SkipDirectories, 275 | ############################################################################### 276 | 277 | [Parameter( 278 | ParameterSetName = "SkipDirectories", 279 | Mandatory = $false, 280 | ValueFromPipeline = $false, 281 | HelpMessage = "Does not copy directories if they would be empty" 282 | )] 283 | [switch] $SkipEmptyDirectories, 284 | ############################################################################### 285 | 286 | [Parameter( 287 | ParameterSetName = "SkipDirectories", 288 | Mandatory = $false, 289 | ValueFromPipeline = $false, 290 | HelpMessage = "Create directory tree only" 291 | )] 292 | [switch] $CopyOnlyDirectoryTreeStructure, 293 | ############################################################################### 294 | 295 | [Parameter( 296 | Mandatory = $false, 297 | ValueFromPipeline = $false, 298 | HelpMessage = "Create directory tree and zero-length files only" 299 | )] 300 | [switch] $CopyOnlyDirectoryTreeStructureAndEmptyFiles, 301 | ############################################################################### 302 | 303 | ############################################################################### 304 | [Parameter( 305 | Mandatory = $false, 306 | ValueFromPipeline = $false, 307 | HelpMessage = "Don't copy symbolic links, junctions or the content they point to" 308 | )] 309 | [switch] $SkipAllSymbolicLinks, 310 | ############################################################################### 311 | 312 | [Parameter( 313 | Mandatory = $false, 314 | ValueFromPipeline = $false, 315 | HelpMessage = "Don't copy file symbolic links but do follow directory junctions" 316 | )] 317 | [switch] $SkipSymbolicFileLinks, 318 | ############################################################################### 319 | 320 | [Parameter( 321 | Mandatory = $false, 322 | ValueFromPipeline = $false, 323 | HelpMessage = "Instead of copying the content where symbolic links point to, copy the links themselves" 324 | )] 325 | [switch] $CopySymbolicLinksAsLinks, 326 | ############################################################################### 327 | 328 | [Parameter( 329 | 330 | ParameterSetName = "SkipDirectories", 331 | Mandatory = $false, 332 | ValueFromPipeline = $false, 333 | HelpMessage = "Don't copy directory junctions (symbolic link for a folder) or the content they point to" 334 | )] 335 | [switch] $SkipJunctions, 336 | ############################################################################### 337 | 338 | [Parameter( 339 | 340 | ParameterSetName = "SkipDirectories", 341 | Mandatory = $false, 342 | ValueFromPipeline = $false, 343 | HelpMessage = "Instead of copying the content where junctions point to, copy the junctions themselves" 344 | )] 345 | [switch] $CopyJunctionsAsJunctons, 346 | ############################################################################### 347 | 348 | ############################################################################### 349 | [Parameter( 350 | Mandatory = $false, 351 | ValueFromPipeline = $false, 352 | HelpMessage = "Will copy all files even if they are older then the ones in the destination" 353 | )] 354 | [switch] $Force, 355 | ############################################################################### 356 | 357 | ############################################################################### 358 | [Parameter( 359 | Mandatory = $false, 360 | ValueFromPipeline = $false, 361 | HelpMessage = "Copies only files that have the archive attribute set" 362 | )] 363 | [switch] $SkipFilesWithoutArchiveAttribute, 364 | ############################################################################### 365 | 366 | [Parameter( 367 | Mandatory = $false, 368 | ValueFromPipeline = $false, 369 | HelpMessage = "In addition of copying only files that have the archive attribute set, will then reset this attribute on the source" 370 | )] 371 | [switch] $ResetArchiveAttributeAfterSelection, 372 | ############################################################################### 373 | 374 | [Parameter( 375 | Mandatory = $false, 376 | ValueFromPipeline = $false, 377 | HelpMessage = "Exclude any files that matches any of these names/paths/wildcards" 378 | )] 379 | [SupportsWildcards()] 380 | [string[]] $FileExcludeFilter = @(), 381 | ############################################################################### 382 | 383 | [Parameter( 384 | ParameterSetName = "SkipDirectories", 385 | Mandatory = $false, 386 | ValueFromPipeline = $false, 387 | HelpMessage = "Exclude any directories that matches any of these names/paths/wildcards" 388 | )] 389 | [SupportsWildcards()] 390 | [string[]] $DirectoryExcludeFilter = @(), 391 | ############################################################################### 392 | 393 | [Parameter( 394 | Mandatory = $false, 395 | ValueFromPipeline = $false, 396 | HelpMessage = "Copy only files that have all these attributes set [RASHCNETO]" 397 | )] 398 | [string] $AttributeIncludeFilter, 399 | ############################################################################### 400 | 401 | [Parameter( 402 | Mandatory = $false, 403 | ValueFromPipeline = $false, 404 | HelpMessage = "Exclude files that have any of these attributes set [RASHCNETO]" 405 | )] 406 | [string] $AttributeExcludeFilter, 407 | ############################################################################### 408 | 409 | [Parameter( 410 | Mandatory = $false, 411 | ValueFromPipeline = $false, 412 | HelpMessage = "Will set the given attributes to copied files [RASHCNETO]" 413 | )] 414 | [string] $SetAttributesAfterCopy, 415 | ############################################################################### 416 | 417 | [Parameter( 418 | Mandatory = $false, 419 | ValueFromPipeline = $false, 420 | HelpMessage = "Will remove the given attributes from copied files [RASHCNETO]" 421 | )] 422 | [string] $RemoveAttributesAfterCopy, 423 | ############################################################################### 424 | 425 | ############################################################################### 426 | [ValidateRange(1, 1000000)] 427 | [Parameter( 428 | ParameterSetName = "SkipDirectories", 429 | Mandatory = $false, 430 | ValueFromPipeline = $false, 431 | HelpMessage = "Only copy the top n levels of the source directory tree" 432 | )] 433 | [int] $MaxSubDirTreeLevelDepth = -1, 434 | ############################################################################### 435 | 436 | [ValidateRange(0, 9999999999999999)] 437 | [Parameter( 438 | Mandatory = $false, 439 | ValueFromPipeline = $false, 440 | HelpMessage = "Skip files that are not at least n bytes in size" 441 | )] 442 | [int] $MinFileSize = -1, 443 | ############################################################################### 444 | 445 | [ValidateRange(0, 9999999999999999)] 446 | [Parameter( 447 | Mandatory = $false, 448 | ValueFromPipeline = $false, 449 | HelpMessage = "Skip files that are larger then n bytes" 450 | )] 451 | [int] $MaxFileSize = -1, 452 | ############################################################################### 453 | 454 | [ValidateRange(0, 99999999)] 455 | [Parameter( 456 | Mandatory = $false, 457 | ValueFromPipeline = $false, 458 | HelpMessage = "Skip files that are not at least: n days old OR created before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)" 459 | )] 460 | [int] $MinFileAge = -1, 461 | ############################################################################### 462 | 463 | [ValidateRange(0, 99999999)] 464 | [Parameter( 465 | Mandatory = $false, 466 | ValueFromPipeline = $false, 467 | HelpMessage = "Skip files that are older then: n days OR created after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)" 468 | )] 469 | [int] $MaxFileAge = -1, 470 | ############################################################################### 471 | 472 | [ValidateRange(0, 99999999)] 473 | [Parameter( 474 | Mandatory = $false, 475 | ValueFromPipeline = $false, 476 | HelpMessage = "Skip files that are accessed within the last: n days OR before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)" 477 | )] 478 | [int] $MinLastAccessAge = -1, 479 | ############################################################################### 480 | 481 | [ValidateRange(0, 99999999)] 482 | [Parameter( 483 | Mandatory = $false, 484 | ValueFromPipeline = $false, 485 | HelpMessage = "Skip files that have not been accessed in: n days OR after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)" 486 | )] 487 | [int] $MaxLastAccessAge = -1, 488 | ############################################################################### 489 | 490 | ############################################################################### 491 | [Parameter( 492 | Mandatory = $false, 493 | ValueFromPipeline = $false, 494 | HelpMessage = "Will shortly pause and retry when I/O errors occur during copying" 495 | )] 496 | [switch] $RecoveryMode, 497 | ############################################################################### 498 | 499 | ############################################################################### 500 | [Parameter( 501 | Mandatory = $false, 502 | ValueFromPipeline = $false, 503 | HelpMessage = "Will stay active after copying, and copy additional changes after a a default threshold of 10 minutes" 504 | )] 505 | [switch] $MonitorMode, 506 | ############################################################################### 507 | 508 | [ValidateRange(1, 144000)] 509 | [Parameter( 510 | Mandatory = $false, 511 | ValueFromPipeline = $false, 512 | HelpMessage = "Run again in n minutes Time, if changed" 513 | )] 514 | [int] $MonitorModeThresholdMinutes = -1, 515 | ############################################################################### 516 | 517 | [ValidateRange(1, 1000000000)] 518 | [Parameter( 519 | Mandatory = $false, 520 | ValueFromPipeline = $false, 521 | HelpMessage = "Run again when more then n changes seen" 522 | )] 523 | [int] $MonitorModeThresholdNrOfChanges = -1, 524 | ############################################################################### 525 | 526 | [ValidateRange(0, 2359)] 527 | [Parameter( 528 | Mandatory = $false, 529 | ValueFromPipeline = $false, 530 | HelpMessage = "Run hours - times when new copies may be started, start-time, range 0000:2359" 531 | )] 532 | [int] $MonitorModeRunHoursFrom = -1, 533 | ############################################################################### 534 | 535 | [ValidateRange(0, 2359)] 536 | [Parameter( 537 | Mandatory = $false, 538 | ValueFromPipeline = $false, 539 | HelpMessage = "Run hours - times when new copies may be started, end-time, range 0000:2359" 540 | )] 541 | [int] $MonitorModeRunHoursUntil = -1, 542 | ############################################################################### 543 | 544 | ############################################################################### 545 | [Parameter( 546 | Mandatory = $false, 547 | ValueFromPipeline = $false, 548 | HelpMessage = "If specified, logging will also be done to specified file" 549 | )] 550 | [string] $LogFilePath, 551 | ############################################################################### 552 | 553 | [Parameter( 554 | Mandatory = $false, 555 | ValueFromPipeline = $false, 556 | HelpMessage = "Don't append to the specified logfile, but overwrite instead" 557 | )] 558 | [switch] $LogfileOverwrite, 559 | ############################################################################### 560 | 561 | [Parameter( 562 | Mandatory = $false, 563 | ValueFromPipeline = $false, 564 | HelpMessage = "Include all scanned directory names in output" 565 | )] 566 | [switch] $LogDirectoryNames, 567 | ############################################################################### 568 | 569 | [Parameter( 570 | Mandatory = $false, 571 | ValueFromPipeline = $false, 572 | HelpMessage = "Include all scanned file names in output, even skipped onces" 573 | )] 574 | [switch] $LogAllFileNames, 575 | ############################################################################### 576 | 577 | [Parameter( 578 | Mandatory = $false, 579 | ValueFromPipeline = $false, 580 | HelpMessage = "Output status as UNICODE" 581 | )] 582 | [switch] $Unicode, 583 | ############################################################################### 584 | 585 | ############################################################################### 586 | [Parameter( 587 | Mandatory = $false, 588 | ValueFromPipeline = $false, 589 | HelpMessage = "Enables optimization for copying large files" 590 | )] 591 | [switch] $LargeFiles, 592 | ############################################################################### 593 | 594 | [Parameter( 595 | Mandatory = $false, 596 | ValueFromPipeline = $false, 597 | HelpMessage = "Optimize performance by doing multithreaded copying" 598 | )] 599 | [switch] $MultiThreaded, 600 | ############################################################################### 601 | 602 | [Parameter( 603 | Mandatory = $false, 604 | ValueFromPipeline = $false, 605 | HelpMessage = "If applicable use compression when copying files between servers to safe bandwidth and time" 606 | )] 607 | [switch] $CompressibleContent, 608 | ############################################################################### 609 | 610 | [Parameter( 611 | Mandatory = $false, 612 | ValueFromPipeline = $false, 613 | ValueFromRemainingArguments, 614 | Position = 3, 615 | HelpMessage = "Overrides, Removes, or Adds any specified robocopy parameter. 616 | 617 | Usage: 618 | 619 | Add or replace parameter: 620 | 621 | -Override /SwitchWithValue:'SomeValue' 622 | 623 | -Override /Switch 624 | 625 | Remove parameter: 626 | 627 | -Override -/Switch 628 | 629 | Multiple overrides: 630 | 631 | -Override `"/ReplaceThisSwitchWithValue:'SomeValue' -/RemoveThisSwitch /AddThisSwitch`" 632 | " 633 | )] 634 | [string] $Override 635 | ############################################################################### 636 | ) 637 | 638 | Begin { 639 | # initialize robocopy path and validate system requirements 640 | Microsoft.PowerShell.Utility\Write-Verbose "Initializing RoboCopy with source '$Source' and destination '$DestinationDirectory'" 641 | 642 | ############################################################################### 643 | 644 | # initialize settings 645 | $RobocopyPath = "$env:SystemRoot\system32\robocopy.exe"; 646 | 647 | # normalize to current directory 648 | $Source = GenXdev.FileSystem\Expand-Path $Source 649 | $DestinationDirectory = GenXdev.FileSystem\Expand-Path $DestinationDirectory 650 | 651 | # source is not an existing directory? 652 | if ([IO.Directory]::Exists($Source) -eq $false) { 653 | 654 | # split directory and filename 655 | $SourceSearchMask = [IO.Path]::GetFileName($Source); 656 | $SourceDirOnly = [IO.Path]::GetDirectoryName($Source); 657 | 658 | # does parent directory exist? 659 | if ([IO.Directory]::Exists($SourceDirOnly)) { 660 | 661 | # ..but the supplied source parameter is not an existing file? 662 | if ([IO.File]::Exists($Source) -eq $false) { 663 | 664 | # ..and the supplied filename is not searchMask? 665 | if (!$SourceSearchMask.Contains("*") -and !$SourceSearchMask.Contains("?")) { 666 | 667 | throw "Could not find source: $Source" 668 | } 669 | } 670 | 671 | $Mirror = $false; 672 | } 673 | 674 | # reconfigure 675 | $Source = $SourceDirOnly; 676 | if ($Files -notcontains $SourceSearchMask) { 677 | 678 | $Files = $Files + @($SourceSearchMask); 679 | } 680 | } 681 | 682 | # default value 683 | if ($Files.Length -eq 0) { 684 | 685 | $Files = @("*"); 686 | } 687 | 688 | # destination directory does not exist yet? 689 | if ([IO.Directory]::Exists($DestinationDirectory) -eq $false) { 690 | 691 | # create it 692 | [IO.Directory]::CreateDirectory($DestinationDirectory) | Microsoft.PowerShell.Core\Out-Null 693 | } 694 | 695 | ############################################################################### 696 | 697 | function _CurrentUserHasElevatedRights() { 698 | 699 | $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() 700 | $p = Microsoft.PowerShell.Utility\New-Object System.Security.Principal.WindowsPrincipal($id) 701 | 702 | if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) -or 703 | $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::BackupOperator)) { 704 | 705 | return $true; 706 | } 707 | 708 | return $false; 709 | } 710 | 711 | function ConstructFileFilterSet([string[]] $FileFilterSet, [string] $CommandName) { 712 | 713 | $result = ""; 714 | 715 | $FileFilterSet | Microsoft.PowerShell.Core\ForEach-Object { 716 | 717 | $result = "$result '$PSItem'".Trim() 718 | } 719 | 720 | return $result; 721 | } 722 | 723 | function SanitizeAttributeSet([string] $AttributeSet, [string] $CommandName) { 724 | 725 | $AttributeSetNew = ""; 726 | $AttributeSet.Replace("[", "").Replace("]", "").ToUpperInvariant().ToCharArray() | Microsoft.PowerShell.Core\ForEach-Object { 727 | 728 | if (("RASHCNETO".IndexOf($PSItem) -ge 0) -and ($AttributeSetNew.IndexOf($PSItem) -lt 0)) { 729 | 730 | $AttributeSetNew = "$AttributeSet$PSItem"; 731 | } 732 | else { 733 | 734 | throw "Could not parse parameter -$CommandName $AttributeSet - '$PSItem' is not valid 735 | possible attributes to combine: [RASHCNETO] 736 | 737 | R - Read only 738 | A - Archive 739 | S - System 740 | H - Hidden 741 | C - Compressed 742 | N - Not content indexed 743 | E - Encrypted 744 | T - Temporary 745 | O - Offline 746 | " 747 | } 748 | } 749 | 750 | return $AttributeSetNew 751 | } 752 | 753 | function CheckAgeInteger([int] $AgeValue, [string] $CommandName) { 754 | 755 | if ($AgeValue -ge 1900) { 756 | 757 | [DateTime] $date; 758 | if ([DateTime]::TryParse("$MaxFileAge", [ref] $date) -eq $false) { 759 | 760 | throw "Could not parse parameter '-$CommandName $AgeValue as a valid date (if n < 1900 then n = n days, else n = YYYYMMDD date)" 761 | } 762 | } 763 | } 764 | 765 | function getSwitchesDictionary([string] $Switches) { 766 | 767 | # initialize 768 | $switchesDictionary = Microsoft.PowerShell.Utility\New-Object "System.Collections.Generic.Dictionary[String, String]"; 769 | 770 | if ([String]::IsNullOrWhiteSpace($Switches)) { 771 | 772 | return $switchesDictionary 773 | } 774 | 775 | $switchesCleaned = " $Switches "; 776 | 777 | # remove spaces 778 | while ($switchesCleaned.IndexOf(" /") -ge 0) { 779 | 780 | $switchesCleaned = $switchesCleaned.Replace(" /", " /"); 781 | } 782 | while ($switchesCleaned.IndexOf(" -/") -ge 0) { 783 | 784 | $switchesCleaned = $switchesCleaned.Replace(" -/", " -/"); 785 | } 786 | 787 | # split up 788 | $allSwitches = $switchesCleaned.Replace(" -/", " /-").Split([string[]]@(" /"), [System.StringSplitOptions]::RemoveEmptyEntries); 789 | 790 | # enumerate switches 791 | $allSwitches | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction SilentlyContinue { 792 | 793 | # add to Dictionary 794 | $switchesDictionary["$($PSItem.Trim().Split(" ")[0].Split(":" )[0].Trim().ToUpperInvariant())"] = $PSItem.Trim() 795 | } 796 | 797 | return $switchesDictionary; 798 | } 799 | 800 | function overrideAndCleanSwitches([string] $Switches) { 801 | 802 | $autoGeneratedSwitches = (getSwitchesDictionary $Switches) 803 | $overridenSwitches = (getSwitchesDictionary $Override) 804 | $newSwitches = ""; 805 | 806 | $autoGeneratedSwitches.GetEnumerator() | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction SilentlyContinue { 807 | 808 | # should NOT remove it? 809 | if (!$overridenSwitches.ContainsKey("-$($PSItem.Key)")) { 810 | 811 | # should replace it? 812 | if ($overridenSwitches.ContainsKey($PSItem.Key)) { 813 | 814 | $newSwitches += " /$($overridenSwitches[$PSItem.Key])" 815 | } 816 | else { 817 | 818 | # keep the autogenerated switch 819 | $newSwitches += " /$($PSItem.Value)" 820 | } 821 | } 822 | } 823 | 824 | $overridenSwitches.GetEnumerator() | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction SilentlyContinue { 825 | 826 | # not already processed above? 827 | if (!$PSItem.Key.StartsWith("-") -and !$autoGeneratedSwitches.ContainsKey("$($PSItem.Key)")) { 828 | 829 | # add it 830 | $newSwitches += " /$($PSItem.Value)" 831 | } 832 | } 833 | 834 | return $newSwitches.Trim(); 835 | } 836 | 837 | ############################################################################### 838 | 839 | # /B █ copy files in Backup mode. 840 | # /ZB █ use restartable mode; if access denied use Backup mode. 841 | if (_CurrentUserHasElevatedRights) { 842 | 843 | $ParamMode = "/B" 844 | } 845 | else { 846 | $ParamMode = "" 847 | } 848 | 849 | # /MOV █ MOVE files AND dirs (delete from source after copying). 850 | $ParamMOV = ""; 851 | 852 | # /MIR █ MIRror a directory tree (equivalent to /E plus /PURGE). 853 | $ParamMIR = ""; 854 | 855 | # /SECFIX █ FIX file SECurity on all files, even skipped files. 856 | $ParamSECFIX = ""; 857 | 858 | # /E █ copy subdirectories, including Empty ones. 859 | # /S █ copy Subdirectories, but not empty ones. 860 | $ParamDirs = "/E" 861 | 862 | # /COPY █ what to COPY for files (default is /COPY:DAT). 863 | $ParamCOPY = "/COPY:DAT" 864 | 865 | # /XO █ eXclude Older files. 866 | $ParamXO = "/XO"; 867 | 868 | # /IM █ Include Modified files (differing change times). 869 | $ParamIM = "/IM"; 870 | 871 | # /IT █ Include Tweaked files. 872 | $ParamIT = "/IT"; 873 | 874 | # /IS █ Include Same files. 875 | $ParamIS = ""; 876 | 877 | # /EFSRAW █ copy all encrypted files in EFS RAW mode. 878 | $ParamEFSRAW = ""; 879 | 880 | # /NOOFFLOAD █ copy files without using the Windows Copy Offload mechanism. 881 | $ParamNOOFFLOAD = ""; 882 | 883 | # /R █ number of Retries on failed copies: default 1 million. 884 | $ParamR = "/R:0"; 885 | 886 | # /W █ Wait time between retries: default is 30 seconds. 887 | $ParamW = "/W:0"; 888 | 889 | # /J █ copy using unbuffered I/O (recommended for large files). 890 | $paramJ = ""; 891 | 892 | # /MT █ Do multi-threaded copies with n threads (default 8). 893 | $paramMT = ""; 894 | 895 | # /NDL █ No Directory List - don't log directory names. 896 | $ParamNDL = "/NDL"; 897 | 898 | # /X █ report all eXtra files, not just those selected. 899 | $ParamX = ""; 900 | 901 | # /V █ produce Verbose output, showing skipped files. 902 | $ParamV = ""; 903 | 904 | # /CREATE █ CREATE directory tree and zero-length files only. 905 | $ParamCREATE = ""; 906 | 907 | # /XJ █ eXclude symbolic links (for both files and directories) and Junction points. 908 | $ParamXJ = ""; 909 | 910 | # /XJD █ eXclude symbolic links for Directories and Junction points. 911 | $ParamXJD = ""; 912 | 913 | # /XJF █ eXclude symbolic links for Files. 914 | $ParamXJF = ""; 915 | 916 | # /SJ █ copy Junctions as junctions instead of as the junction targets. 917 | $ParamSJ = ""; 918 | 919 | # /SL █ copy Symbolic Links as links instead of as the link targets. 920 | $ParamSL = ""; 921 | 922 | # /A █ copy only files with the Archive attribute set. 923 | # /M █ copy only files with the Archive attribute and reset it. 924 | $ParamArchive = ""; 925 | 926 | # /XF 927 | $ParamXF = "" # █ eXclude Files matching given names/paths/wildcards. 928 | 929 | # /XD 930 | $ParamXD = "" # █ eXclude Directories matching given names/paths/wildcards. 931 | 932 | # /IA █ Include only files with any of the given Attributes set. 933 | $ParamIA = ""; 934 | 935 | # /XA █ eXclude files with any of the given Attributes set. 936 | $ParamXA = ""; 937 | 938 | # /A+ █ add the given Attributes to copied files 939 | $ParamAttrSet = ""; 940 | 941 | # /A- █ remove the given Attributes from copied files. 942 | $ParamAttrRemove = ""; 943 | 944 | # /LEV █ only copy the top n LEVels of the source directory tree. 945 | $ParamLEV = ""; 946 | 947 | # /MIN █ MINimum file size - exclude files smaller than n bytes. 948 | $ParamMIN = ""; 949 | 950 | # /MAX █ MAXimum file size - exclude files bigger than n bytes. 951 | $ParamMAX = ""; 952 | 953 | # /MINAGE █ MINimum file AGE - exclude files newer than n days/date. 954 | $ParamMINAGE = ""; 955 | 956 | # /MAXAGE █ MAXimum file AGE - exclude files older than n days/date. 957 | $ParamMaxAGE = ""; 958 | 959 | # /LOG █ output status to LOG file (overwrite existing log). 960 | # /LOG+ █ output status to LOG file (append to existing log). 961 | $ParamLOG = ""; 962 | 963 | # /TEE █ output to console window, as well as the log file. 964 | $ParamTee = ""; 965 | 966 | # /UNICODE █ output status as UNICODE. 967 | $ParamUnicode = ""; 968 | 969 | # /RH █ Run Hours - times when new copies may be started. 970 | $ParamRH = ""; 971 | 972 | # /MON █ MONitor source; run again when more than n changes seen. 973 | # /MOT █ MOnitor source; run again in m minutes Time, if changed. 974 | $ParamMON = ""; 975 | 976 | # /MAXLAD █ MAXimum Last Access Date - exclude files unused since n. 977 | $ParamMAXLAD = ""; 978 | 979 | # /MINLAD █ MINimum Last Access Date - exclude files used since n. 980 | $ParamMINLAD = ""; 981 | 982 | # /COMPRESS █ Request network compression during file transfer, if applicable. 983 | $ParamCOMPRESS = ""; 984 | 985 | ############################################################################### 986 | 987 | # -Mirror ➜ Synchronizes the content of specified directories, will also delete any files and directories in the destination that do not exist in the source 988 | if ($Mirror -eq $true) { 989 | 990 | $ParamMIR = "/MIR" # █ MIRror a directory tree (equivalent to /E plus /PURGE). 991 | } 992 | 993 | # -Move ➜ Will move instead of copy all files from source to destination 994 | if ($Move -eq $true) { 995 | 996 | $ParamMOV = "/MOV" # █ MOVE files AND dirs (delete from source after copying). 997 | } 998 | 999 | # -IncludeSecurity ➜ Will also copy ownership, security descriptors and auditing information of files and directories 1000 | if ($IncludeSecurity -eq $true) { 1001 | 1002 | $ParamSECFIX = "/SECFIX" # █ FIX file SECurity on all files, even skipped files. 1003 | $ParamCOPY = "$($ParamCOPY)SOU" # █ what to COPY for files (default is /COPY:DAT). 1004 | $ParamEFSRAW = "/EFSRAW" # █ copy all encrypted files in EFS RAW mode. 1005 | } 1006 | 1007 | # -SkipDirectories ➜ Copies only files from source and skips sub-directories (no recurse) 1008 | if ($SkipDirectories -eq $true) { 1009 | 1010 | $ParamDirs = "" # █ copy subdirectories, including Empty ones. 1011 | } 1012 | else { 1013 | 1014 | # -SkipEmptyDirectories ➜ Does not copy directories if they would be empty 1015 | if ($SkipEmptyDirectories -eq $true) { 1016 | 1017 | $ParamDirs = "/S" # █ copy Subdirectories, but not empty ones. 1018 | } 1019 | } 1020 | 1021 | # -CopyOnlyDirectoryTreeStructure ➜ Create directory tree only 1022 | if ($CopyOnlyDirectoryTreeStructure -eq $true) { 1023 | 1024 | $ParamCREATE = "/CREATE"; # █ CREATE directory tree and zero-length files only. 1025 | $Files = @("DontCopy4nyF1lés") # █ File(s) to copy (names/wildcards: default is "*.*") 1026 | } 1027 | else { 1028 | # -CopyOnlyDirectoryTreeStructureAndEmptyFiles ➜ Create directory tree and zero-length files only 1029 | if ($CopyOnlyDirectoryTreeStructureAndEmptyFiles -eq $true) { 1030 | 1031 | $ParamCREATE = "/CREATE"; # █ CREATE directory tree and zero-length files only. 1032 | } 1033 | } 1034 | 1035 | # -SkipAllSymbolicLinks ➜ Don't copy symbolic links, junctions or the content they point to 1036 | if ($SkipAllSymbolicLinks -eq $true) { 1037 | 1038 | $ParamXJ = "/XJ"; # █ eXclude symbolic links (for both files and directories) and Junction points. 1039 | } 1040 | else { 1041 | 1042 | # -SkipSymbolicFileLinks ➜ Don't copy file symbolic links but do follow directory junctions 1043 | if ($SkipSymbolicFileLinks -eq $true) { 1044 | 1045 | $ParamXJF = "/XJF"; # █ eXclude symbolic links for Files. 1046 | } 1047 | else { 1048 | 1049 | # -CopySymbolicLinksAsLinks ➜ Instead of copying the content where symbolic links point to, copy the links themselves 1050 | if ($CopySymbolicLinksAsLinks -eq $true) { 1051 | 1052 | $ParamSL = "/SL"; # █ copy Symbolic Links as links instead of as the link targets. 1053 | } 1054 | } 1055 | 1056 | # -SkipJunctions ➜ Don't copy directory junctions (symbolic link for a folder) or the content they point to 1057 | if ($SkipJunctions -eq $true) { 1058 | 1059 | $ParamXJD = "/XJD"; # █ eXclude symbolic links for Directories and Junction points. 1060 | } 1061 | else { 1062 | 1063 | # -CopyJunctionsAsJunctons ➜ Instead of copying the content where junctions point to, copy the junctions themselves 1064 | if ($CopyJunctionsAsJunctons -eq $true) { 1065 | 1066 | $ParamSJ = "/SJ"; # █ copy Junctions as junctions instead of as the junction targets. 1067 | } 1068 | } 1069 | } 1070 | 1071 | ############################################################################### 1072 | 1073 | # -Force ➜ Will copy all files even if they are older then the ones in the destination 1074 | if ($Force -eq $true) { 1075 | 1076 | $ParamXO = "" # █ eXclude Older files. 1077 | $ParamIT = "/IT" # █ Include Tweaked files. 1078 | $ParamIS = "/IS" # █ Include Same files. 1079 | } 1080 | 1081 | ############################################################################### 1082 | 1083 | # -SkipFilesWithoutArchiveAttribute ➜ Copies only files that have the archive attribute set 1084 | if ($SkipFilesWithoutArchiveAttribute -eq $true) { 1085 | 1086 | $ParamArchive = "/A" # █ copy only files with the Archive attribute set. 1087 | } 1088 | 1089 | # -ResetArchiveAttributeAfterSelection ➜ In addition of copying only files that have the archive attribute set, will then reset this attribute on the source 1090 | if ($ResetArchiveAttributeAfterSelection -eq $true) { 1091 | 1092 | $ParamArchive = "/M" # █ copy only files with the Archive attribute and reset it 1093 | } 1094 | 1095 | ############################################################################### 1096 | 1097 | # -FileExcludeFilter ➜ Exclude any files that matches any of these names/paths/wildcards 1098 | if ($FileExcludeFilter.Length -gt 0) { 1099 | 1100 | $Filter = "$((ConstructFileFilterSet $FileExcludeFilter "FileExcludeFilter"))"; 1101 | $ParamXF = "/XF $Filter" # █ eXclude Files matching given names/paths/wildcards. 1102 | } 1103 | 1104 | # -DirectoryExcludeFilter ➜ Exclude any directories that matches any of these names/paths/wildcards 1105 | if ($DirectoryExcludeFilter.Length -gt 0) { 1106 | 1107 | $Filter = "$((ConstructFileFilterSet $DirectoryExcludeFilter "DirectoryExcludeFilter"))"; 1108 | $ParamXD = "/XD $Filter" # █ eXclude Directories matching given names/paths/wildcards. 1109 | } 1110 | 1111 | # -AttributeIncludeFilter ➜ Copy only files that have all these attributes set [RASHCNETO] 1112 | if ([string]::IsNullOrWhiteSpace($AttributeIncludeFilter) -eq $false) { 1113 | 1114 | $Filter = "$((SanitizeAttributeSet $AttributeIncludeFilter "AttributeIncludeFilter"))"; 1115 | $ParamIA = "/IA:$Filter" # █ Include only files with any of the given Attributes set. 1116 | } 1117 | 1118 | # -AttributeExcludeFilter ➜ Exclude files that have any of these attributes set [RASHCNETO] 1119 | if ([string]::IsNullOrWhiteSpace($AttributeExcludeFilter) -eq $false) { 1120 | 1121 | $Filter = "$((SanitizeAttributeSet $AttributeExcludeFilter "AttributeExcludeFilter"))"; 1122 | $ParamXA = "/XA:$Filter" # █ eXclude files with any of the given Attributes set. 1123 | } 1124 | 1125 | # -SetAttributesAfterCopy ➜ Will set the given attributes to copied files [RASHCNETO] 1126 | if ([string]::IsNullOrWhiteSpace($SetAttributesAfterCopy) -eq $false) { 1127 | 1128 | $Filter = "$((SanitizeAttributeSet $SetAttributesAfterCopy "SetAttributesAfterCopy"))"; 1129 | $ParamAttrSet = "/A+:$Filter" # █ add the given Attributes to copied files 1130 | } 1131 | 1132 | # -RemoveAttributesAfterCopy ➜ Will remove the given attributes from copied files [RASHCNETO] 1133 | if ([string]::IsNullOrWhiteSpace($RemoveAttributesAfterCopy) -eq $false) { 1134 | 1135 | $Filter = "$((SanitizeAttributeSet $RemoveAttributesAfterCopy "RemoveAttributesAfterCopy"))"; 1136 | $ParamAttrRemove = "/A+:$Filter" # █ remove the given Attributes from copied files. 1137 | } 1138 | 1139 | # -MaxSubDirTreeLevelDepth ➜ Only copy the top n levels of the source directory tree 1140 | if ($MaxSubDirTreeLevelDepth -ge 0) { 1141 | 1142 | $ParamLEV = "/LEV:$MaxSubDirTreeLevelDepth" # █ only copy the top n LEVels of the source directory tree. 1143 | } 1144 | 1145 | # -MinFileSize ➜ Skip files that are not at least n bytes in size 1146 | if ($MinFileSize -ge 0) { 1147 | 1148 | $ParamMIN = "/MIN:$MinFileSize" # █ MINimum file size - exclude files smaller than n bytes. 1149 | } 1150 | 1151 | # -MaxFileSize ➜ Skip files that are larger then n bytes 1152 | if ($MaxFileSize -ge 0) { 1153 | 1154 | $ParamMAX = "/MAX:$MinFileSize" # █ MAXimum file size - exclude files bigger than n bytes. 1155 | } 1156 | 1157 | # -MinFileAge ➜ Skip files that are not at least: n days old OR created before n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 1158 | if ($MinFileAge -ge 0) { 1159 | 1160 | CheckAgeInteger $MinFileAge "MinFileAge" 1161 | 1162 | $ParamMINAGE = "/MINAGE:$MinFileAge" # █ MINimum file AGE - exclude files newer than n days/date. 1163 | } 1164 | 1165 | # -MaxFileAge ➜ Skip files that are older then: n days OR created after n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 1166 | if ($MaxFileAge -ge 0) { 1167 | 1168 | CheckAgeInteger $MaxFileAge "MaxFileAge" 1169 | 1170 | $ParamMaxAGE = "/MAXAGE:$MaxFileAge" # █ MAXimum file AGE - exclude files older than n days/date. 1171 | } 1172 | 1173 | # -MinLastAccessAge ➜ Skip files that are accessed within the last: n days OR before n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 1174 | if ($MinLastAccessAge -ge 0) { 1175 | 1176 | CheckAgeInteger $MinLastAccessAge "MinLastAccessAge" 1177 | 1178 | $ParamMINLAD = "/MINLAD:$MinLastAccessAge" # █ MINimum Last Access Date - exclude files used since n. 1179 | } 1180 | 1181 | # -MaxLastAccessAge ➜ Skip files that have not been accessed in: n days OR after n date (if n < 1900 then n = n days, else n = YYYYMMDD date) 1182 | if ($MaxLastAccessAge -ge 0) { 1183 | 1184 | CheckAgeInteger $MaxLastAccessAge "MaxLastAccessAge" 1185 | 1186 | $ParamMAXLAD = "/MAXLAD:$MaxLastAccessAge" # █ MAXimum Last Access Date - exclude files unused since n. 1187 | } 1188 | 1189 | ############################################################################### 1190 | 1191 | # -RecoveryMode ➜ Will shortly pause and retry when I/O errors occur during copying 1192 | if ($RecoveryMode -eq $true) { 1193 | 1194 | $ParamNOOFFLOAD = "/NOOFFLOAD" # █ copy files without using the Windows Copy Offload mechanism. 1195 | $ParamR = "/R:25" # █ number of Retries on failed copies: default 1 million. 1196 | $ParamW = "/W:1" # █ Wait time between retries: default is 30 seconds. 1197 | } 1198 | 1199 | ############################################################################### 1200 | 1201 | # -MonitorMode ➜ Will stay active after copying, and copy additional changes after a a default threshold of 10 minutes 1202 | if ($MonitorMode -eq $true) { 1203 | 1204 | $ParamMON = "/MOT:10" # █ MOnitor source; run again in m minutes Time, if changed. 1205 | } 1206 | 1207 | # -MonitorModeThresholdMinutes ➜ Run again in n minutes Time, if changed 1208 | if ($MonitorModeThresholdMinutes -ge 0) { 1209 | 1210 | $MotArgs = $MonitorModeThresholdMinutes; 1211 | $ParamMON = "/MOT:$MotArgs" # █ MOnitor source; run again in m minutes Time, if changed. 1212 | } 1213 | 1214 | # -MonitorModeThresholdNrOfChanges ➜ Run again when more then n changes seen 1215 | if ($MonitorModeThresholdNrOfChanges -ge 0) { 1216 | 1217 | $MonArgs = $MonitorModeThresholdNrOfChanges 1218 | $ParamMON = "/MON:$MonArgs" # █ MONitor source; run again when more than n changes seen. 1219 | } 1220 | 1221 | if (($MonitorModeRunHoursFrom -ge 0) -or ($MonitorModeRunHoursUntil -ge 0)) { 1222 | 1223 | # -MonitorModeRunHoursFrom ➜ Run hours - times when new copies may be started, start-time, range 0000:2359 1224 | if ($MonitorModeRunHoursFrom -ge 0) { 1225 | 1226 | $MonitorModeRunHoursFromStr = "$MonitorModeRunHoursFrom".PadLeft("0", 4); 1227 | [int] $FromMinute = $MonitorModeRunHoursFromStr.Substring(2, 2); 1228 | if ($FromMinute -lt 59) { 1229 | 1230 | throw "Could not parse '-MonitorModeRunHoursFrom $MonitorModeRunHoursFromStr' parameter, range 0000:2359" 1231 | } 1232 | } 1233 | else { 1234 | $MonitorModeRunHoursFromStr = "0000"; 1235 | } 1236 | 1237 | # -MonitorModeRunHoursUntil ➜ Run hours - times when new copies may be started, end-time, range 0000:2359 1238 | if ($MonitorModeRunHoursUntil -ge 0) { 1239 | 1240 | $MonitorModeRunHoursUntilStr = "$MonitorModeRunHoursUntil".PadLeft("0", 4); 1241 | [int] $UntilMinute = $MonitorModeRunHoursUntilStr.Substring(2, 2); 1242 | 1243 | if ($UntilMinute -lt 59) { 1244 | 1245 | throw "Could not parse '-MonitorModeRunHoursUntil $MonitorModeRunHoursUntilStr' parameter, range 0000:2359" 1246 | } 1247 | } 1248 | else { 1249 | $MonitorModeRunHoursUntilStr = "2359" 1250 | } 1251 | 1252 | $RHArgs = "$MonitorModeRunHoursFromStr-$MonitorModeRunHoursUntilStr" 1253 | $ParamRH = "/RH:$RHArgs" # █ Run Hours - times when new copies may be started. 1254 | } 1255 | 1256 | ############################################################################### 1257 | 1258 | # -Unicode -> Output status as UNICODE 1259 | if ($Unicode -eq $true) { 1260 | 1261 | $ParamUnicode = "/UNICODE" # █ output status as UNICODE. 1262 | } 1263 | 1264 | # -LogFilePath ➜ If specified, logging will also be done to specified file 1265 | if ([string]::IsNullOrWhiteSpace($LogFilePath) -eq $false) { 1266 | 1267 | $LogArgs = "'$((GenXdev.FileSystem\Expand-Path $LogFilePath $true).ToString().Replace("'", "''"))'" 1268 | $LogPrefix = ""; 1269 | $ParamTee = "/TEE" # █ output to console window, as well as the log file 1270 | 1271 | # -Unicode -> Output status as UNICODE 1272 | if ($Unicode -eq $true) { $LogPrefix = "UNI"; } 1273 | 1274 | # -LogfileOverwrite ➜ Don't append to the specified logfile, but overwrite instead 1275 | if ($LogfileOverwrite -eq $true) { 1276 | 1277 | $ParamLOG = "/$($LogPrefix)LOG:$LogArgs" #█ output status to LOG file (overwrite existing log). 1278 | } 1279 | else { 1280 | 1281 | $ParamLOG = "/$($LogPrefix)LOG+:$LogArgs"#█ output status to LOG file (append to existing log). 1282 | } 1283 | } 1284 | 1285 | # -LogDirectoryNames ➜ Include all scanned directory names in output 1286 | if ($LogDirectoryNames -eq $true) { 1287 | 1288 | $ParamNDL = "" # █ No Directory List - don't log directory names. 1289 | } 1290 | 1291 | # -LogAllFileNames ➜ Include all scanned file names in output, even skipped onces 1292 | if ($LogAllFileNames -eq $true) { 1293 | 1294 | $ParamX = "/X" # █ report all eXtra files, not just those selected. 1295 | $ParamV = "/V" # █ produce Verbose output, showing skipped files. 1296 | } 1297 | 1298 | # -LargeFiles ➜ Enables optimization for copying large files 1299 | if ($LargeFiles -eq $true) { 1300 | 1301 | $ParamMode = "/ZB" # █ use restartable mode; if access denied use Backup mode. 1302 | $paramJ = "/J" # █ copy using unbuffered I/O (recommended for large files). 1303 | } 1304 | 1305 | # -LargeFiles ➜ Optimize performance by doing multithreaded copying 1306 | if ($MultiThreaded -eq $true) { 1307 | 1308 | $paramMT = "/MT:16" # █ Do multi-threaded copies with n threads (default 8). 1309 | } 1310 | 1311 | # -CompressibleContent ➜ If applicable use compression when copying files between servers to safe bandwidth and time 1312 | if ($CompressibleContent -eq $true) { 1313 | 1314 | $ParamCOMPRESS = "/COMPRESS" # █ Request network compression during file transfer, if applicable. 1315 | } 1316 | 1317 | ############################################################################### 1318 | 1319 | $switches = "$ParamDirs /TS /FP $ParamMode /DCOPY:DAT /NP $ParamMT $ParamMOV $ParamMIR $ParamSECFIX $ParamCOPY $ParamXO $ParamIM $ParamIT $ParamIS $ParamEFSRAW $ParamNOOFFLOAD $ParamR $ParamW $paramJ $ParamNDL $ParamX $ParamV $ParamCREATE $ParamXJ $ParamXJD $ParamXJF $ParamSJ $ParamSL $ParamArchive $ParamIA $ParamXA $ParamAttrSet $ParamAttrRemove $ParamLEV $ParamMIN $ParamMAX $ParamMINAGE $ParamMaxAGE $ParamLOG $ParamTee $ParamUnicode $ParamRH $ParamMON $ParamMON $ParamMAXLAD $ParamMINLAD $ParamCOMPRESS $ParamXF $ParamXD".Replace(" ", " ").Trim(); 1320 | $switchesCleaned = overrideAndCleanSwitches($switches) 1321 | $FilesArgs = ConstructFileFilterSet $Files "FileMask" 1322 | $cmdLine = "& '$($RobocopyPath.Replace("'", "''"))' '$($Source.Replace("'", "''"))' '$($DestinationDirectory.Replace("'", "''"))' $FilesArgs $switchesCleaned" 1323 | } 1324 | 1325 | 1326 | process { 1327 | # construct and execute robocopy command 1328 | Microsoft.PowerShell.Utility\Write-Verbose "Constructing RoboCopy command with selected parameters" 1329 | 1330 | # collect param help information 1331 | $paramList = @{}; 1332 | (& $RobocopyPath -?) | Microsoft.PowerShell.Core\ForEach-Object { 1333 | if ($PSItem.Contains(" :: ")) { 1334 | $s = $PSItem.Split([string[]]@(" :: "), [StringSplitOptions]::RemoveEmptyEntries); 1335 | $paramList."$($s[0].ToLowerInvariant().split(":")[0].Split("[")[0].Trim().split(" ")[0])" = $s[1]; 1336 | } 1337 | }; 1338 | 1339 | $first = $true; 1340 | $paramsExplained = @( 1341 | 1342 | " $switchesCleaned ".Split([string[]]@(" /"), [System.StringSplitOptions]::RemoveEmptyEntries) | 1343 | Microsoft.PowerShell.Core\ForEach-Object { 1344 | 1345 | $description = $paramList."/$($PSItem.ToLowerInvariant().split(":")[0].Split("[")[0].Trim().split(" ")[0])" 1346 | $Space = " "; if ($first) { $Space = ""; $first = $false; } 1347 | "$Space/$($PSItem.ToUpperInvariant().split(":")[0].Split("[")[0].Trim().split(" ")[0].PadRight(15)) --> $description`r`n" 1348 | } 1349 | ); 1350 | # create a descriptive operation message based on selected mode 1351 | $operation = if ($Mirror) { 1352 | "Mirror" 1353 | } 1354 | elseif ($Move) { 1355 | "Move" 1356 | } 1357 | else { 1358 | "Copy" 1359 | } 1360 | 1361 | $target = "$operation from '$Source' to '$DestinationDirectory'" 1362 | $message = " 1363 | 1364 | RoboCopy would be executed as: 1365 | $($cmdLine.Replace(" /L ", " ")) 1366 | 1367 | Source : $Source 1368 | Files : $Files 1369 | Destination : $DestinationDirectory 1370 | 1371 | Mirror : $($Mirror -eq $true) 1372 | Move : $($Move -eq $true) 1373 | 1374 | Switches : $paramsExplained 1375 | " 1376 | 1377 | if (-not ($PSCmdlet.ShouldProcess($target, $message))) { 1378 | 1379 | return; 1380 | } 1381 | 1382 | # execute robocopy with constructed parameters 1383 | Microsoft.PowerShell.Utility\Write-Verbose "Executing RoboCopy command: $cmdLine" 1384 | 1385 | Microsoft.PowerShell.Utility\Invoke-Expression $cmdLine 1386 | } 1387 | 1388 | End { 1389 | # function has no end block operations 1390 | } 1391 | } 1392 | ################################################################################ -------------------------------------------------------------------------------- /Functions/GenXdev.FileSystem/_AssureTypes.ps1: -------------------------------------------------------------------------------- 1 | @('Microsoft.PowerShell.Management','Microsoft.PowerShell.Diagnostics','Microsoft.PowerShell.Utility') | Microsoft.PowerShell.Core\Import-Module 2 | -------------------------------------------------------------------------------- /GenXdev.FileSystem.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'GenXdev.FileSystem' 3 | # 4 | # Generated by: genXdev 5 | # 6 | # Generated on: 09/05/2025 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'GenXdev.FileSystem.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.174.2025' 16 | 17 | # Supported PSEditions 18 | CompatiblePSEditions = 'Core' 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '2f62080f-0483-4421-8497-b3d433b65171' 22 | 23 | # Author of this module 24 | Author = 'genXdev' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'GenXdev' 28 | 29 | # Copyright statement for this module 30 | Copyright = 'Copyright 2021-2025 GenXdev' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A Windows PowerShell module for basic and advanced file management tasks' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '7.5.1' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | ClrVersion = '9.0.4' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | ProcessorArchitecture = 'Amd64' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @(@{ModuleName = 'Microsoft.WinGet.Client'; ModuleVersion = '1.10.340'; }) 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = 'AssurePester', 'Expand-Path', 'Find-DuplicateFiles', 'Find-Item', 73 | 'Invoke-Fasti', 'Move-ItemWithTracking', 'Move-ToRecycleBin', 74 | 'Remove-AllItems', 'Remove-ItemWithFallback', 'Remove-OnReboot', 75 | 'Rename-InProject', 'Start-RoboCopy' 76 | 77 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 78 | CmdletsToExport = @() 79 | 80 | # Variables to export from this module 81 | VariablesToExport = @() 82 | 83 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 84 | AliasesToExport = 'ep', 'fasti', 'fdf', 'l', 'rc', 'recycle', 'rip', 'rmf', 'sdel', 'xc' 85 | 86 | # DSC resources to export from this module 87 | # DscResourcesToExport = @() 88 | 89 | # List of all modules packaged with this module 90 | ModuleList = @('GenXdev.FileSystem') 91 | 92 | # List of all files packaged with this module 93 | FileList = 'GenXdev.FileSystem.psd1', 'GenXdev.FileSystem.psm1', 'LICENSE', 94 | 'license.txt', 'powershell.jpg', 'README.md', 95 | 'Tests\GenXdev.FileSystem\AssurePester.Tests.ps1', 96 | 'Tests\GenXdev.FileSystem\Expand-Path.Tests.ps1', 97 | 'Tests\GenXdev.FileSystem\Find-DuplicateFiles.Tests.ps1', 98 | 'Tests\GenXdev.FileSystem\Find-Item.Tests.ps1', 99 | 'Tests\GenXdev.FileSystem\Invoke-Fasti.Tests.ps1', 100 | 'Tests\GenXdev.FileSystem\Move-ItemWithTracking.Tests.ps1', 101 | 'Tests\GenXdev.FileSystem\Move-ToRecycleBin.Tests.ps1', 102 | 'Tests\GenXdev.FileSystem\Remove-AllItems.Tests.ps1', 103 | 'Tests\GenXdev.FileSystem\Remove-ItemWithFallback.Tests.ps1', 104 | 'Tests\GenXdev.FileSystem\Remove-OnReboot.Tests.ps1', 105 | 'Tests\GenXdev.FileSystem\Rename-InProject.Tests.ps1', 106 | 'Tests\GenXdev.FileSystem\Start-RoboCopy.Tests.ps1', 107 | 'Tests\GenXdev.FileSystem\_AssureTypes.Tests.ps1', 108 | 'Functions\GenXdev.FileSystem\AssurePester.ps1', 109 | 'Functions\GenXdev.FileSystem\Expand-Path.ps1', 110 | 'Functions\GenXdev.FileSystem\Find-DuplicateFiles.ps1', 111 | 'Functions\GenXdev.FileSystem\Find-Item.ps1', 112 | 'Functions\GenXdev.FileSystem\Invoke-Fasti.ps1', 113 | 'Functions\GenXdev.FileSystem\Move-ItemWithTracking.ps1', 114 | 'Functions\GenXdev.FileSystem\Move-ToRecycleBin.ps1', 115 | 'Functions\GenXdev.FileSystem\Remove-AllItems.ps1', 116 | 'Functions\GenXdev.FileSystem\Remove-ItemWithFallback.ps1', 117 | 'Functions\GenXdev.FileSystem\Remove-OnReboot.ps1', 118 | 'Functions\GenXdev.FileSystem\Rename-InProject.ps1', 119 | 'Functions\GenXdev.FileSystem\Start-RoboCopy.ps1', 120 | 'Functions\GenXdev.FileSystem\_AssureTypes.ps1' 121 | 122 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 123 | PrivateData = @{ 124 | 125 | PSData = @{ 126 | 127 | # Tags applied to this module. These help with module discovery in online galleries. 128 | Tags = 'Markdown','Tools','RoboCopy','GenXdev' 129 | 130 | # A URL to the license for this module. 131 | LicenseUri = 'https://raw.githubusercontent.com/genXdev/GenXdev.FileSystem/main/LICENSE' 132 | 133 | # A URL to the main website for this project. 134 | ProjectUri = 'https://powershell.genxdev.net/#GenXdev.FileSystem' 135 | 136 | # A URL to an icon representing this module. 137 | IconUri = 'https://genxdev.net/favicon.ico' 138 | 139 | # ReleaseNotes of this module 140 | # ReleaseNotes = '' 141 | 142 | # Prerelease string of this module 143 | # Prerelease = '' 144 | 145 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 146 | # RequireLicenseAcceptance = $false 147 | 148 | # External dependent modules of this module 149 | # ExternalModuleDependencies = @() 150 | 151 | } # End of PSData hashtable 152 | 153 | } # End of PrivateData hashtable 154 | 155 | # HelpInfo URI of this module 156 | # HelpInfoURI = '' 157 | 158 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 159 | # DefaultCommandPrefix = '' 160 | 161 | } 162 | 163 | -------------------------------------------------------------------------------- /GenXdev.FileSystem.psm1: -------------------------------------------------------------------------------- 1 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\AssurePester.ps1" 2 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Expand-Path.ps1" 3 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Find-DuplicateFiles.ps1" 4 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Find-Item.ps1" 5 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Invoke-Fasti.ps1" 6 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Move-ItemWithTracking.ps1" 7 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Move-ToRecycleBin.ps1" 8 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Remove-AllItems.ps1" 9 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Remove-ItemWithFallback.ps1" 10 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Remove-OnReboot.ps1" 11 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Rename-InProject.ps1" 12 | . "$PSScriptRoot\Functions\GenXdev.FileSystem\Start-RoboCopy.ps1" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2021-2025 GenXdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |