Public/Update-ALHSysinternalsSuite.ps1
<#PSScriptInfo
.VERSION 2.0.0 .GUID 37ba3cb8-5fdd-4b1b-beb1-06a8f4c09c6f .AUTHOR Dieter Koch .COMPANYNAME .COPYRIGHT (c) 2021-2023 Dieter Koch .TAGS .LICENSEURI https://github.com/admins-little-helper/ALH/blob/main/LICENSE .PROJECTURI https://github.com/admins-little-helper/ALH .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 1.0.0 - Initial release 1.1.0 - Implemented file compare to copy only changed files. 1.2.0 - Implemented parameters 'Clean' and 'CleanAll'. - Cleand up code 1.3.0 - Added check for starting WebClient service 1.4.0 - Cleaned up code 1.5.0 - Added pipeline support for destination path 2.0.0 - Changed how the script retrieves source files. Instead of using downloading indiviual files via WebDav, now by default the full ZipFile is downloaded and used as source. #> <# .DESCRIPTION Contains a function to install or update SysinternalsSuite tools. .LINK https://sysinternals.com .LINK https://live.sysinternals.com #> function Update-ALHSysinternalsSuite { <# .SYNOPSIS Installs or updates SysinternalsSuite tools. .DESCRIPTION Installs or updates SysinternalsSuite tools either from a given source path or from https://live.sysinternals.com. The function compares the last modified date of the files in the source and destination path and only copies newer files from the source path. .PARAMETER SourcePath Specifies the source path from where to copy SysinternalsSuite tools. If not specified, 'SysinternalsSuite.zip' from http://download.sysinternals.com/files/ will be downloaded and used as source. Allows to specify the keyword 'WebDav' which will then set the SourcePath to 'https://live.sysinternals.com/tools'. .PARAMETER DestinationPath Specifies the destination path to which the files should be copied. The destination folder will be created in case the it does not yet exist. Multiple destination paths can be specified to update SysinternalsSuite for example on multiple remote systems. .PARAMETER Clean If specified, any file in the destination folder that does not exist in the source folder will be deleted before the update. .PARAMETER CleanAll If specified, all files in the destination folder will be deleted before copying files from the source. .EXAMPLE Update-ALHSysinternalsSuite -DestinationPath C:\Admin\SysinternalsSuite Install Sysinternals tools by downloading the 'SysinternalsSuite.zip' from http://download.sysinternals.com/files/SysinternalsSuite.zip, expanding the zip file and copying the files to the specified destiantion directory 'C:\Admin\SysinternalsSuite'. .EXAMPLE Update-ALHSysinternalsSuite -SourcePath \\server\share\SysinternalsSuiteFolder -DestinationPath C:\Admin\SysinternalsSuite Installing or updating the SysinternalsSuite tools from a local network share. .EXAMPLE Update-ALHSysinternalsSuite -DestinationPath C:\Admin\SysinternalsSuite -Clean -Verbose Install Sysinternals tools by downloading the 'SysinternalsSuite.zip' from http://download.sysinternals.com/files/SysinternalsSuite.zip, expanding the zip file and copying the files to the specified destination directory 'C:\Admin\SysinternalsSuite'. All files existing in the destination path that are not found in the source zip file, will be removed. .EXAMPLE Update-ALHSysinternalsSuite -DestinationPath C:\Admin\SysinternalsSuite -CleanAll -Verbose Install Sysinternals tools by downloading the 'SysinternalsSuite.zip' from http://download.sysinternals.com/files/SysinternalsSuite.zip, expanding the zip file and copying the files to the specified destiantion directory 'C:\Admin\SysinternalsSuite'. All files already existing in the destination path will be removed. .INPUTS Nothing .OUTPUTS Nothing .NOTES Author: Dieter Koch Email: diko@admins-little-helper.de .LINK https://github.com/admins-little-helper/ALH/blob/main/Help/Update-ALHSysinternalsSuite.txt #> [Cmdletbinding(SupportsShouldProcess, DefaultParametersetName = "default")] [Alias("Install-ALHSysinternalsSuite")] param ( [ValidateScript({ if ($_ -eq 'WebDav') { # In case 'WebDav' was specified as SourcePath, set the SourcePath to the live.sysinternals.com webdav path later. $true } else { # Check if the given path is valid. if (-not (Test-Path -Path $_) ) { throw "Folder does not exist" } # Check if the given path is a directory. if (-not (Test-Path -Path $_ -PathType Container) ) { throw "The Path argument must be a folder. File paths are not allowed." } # This point is only reached if all previous checks have been passed. return $true } })] [Parameter(HelpMessage = "The path to the directory from which the SysinternalsSuite tools should be copied.")] [string] $SourcePath = "WebDav", [ValidateScript({ # Check if the given path exists and is valid. if (-not (Test-Path -Path $_) ) { Write-Warning -Message "Folder does not exist." if (Test-Path -Path $_ -IsValid) { Write-Warning -Message "The given path is a valid path string. Will create new folder." } } # Check if the given path is a directory. if (-not (Test-Path -Path $_ -PathType Container) ) { throw "The Path argument must be a folder. File paths are not allowed." } # This point is only reached if all previous checks have been passed. return $true })] [Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "The path(s) to the directory in that the SysinternalsSuite tools should be installed.")] [string[]] $DestinationPath, [Parameter(ParameterSetName = "Clean")] [switch] $Clean, [Parameter(ParameterSetName = "CleanAll")] [switch] $CleanAll ) begin { # Remember when the process started to calculate how much time following actions took. $StartTime = (Get-Date) Write-Verbose -Message "Start date/time [$StartTime]." if ($PSBoundParameters.ContainsKey('SourcePath')) { # In case the parameter 'SourcePath' was specified, check it's value. if ($SourcePath -eq 'WebDav') { # In case 'WebDav' was specified as source, we set the SysinternalsSuite WebDav source. $SourcePath = "\\live.sysinternals.com\DavWWWRoot\tools" Write-Verbose -Message "Source was specified as 'WebDav'. Set SourcePath to [$SourcePath]." } } else { # Otherwise we download the SysinternalsSuite.zip from the web, extract it to a temporary folder und use that temporary folder as source. Write-Verbose -Message "No SourcePath was specified. Downloading SysinternalsSuite.zip from Web." $TempOutputPath = "$env:TEMP\Update-ALHSysinternalsSuite" # Create the temporary output directory. $null = New-Item -Path $TempOutputPath -ItemType Directory -Force # Define the parameters for the downloading the file. $InvokeWebRequestParams = @{ Uri = "http://download.sysinternals.com/files/SysinternalsSuite.zip" OutFile = "$TempOutputPath/SysinternalsSuite.zip" } try { # Execute the download. Invoke-WebRequest @InvokeWebRequestParams if (Test-Path -Path "$TempOutputPath\SysinternalsSuite.zip") { # If the download was successful, we should have a valid zip file in the temporary folder. try { # Expand the zip file to a subdirectory of the temporary folder. Expand-Archive -Path "$TempOutputPath\SysinternalsSuite.zip" -DestinationPath "$TempOutputPath\SysinternalsSuite\" -Force $SourcePath = "$TempOutputPath\SysinternalsSuite\" } catch { throw "Error expanding file '$TempOutputPath\SysinternalsSuite.zip' to directory '$TempOutputPath\SysinternalsSuite\'." } } } catch { throw "Something went wrong downloading SysinternalsSuite.zip from '$($InvokeWebRequestParams.Uri)'" } } # Check the SourcePath. if ($SourcePath -eq "\\live.sysinternals.com\DavWWWRoot\tools" -or $SourcePath -match "^https?://") { # Seems a WebDav source was specified... Write-Verbose -Message "Detected http/https source - assuming WebDav source" Write-Verbose -Message "Checking service 'WebClient' to be able to copy data from WebDav source..." if ($SourcePath -match "^https?://") { Write-Verbose -Message "Convert URL to UNC path..." Write-Verbose -Message "SourcePath: [$SourcePath]." $SourcePath = $SourcePath -replace "^https?:", '' $SourcePath = $SourcePath -replace "/", '\' Write-Verbose -Message "Converted UNC Path: [$SourcePath]." } # Check the status of the 'WebClient' service and try to start it in case it's not running. $WebClientServiceStatus = Get-Service -Name 'WebClient' if ($null -eq $WebClientServiceStatus) { $ComputerInfo = Get-ComputerInfo -Property OSName if ($ComputerInfo.OsName -match "Server") { Write-Warning -Message "It seems the 'WebClient' service is not installed on this system." Write-Warning -Message "This system runs a Windows Server operating system. The 'WebClient' service is not installed on server os by default. You can install it using 'Install-WindowsFeature WebDav-Redirector' and reboot the system." Write-Warning -Message "Alternatively you can run 'Update-SysinternalsSuite' without specifying the parameter 'SourcePath' to download the SysinternalsSuite.zip and use that as source." throw "The service 'WebClient' seems to be not installed on this system." } } else { if (($WebClientServiceStatus.Status -ne 'Running')) { Write-Verbose -Message "Service 'WebClient' is not running. Trying to start service 'WebClient'. This requires administrator privileges and migth fail." try { Start-Service -Name WebClient -ErrorAction Stop } catch [System.Management.Automation.MethodInvocationException], [Microsoft.PowerShell.Commands.ServiceCommandException] { Write-Verbose -Message "Could not start service 'WebClient'" Write-Verbose -Message "Trying to start it indirectly by running net use..." Start-Process -FilePath "$env:WINDIR\system32\net.exe" -ArgumentList "use $SourcePath" -Wait -WindowStyle Hidden } catch { Write-Error -Message "Unknown error occured" } } else { Write-Verbose -Message "Service 'WebClient' is running already." } } } Write-Information -MessageData $("`nElapsed time for preparing update: {0:0.##}" -f $((((Get-Date) - $StartTime).Totalseconds))) -InformationAction Continue } process { foreach ($DestinationPathElement in $DestinationPath) { Write-Verbose -Message "`nWorkign on destination path [$DestinationPathElement]." # Remember when we start processing the current destination path. $LapTime = (Get-Date) # Define some counters for showing statistics. $FileCounter = @{ UpdateCounter = 0 SameVersionCounter = 0 NewCounter = 0 RemoveCounter = 0 } # Check if the destination path exists. if (Test-Path -Path $DestinationPathElement -ErrorAction SilentlyContinue) { Write-Verbose -Message "Destination path exists." if ($CleanAll.IsPresent) { Write-Verbose -Message "Parameter -CleanAll specified. Deleting all files from destination path first..." $FilesInDestination = Get-ChildItem -Path $DestinationPathElement -File -Force -ErrorAction Stop foreach ($DestinationFile in $FilesInDestination) { Remove-Item -Path $DestinationFile.FullName -Force -Confirm:$false $FileCounter.RemoveCounter++ } } Write-Verbose -Message "Getting list of files in source directory..." $FilesInSourceHT = [ordered]@{} $FilesInSource = Get-ChildItem -Path $SourcePath -File -Force -ErrorAction Stop foreach ($SourceFile in $FilesInSource) { $FilesInSourceHT.Add($SourceFile.Name.Trim([char]0), $SourceFile.LastWriteTime) } Write-Verbose -Message "Getting list of files in destination directory..." $FilesInDestinationHT = [ordered]@{} $FilesInDestination = Get-ChildItem -Path $DestinationPathElement -File foreach ($DestinationFile in $FilesInDestination) { $FilesInDestinationHT.Add($DestinationFile.Name.Trim([char]0), $DestinationFile.LastWriteTime) } Write-Verbose -Message "Comparing source and destination files..." $FilesInDestiantionNotInSource = foreach ($Item in $FilesInDestinationHT.Keys) { if ($FilesInDestinationHT.Contains($Item) -and !$FilesInSourceHT.Contains($Item)) { $Item } } if ($FilesInDestiantionNotInSource.Count -gt 0) { foreach ($File in $FilesInDestiantionNotInSource) { Write-Verbose -Message "This file exists in destination but not in source folder: $File" if ($Clean.IsPresent) { Write-Verbose -Message "Parameter -Clean specified. Deleting file existing in destination path that do not exist in source path." Remove-Item -Path $(Join-Path -Path $DestinationPathElement -ChildPath $file) -Force -Confirm:$false -Verbose $FileCounter.RemoveCounter++ } } } Write-Information -MessageData "`nUpdate started for destination path [$DestinationPathElement]." -InformationAction Continue foreach ($File in $FilesInSourceHT.Keys) { Write-Verbose "Checking file [$File]" if ($File) { $SourceFileDate = $FilesInSourceHT["$File"] $DestinationFileDate = $FilesInDestinationHT["$File"] $DestinationFilePath = Join-Path -Path $DestinationPathElement -ChildPath $File if ($SourceFileDate -ne $DestinationFileDate) { try { if ($null -eq $DestinationFileDate) { $FileCounter.NewCounter++ } else { $FileCounter.UpdateCounter++ } $SourceFile = Join-Path -Path $SourcePath -ChildPath $File Copy-Item -LiteralPath "$SourceFile" -Destination "$DestinationPathElement" -Force Write-Information -MessageData "Copied/Updated file: [$DestinationFilePath]" -InformationAction Continue } catch { Write-Information -MessageData "An error occurred: $_.Exception.Message" -InformationAction Continue } } else { $FileCounter.SameVersionCounter++ Write-Information -MessageData "Same version/date as source file: [$DestinationFilePath]" -InformationAction Continue } } } $FilesInDestinationHTAfterUpdate = [ordered]@{} Write-Verbose -Message "Getting files in destination after update..." $FilesInDestinationAfterUpdate = Get-ChildItem -Path $DestinationPathElement -File foreach ($DestinationFileAfterUpdate in $FilesInDestinationAfterUpdate) { $FilesInDestinationHTAfterUpdate.Add($DestinationFileAfterUpdate.Name, $DestinationFileAfterUpdate.LastWriteTime) } $FileDiff = $FilesInDestinationHTAfterUpdate.Count - $FilesInSourceHT.Count Write-Information -MessageData "Update finished..." -InformationAction Continue Write-Information -MessageData "# of files updated: $($FileCounter.UpdateCounter)" -InformationAction Continue Write-Information -MessageData "# of files with same date: $($FileCounter.SameVersionCounter)" -InformationAction Continue Write-Information -MessageData "# of files new in destination: $($FileCounter.NewCounter)" -InformationAction Continue Write-Information -MessageData "# of files total in source: $($FilesInSourceHT.Count)" -InformationAction Continue Write-Information -MessageData "# of files total in destination: $($FilesInDestinationHTAfterUpdate.Count)" -InformationAction Continue Write-Information -MessageData "# of files removed in destination: $($FileCounter.RemoveCounter)" -InformationAction Continue Write-Verbose -Message "Comparing source and destination files after update..." $FilesInDestiantionNotInSourceAfterUpdate = $FilesInDestinationHTAfterUpdate.Keys | ForEach-Object { if ($FilesInDestinationHTAfterUpdate.Contains($_) -and !$FilesInSourceHT.Contains($_)) { $_ } } if ($FilesInDestiantionNotInSourceAfterUpdate.count -gt 0) { Write-Information -MessageData "WARNING: there are more files in the destination folder than in the source folder: $FileDiff" -InformationAction Continue } else { Write-Verbose -Message "Source and destination contain the same files." } } else { # Otherwise try to create it. try { Write-Information -MessageData "Destination path does not exist. Creating it..." -InformationAction Continue New-Item -Path $DestinationPathElement -ItemType Directory -Force | Out-Null if (Test-Path -Path $DestinationPathElement) { Write-Verbose "Destination path created successfully" # Call the function recursively to finally install SysinternalsSuite. $UpdateALHSysinternalsSuiteParams = @{ SourcePath = $SourcePath DestinationPath = $DestinationPathElement # Parameters -Clean and -CleanAll are not needed here because the destination folder was just created - so it's empty for sure. } Update-ALHSysinternalsSuite @UpdateALHSysinternalsSuiteParams } else { Write-Error "Error creating destination path [$DestinationPathElement]." } } catch { Write-Information -MessageData -Message "An error occurred: $_.Exception.Message" -InformationAction Continue } } Write-Information -MessageData $("Elapsed time in seconds for current folder: {0:0.##}" -f $((((Get-Date) - $LapTime).Totalseconds))) -InformationAction Continue } } end { Write-Verbose -Message "Cleaning up..." if (Test-Path -Path "$TempOutputPath") { Remove-Item -Path "$TempOutputPath" -Force -Confirm:$false -Recurse } Write-Information -MessageData $("`nDONE - Total elapsed time in seconds: {0:0.##}" -f $((((Get-Date) - $StartTime).Totalseconds))) -InformationAction Continue } } #region EndOfScript <# ################################################################################ ################################################################################ # # ______ _ __ _____ _ _ # | ____| | | / _| / ____| (_) | | # | |__ _ __ __| | ___ | |_ | (___ ___ _ __ _ _ __ | |_ # | __| | '_ \ / _` | / _ \| _| \___ \ / __| '__| | '_ \| __| # | |____| | | | (_| | | (_) | | ____) | (__| | | | |_) | |_ # |______|_| |_|\__,_| \___/|_| |_____/ \___|_| |_| .__/ \__| # | | # |_| ################################################################################ ################################################################################ # created with help of http://patorjk.com/software/taag/ #> #endregion |