465 lines
17 KiB
PowerShell
Executable File
465 lines
17 KiB
PowerShell
Executable File
#===============================================================================
|
|
#
|
|
# DIRECTORY:
|
|
# (variable)
|
|
#
|
|
# FILE:
|
|
# copyfiles.ps1 (or copyfiles.exe)
|
|
#
|
|
# USAGE:
|
|
# Step 1 (cmd.exe):
|
|
# C:\>powershell -ExecutionPolicy Unrestricted
|
|
# Step 2:
|
|
# PS C:\> E:\copyfiles.ps1 <Sourcepath> <Destinationpath> [Options]
|
|
# or
|
|
# PS C:\> E:\copyfiles.ps1 -s[ourcepath] <Path> -d[estinationpath] <Path> [Options]
|
|
#
|
|
# OPTIONS:
|
|
# -l[ogfilepath] <Path> : relative or absolute path to the logfile
|
|
# -i[nfofilepath] <Path> : 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# 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 <Sourcepath> <Destinationpath> [Options]
|
|
or
|
|
PS C:\> E:\copyfiles.ps1 -s[ourcepath] <Path> -d[estinationpath] <Path> [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] <Path>
|
|
Relative or absolute path to the logfile.
|
|
|
|
-i[nfofilepath] <Path>
|
|
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 |