#=============================================================================== # # DIRECTORY: # (variable) # # FILE: # copyfiles.ps1 (or copyfiles.exe) # # USAGE: # Step 1a (cmd.exe as Administrator): # C:\>powershell -ExecutionPolicy "Unrestricted" # Step 1b: # PS C:\> E:\copyfiles.ps1 [Options] # or # PS C:\> E:\copyfiles.ps1 -s[ourcepath] -d[estinationpath] [Options] # # OR: # # Step 2a (powershell.exe as Administrator): # PS C:\> Set-ExecutionPolicy "Unrestricted" # Step 2b: # PS C:\> E:\copyfiles.ps1 [Options] # or # PS C:\> E:\copyfiles.ps1 -s[ourcepath] -d[estinationpath] [Options] # # OPTIONS: # -l[ogfilepath] : relative or absolute path to the logfile # -i[nfofilepath] : relative or absolute path to the infofile # -v[erify] : turn on output of success # -q[uiet] : turn off all output # -h[elp] : show a short help # # EXIT STATES: # True (0) = success # False (1) = sourcepath is empty # False (2) = sourcepath has to exist # False (3) = destinationpath is empty # False (4) = cannot create destinationpath there # False (5) = destinationpath already exist # False (6) = logfilepath and infofilepath are equal # False (7) = logfilepath already exist # False (8) = cannot create logfile there # False (9) = unsupported extension for logfile # False (10) = infofilepath already exist # False (11) = cannot create infofile there # False (12) = unsupported extension for infofile # # DESCRIPTION: # Copy a file or directory (incl. files) from a given sourcepath # to a given destinationpath and verify that the content of the # file(s) has not changed. # Optionally output success in addition to fails. # Optionally write metadata to an infofile and/or each step # into a logfile. # Optionally suppress all output. # # REQUIREMENTS: # Powershell 4.0 (first Version with Get-FileHash commandlet) # # BUGS: # --- # # NOTES: # Syntax checked with PSScriptAnalyzer: # PS C:\> Invoke-ScriptAnalyzer E:\copyfiles.ps1 # # Tested on # - Microsoft Windows 10 (1703) Pro (x64) # with # - PowerShell Version 5.1.15063.608 ($PSVersionTable.PSVersion) # # AUTHOR: # Patrick Neumann, patrick@neumannsland.de # # COMPANY: # (privately) # # VERSION: # 0.9.0 (beta) # # LINK TO THE MOST CURRENT VERSION: # https://git.neumannsland.de/casualscripter/copyfiles # # CREATED: # 2017-09-29 # # COPYRIGHT (C): # 2017 - Patrick Neumann # # LICENSE: # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # WARRANTY: # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # TODO: # - More testing (Powershell on macOS and/or ArchLinux) # # HISTORY: # 0.9.0 - Patrick Neumann - Initial (for the peer reviewer eyes only) release # #=============================================================================== #=== CONFIGURATION (user) ====================================================== # Has to be at the top of the code (excl. comments)! [CmdletBinding()] Param( [Parameter(Position=1)] # "Mandatory=$True" vs. easy -help [string]$sourcepath, [Parameter(Position=2)] # "Mandatory=$True" vs. easy -help [string]$destinationpath, [string]$logfilepath, # like log= support of dc3dd [string]$infofilepath, # like hlog= support of dc3dd [switch]$verify, # like --check support in GNU md5sum/sha1sum [switch]$quiet, [switch]$help ) #=== CONFIGURATION (dynamic) =================================================== # Date of start Get-Date -Format d | New-Variable -Name CURDATE -Option constant # Time of start Get-Date -Format T | New-Variable -Name CURTIME -Option constant # Timezone of start (as difference from UTC because CEST is not supported) Get-Date -UFormat "UTC%Z" | New-Variable -Name CURTZ -Option constant #=== CONFIGURATION (static) ==================================================== # Filename of the script New-Variable -Name MYNAME -Value $MyInvocation.MyCommand.Name -Option constant # Version of the script New-Variable -Name VERSION -Value "0.9.0" -Option constant # Creation date of the script New-Variable -Name CREATED -Value "2017-09-29" -Option constant #------------------------------------------------------------------------------- # First output. #------------------------------------------------------------------------------- # nmap like output: # Starting Nmap 7.40 ( https://nmap.org ) at 2017-09-26 21:13 CEST $scriptinfo = "Starting $MYNAME $VERSION ($CREATED) at $CURDATE $CURTIME $CURTZ" Write-Output "$scriptinfo`n" #------------------------------------------------------------------------------- # Help. #------------------------------------------------------------------------------- $helpstring = @" NAME copyfiles.ps1 (or copyfiles.exe) SYNOPSIS Copy a file or directory from sourcepath to destinationpath. SYNTAX PS C:\> E:\copyfiles.ps1 [Options] or PS C:\> E:\copyfiles.ps1 -s[ourcepath] -d[estinationpath] [Options] DESCRIPTION Copy a file or directory (incl. files) from a given sourcepath to a given destinationpath and verify that the content of the file(s) has not changed. Optionally output success in addition to fails. Optionally write metadata to an infofile and/or each step into a logfile. Optionally suppress all output. REQUIREMENTS Powershell 4.0 (first Version with Get-FileHash commandlet). You have to open the Powershell with unrestricted execution policy or use "a" .exe version! PARAMETERS -l[ogfilepath] Relative or absolute path to the logfile. -i[nfofilepath] Relative or absolute path to the infofile. -v[erify] Turn on output of success (in addition to fails) -q[uiet] Turn off all output. -h[elp] Show this help. "@ If($help){ Write-Output $helpstring Exit $True } #=== FUNCTION ================================================================== # NAME: Write-Error-and-Exit-PN # DESCRIPTION: Write red text to the console and exit with individual exitcode. # PARAMETER 1: string # PARAMETER 2: integer #=============================================================================== Function Write-Error-and-Exit-PN { [CmdletBinding()] Param( [Parameter(Mandatory=$True, Position=1)] [string]$message, [Parameter(Mandatory=$True, Position=2)] [string]$exitcode ) # If you want color you have to use Write-Host! # (But be aware: Write-Host will not be available on "every host"!) Write-Host " ERROR: $message ... EXIT!`n" -ForegroundColor Red Write-Output $helpstring Exit $exitcode } #------------------------------------------------------------------------------- # Checks for sourcepath. #------------------------------------------------------------------------------- if(-Not $sourcepath){ Write-Error-and-Exit-PN "sourcepath is empty" 1 } if(-Not [System.IO.Path]::IsPathRooted($sourcepath)){ # convert relative into absolut path $sourcepath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($sourcepath) } if(-Not (Test-Path $sourcepath)){ Write-Error-and-Exit-PN "sourcepath has to exist" 2 } #$sourcepathitem = Get-Item $sourcepath ## if a file, just copy it #$sourcepathitem -is [System.IO.FileInfo] ## after copy a symlink is a file! ## if a folder, create it and copy the childitems into it #$sourcepathitem -is [System.IO.DirectoryInfo] ## or only copy content to the destinationpath (tailing backslash) #$sourcepath.EndsWith([IO.Path]::DirectorySeparatorChar) ## other items are not supported #------------------------------------------------------------------------------- # Checks for destinationpath. #------------------------------------------------------------------------------- if(-Not $destinationpath){ Write-Error-and-Exit-PN "destinationpath is empty" 3 } if(-Not [System.IO.Path]::IsPathRooted($destinationpath)){ # convert relative into absolut path $destinationpath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($destinationpath) } if(-Not (Test-Path (Split-Path $destinationpath))){ Write-Error-and-Exit-PN "cannot create destinationpath there" 4 } # It is not a good idea to use an existing destination because of the # possiblity of mixing traces from different cases! If(Test-Path $destinationpath){ Write-Error-and-Exit-PN "destinationpath already exist" 5 } #------------------------------------------------------------------------------- # Like dc3dd you have to give "log" or "hlog" to activate "log" or "info". #------------------------------------------------------------------------------- # Checks for logfilepath != infofilepath. #------------------------------------------------------------------------------- if($logfilepath -and $infofilepath){ if($logfilepath -ceq $infofilepath){ Write-Error-and-Exit-PN "logfilepath and infofilepath are equal" 6 } } #------------------------------------------------------------------------------- # Checks for logfilepath. #------------------------------------------------------------------------------- # TODO: # Set "logfile.txt" in the destinationpath if a path is missing after "-l". #------------------------------------------------------------------------------- $logfileextensions = @(".log", ".txt") if($logfilepath){ # It is not a good idea to overwrite an existing logfile! if(Test-Path $logfilepath){ Write-Error-and-Exit-PN "logfilepath already exist" 7 } if(-Not [System.IO.Path]::IsPathRooted($logfilepath)){ # convert relative into absolut path $logfilepath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($logfilepath) } if(-Not (Test-Path (Split-Path $logfilepath))){ Write-Error-and-Exit-PN "cannot create logfile there" 8 } # Check extension for logfile (.log or .txt) if($logfileextensions -notcontains [System.IO.Path]::GetExtension($logfilepath)){ Write-Error-and-Exit-PN "unsupported extension for logfile" 9 } } #------------------------------------------------------------------------------- # Checks for infofilepath. #------------------------------------------------------------------------------- # TODO: # Set "infofile.csv" in the destinationpath if a path is missing after "-i". #------------------------------------------------------------------------------- $infofileextensions = @(".csv", ".txt") if($infofilepath){ # It is not a good idea to overwrite an existing infofile! if(Test-Path $infofilepath){ Write-Error-and-Exit-PN "infofilepath already exist" 10 } if(-Not [System.IO.Path]::IsPathRooted($infofilepath)){ # convert relative into absolut path $infofilepath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($infofilepath) } if(-Not (Test-Path (Split-Path $infofilepath))){ Write-Error-and-Exit-PN "cannot create infofile there" 11 } # Check extension for infofile (.csv or .txt) if($infofileextensions -notcontains [System.IO.Path]::GetExtension($infofilepath)){ Write-Error-and-Exit-PN "unsupported extension for infofile" 12 } } #------------------------------------------------------------------------------- # Start write to logfile if "-l" is given. #------------------------------------------------------------------------------- if($logfilepath){ "$scriptinfo`n" | Out-File $logfilepath } #------------------------------------------------------------------------------- # Start write to infofile if "-i" is given. #------------------------------------------------------------------------------- if($infofilepath){ "# $scriptinfo" | Out-File $infofilepath $csvtablehead = '"FullName"' $csvtablehead += ',"Length"' $csvtablehead += ',"Mode"' $csvtablehead += ',"Attributes"' $csvtablehead += ',"SymbolicLinkTarget"' $csvtablehead += ',"LastWriteTime"' $csvtablehead += ',"LastAccessTime"' $csvtablehead += ',"CreationTime"' $csvtablehead += ',"Owner"' $csvtablehead += ',"Group"' $csvtablehead += ',"MD5"' $csvtablehead += ',"SHA1"' $csvtablehead | Out-File $infofilepath -Append } #------------------------------------------------------------------------------- # We want as much as possible (and skip only devices and don't want errors). #------------------------------------------------------------------------------- $sourceroot = Get-Item -Path $sourcepath 2>$null if($sourceroot -Is [System.IO.FileInfo]){ # copy only one file } elseif($sourceroot -Is [System.IO.DirectoryInfo]){ $sourcefiles = Get-ChildItem -Path $sourcepath ` -Recurse ` -Attributes ReadOnly, Hidden, System, Directory, Archive, Normal, Temporary, SparseFile, ReparsePoint, Compressed, Offline, NotContentIndexed, Encrypted, IntegrityStream, NoScrubData 2>$null } else{ # type of source not supported } #------------------------------------------------------------------------------- # Main. #------------------------------------------------------------------------------- New-Item $destinationpath -ItemType "Directory" | Out-Null if($logfilepath){ "Destination path successfully created.`n" | Out-File $logfilepath -Append } ForEach($sourcefile in $sourcefiles){ # md5 and sha1 secure like sha256... # Tee-Object w/o process substitution... if($sourcefile -Is [System.IO.FileInfo]){ $md5ofsource = (Get-FileHash -Path $sourcefile.FullName -Algorithm MD5).hash $sha1ofsource = (Get-FileHash -Path $sourcefile.FullName -Algorithm SHA1).hash } if($infofilepath){ $line = new-object PSObject $line | add-member -membertype NoteProperty -name "FullName" -value $sourcefile.FullName $line | add-member -membertype NoteProperty -name "Mode" -value $sourcefile.Mode $line | add-member -membertype NoteProperty -name "Attributes" -value $sourcefile.Attributes if($sourcefile.LinkType -eq "SymbolicLink"){ $line | add-member -membertype NoteProperty -name "SymbolicLinkTarget" -value $sourcefile.Target } $line | add-member -membertype NoteProperty -name "LastWriteTime" -value $sourcefile.LastWriteTime $line | add-member -membertype NoteProperty -name "LastAccessTime" -value $sourcefile.LastAccessTime $line | add-member -membertype NoteProperty -name "CreationTime" -value $sourcefile.CreationTime $line | add-member -membertype NoteProperty -name "Owner" -value (Get-Acl $sourcefile.FullName).Owner $line | add-member -membertype NoteProperty -name "Group" -value (Get-Acl $sourcefile.FullName).Group if($sourcefile -Is [System.IO.FileInfo]){ $line | add-member -membertype NoteProperty -name "Length" -value $sourcefile.Length $line | add-member -membertype NoteProperty -name "MD5" -value $md5ofsource $line | add-member -membertype NoteProperty -name "SHA1" -value $sha1ofsource } $line | Export-Csv $infofilepath -Delimiter "," -Append -Encoding UTF8 -NoTypeInformation -Force $line = $null } # os independent path directory separator # and # -Replace = RegExp und .Replace() = Strings $destinationfile = [System.IO.FileInfo](($destinationpath, [IO.Path]::DirectorySeparatorChar, $sourceroot.Name, [IO.Path]::DirectorySeparatorChar, $sourcefile.FullName.Replace($sourcepath, "")) -join "") # Verify (= Verbose) und Quiet funktioniert so nicht wirklich einfach!? # Erst msg bauen und dann nix, ausgabe oder in datei mit if!? Write-Host "$($destinationfile.FullName): " -NoNewline # Am besten gleich zusammen mit: # Logfile support: "Out-File -Append" Copy-Item $sourcefile.FullName $destinationfile.FullName | Out-Null if($destinationfile.Exists){ #$destinationfile.Mode = $sourcefile.Mode # is ReadOnly :-( $destinationfile.Attributes = $sourcefile.Attributes $destinationfile.LastWriteTime = $sourcefile.LastWriteTime $destinationfile.LastAccessTime = $sourcefile.LastAccessTime $destinationfile.CreationTime = $sourcefile.CreationTime Set-Acl $destinationfile.FullName -AclObject (Get-Acl $sourcefile.FullName) if($destinationfile -Is [System.IO.FileInfo]){ $md5ofdestination = (Get-FileHash -Path $destinationfile.FullName -Algorithm MD5).hash $sha1ofdestination = (Get-FileHash -Path $destinationfile.FullName -Algorithm SHA1).hash if(($md5ofdestination -eq $md5ofsource) -and ($sha1ofdestination -eq $sha1ofsource)){ Write-Host "OK" -ForegroundColor Green } else{ Write-Host "Failed (hash)" -ForegroundColor Red } } if($destinationfile -Is [System.IO.DirectoryInfo]){ Write-Host "OK" -ForegroundColor Green } } else{ Write-Host "Failed (copy)" -ForegroundColor Red } } exit $True