diff --git a/copyfiles.ps1 b/copyfiles.ps1 new file mode 100755 index 0000000..78acb9b --- /dev/null +++ b/copyfiles.ps1 @@ -0,0 +1,465 @@ +#=============================================================================== +# +# DIRECTORY: +# (variable) +# +# FILE: +# copyfiles.ps1 (or copyfiles.exe) +# +# USAGE: +# Step 1 (cmd.exe): +# C:\>powershell -ExecutionPolicy Unrestricted +# Step 2: +# 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: +# (Sorry, I bet, I'm not allowed to publish it over GitHub!) +# +# 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 \ No newline at end of file