Private/Tools.Copy.ps1
function Copy-FileWithBackup{ <# .DESCRIPTION Copy a simple file to a destination. If the destination exists, the source file will cover the destination file. Before the covering, the destination file will be backup to a directory. .PARAMETER Path The source file path. .PARAMETER Destination The destination file path. .PARAMETER BackupDir The backup directory path. .PARAMETER Indicator The indicator string to indicate the backup file. .OUTPUTS None. .NOTES Only support a simple file. Do not support a directory, a file symbolic link, or a hard link. .LINK Refer to the [question and answer](https://stackoverflow.com/a/77062276/17357963) for type assignment/converting. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [FormattedFileSystemPath]$Path, [Parameter(Mandatory)] [string]$Destination, [Parameter(Mandatory)] [FormattedFileSystemPath]$BackupDir, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Indicator ) if(!$Path.IsFile){ throw "The $Path should be a file." } if($Path.IsSymbolicLink -or $Path.IsHardLink){ throw "The $Path should not be a symbolic link or a hard link." } $source = $Path $destination = $Destination if ($PSCmdlet.ShouldProcess("Then copy $source to $destination. Backup $destination if it exists already.",'','')){ if(Test-Path $destination){ [psobject] $destination = [FormattedFileSystemPath]::new($destination) if(!$destination.IsFile){ throw "The $destination should be a file." } if($destination.IsSymbolicLink -or $destination.IsHardLink){ throw "The $destination should not be a symbolic link or a hard link." } $destination_backup = "$BackupDir/$Indicator-$($destination.ToShortName())" Write-Log "Copy-Item -Path $destination -Destination $destination_backup -Force" Copy-Item -Path $destination -Destination $destination_backup -Force }else{ Write-Log "Copy-Item -Path $source -Destination $destination -Force" Copy-Item -Path $source -Destination $destination -Force } } } function Copy-DirWithBackup{ <# .DESCRIPTION Copy a simple directory to a destination. If the destination exists, the source directory will be merged to the destination directory. Before the merging, both the source and destination will be backup to a directory. .PARAMETER Path The source directory path. .PARAMETER Destination The destination directory path. .PARAMETER BackupDir The backup directory path. .PARAMETER Indicator The indicator string to indicate the backup directory. .NOTES Only support a simple directory. Do not support a file, a directory symbolic link, or a junction point. Try Robocopy first, if failed, use Copy-Item instead. But Copy-Item will loss the metadata of directories in the source. Patched to avoid Copy-Item's ambiguity: If the destination is existing, Copy-Item will merge the source to the destination. (Items within the source will be copied and to the destination, covering the existing items with the same name.) If the destination is non-existing, Copy-Item will create a new directory with the source's name. .LINK Refer to the [question and answer](https://stackoverflow.com/a/77062276/17357963) for type assignment/converting. Refer to the [doc](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy) for more information about Copy-Item. |Robocopy args| effections| |---|---| | /e | Copies subdirectories. This option automatically includes empty directories.| | /copyall | Copies all file information (equivalent to /copy:DATSOU).| | /dcopy:DATE | Specifies what to copy in directories. D-Data A-Attributes T-Time stamps E-Extended attribute| | /log+:<logfile> | Writes the status output to the log file (appends the output to the existing log file).| #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [FormattedFileSystemPath]$Path, [Parameter(Mandatory)] [string]$Destination, [Parameter(Mandatory)] [FormattedFileSystemPath]$BackupDir, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Indicator ) if(!$Path.IsDir){ throw "The $Path should be a directory." } if($Path.IsSymbolicLink -or $Path.IsJunction){ throw "The $Path should not be a symbolic link or a junction point." } $source = $Path $destination = $Destination if ($PSCmdlet.ShouldProcess("Then copy $source to $destination. Backup $source and $destination if the latter exists already.",'','')){ if(Test-Path $destination){ $source_backup = "$BackupDir/$Indicator-$($source.ToShortName())" [psobject] $destination = [FormattedFileSystemPath]::new($destination) if(!$destination.IsDir){ throw "The $destination should be a directory." } if($destination.IsSymbolicLink -or $destination.IsJunction){ throw "The $destination should not be a symbolic link or a junction point." } $destination_backup = "$BackupDir/$Indicator-$($destination.ToShortName())" try { Assert-AdminRobocopyAvailable $log_file = Get-LogFileName "Robocopy Copy-DirWithBackup" Robocopy $source $source_backup /e /copyall /dcopy:DATE /log+:"$log_file" Robocopy $destination $destination_backup /e /copyall /dcopy:DATE /log+:"$log_file" Robocopy $source $destination /e /copyall /dcopy:DATE /log+:"$log_file" } catch { Write-Log "[Try Robocopy Failed] Use Copy-Item instead. But Copy-Item will loss the metadata of directories in the source." New-Item -Path $source_backup -ItemType Directory -ErrorAction SilentlyContinue | Out-Null Write-Log " Copy-Item -Path `"$source\*`" -Destination $source_backup -Recurse -Force" Copy-Item -Path "$source\*" -Destination $source_backup -Recurse -Force New-Item -Path $destination_backup -ItemType Directory -ErrorAction SilentlyContinue | Out-Null Write-Log "Copy-Item -Path `"$destination\*`" -Destination $destination_backup -Recurse -Force" Copy-Item -Path "$destination\*" -Destination $destination_backup -Recurse -Force Write-Log "Copy-Item -Path `"$source\*`" -Destination $destination -Recurse -Force" Copy-Item -Path "$source\*" -Destination $destination -Recurse -Force } }else{ Write-Log "Copy-Item -Path $source -Destination $destination -Recurse -Force" Copy-Item -Path $source -Destination $destination -Recurse -Force } } } |