Copying subfolders and files, checking "last modified time" - powershell

I have made a backup script that:
Reads source file paths and destination folder path from an XML file
Checks if source file and destination path exist (for each file)
Checks if source file (same name) exists in the target folder
Checks the last modified date of every source and destination file, if the file exists in the target folder
Copies source files to the target folder if the file does not already exist, or if the source file is newer than the existing file in the destination folder, otherwise does nothing
This only works on source files, if a source folder is specified in the XML file, only that folder will be copied, and none of its content.
I don't want to use Copy-Item -Recurse because I want to check the last modified date of every item, and if it fails the above conditions I don't want to copy it at all.
This brings me to Get-ChildItem -Recurse to list everything, but I'm having trouble coming up with something that works for this example:
C:\powershell\test\ (XML specified source)
Underlying structure:
C:\powershell\test\xmltest2.xml
C:\powershell\test\test2\xmltest.xml
C:\powershell\test\test3\test4\xmltest3.xml
etc.
i.e. I want to check every file before copying it but if say a folder has not been modified but a file inside it should be copied it should still work, AND retain the same folder structure.
Any ideas? :)

As Ansgar Wiechers says, you are reinventing the wheel, RoboCopy will do it much more easily. RoboCopy can also copy the security permissions and the created/modified dates as well, which is great. Relevant RoboCopy discussion: https://superuser.com/a/445137/67909
Still, it's not as fun as writing it yourself, eh? I've come up with this:
# Assuming these two come from your XML config, somehow
$xmlSrc = "c:\users\test\Documents\test1"
$xmlDestPath = "c:\users\test\Documents\test2"
#==========
# Functions
#==========
function process-file ($item) {
#$item should be a string, full path to a file
#e.g. 'c:\users\test\Documents\file.txt'
# Make the destination file full path
$destItem = $item.ToLower().Replace($xmlSrc, $xmlDestPath)
if (-not (Test-Path $destItem)) { #File doesn't exist in destination
#Is there a folder to put it in? If not, make one
$destParentFolder = Split-Path $destItem -Parent
if (-not (Test-Path $destParentFolder)) { mkdir $destParentFolder }
# Copy file
Copy-Item $item -Destination $destParentFolder -WhatIf
} else { #File does exist
if ((Get-Item $item).LastAccessTimeUtc -gt (Get-Item $destItem).LastAccessTimeUtc) {
#Source file is newer, copy it
$destParentFolder = Split-Path $destItem -Parent
Copy-Item $item -Destination $destParentFolder -Force -WhatIf
}
}
}
function process-directory($dir) {
# Function mostly handles "copying" empty directories
# Otherwise it's not really needed
# Make the destination folder path
$destDir = $dir.ToLower().Replace($xmlSrc, $xmlDestPath)
# If that doesn't exist, make it
if (-not (Test-Path $destDir)) { mkdir $destDir -whatif }
}
#==========
# Main code
#==========
if ((Get-Item $xmlSrc).PsIsContainer) {
# You specified a folder
Get-ChildItem $xmlSrc -Recurse | ForEach {
if ($_.PsIsContainer) {
process-directory $_.FullName
} else {
process-file $_.FullName
}
}|Out-Null
} else {
# You specified a file
process-file $xmlSrc
}
NB. The copies are -WhatIf so it won't do anything drastic. And it has two immediate problems:
It makes everything lowercase. Otherwise you have to match the case properly because .Replace() is case sensitive.
I used .Replace() because -replace treats the \ in the file path as part of a regular expression and doesn't work. There's probably an escape-string commandlet to fix this, but I haven't looked for one.
If you put \ at the end of the $xmlSrc or $xmlDestPath it will fall over.

Related

Verify file copied using copy-item

i'm trying to copy file from source to destination any verify if file copied or not.
But the problem is if i make changes inside the source file which was copied earlier then destination file not getting override when i execute the code again. Also i want log file each time files are copied.
Files in folder:- .csv, .log, .png, .html
$source="C:\52DWM93"
$destination="C:\Temp\"
Copy-Item -Path $source -Destination $destination -Force
$ver=(Get-ChildItem -file -path $destination -Recurse).FullName | foreach {get-filehash $_ -Algorithm md5}
If($ver)
{Write-Host "ALL file copied"}
else
{Write-Host "ALL file not copied"}
If you copy directories like this you need the -Recurse switch for Copy-Item. Without it you're not going to copy anything except the directory itself.
You can of course also use Get-ChildItem with whatever filter or Recurse flag you care about and pipe that into Copy-Item.
Use the *-FileCatalog cmdlets for verification.

Remove everything before \

I need to copy a lot of files and use the same sort of folder structure where the files needs to go.
So for instance if I have the following two documents:
\\Server1\Projects\OldProject\English\Text_EN.docx
\\Server1\Projects\OldProject\English\Danish\Text_DA.docx
I would need to move them to a new place on the server, but they need to be in the same "language folder". So I need to move them like this:
\\Server1\Projects\OldProject\English\Text_EN.docx -> \\Server1\Projects\NewProject\English\Text_EN.docx
\\Server1\Projects\OldProject\English\Danish\Text_DA.docx -> \\Server1\Projects\NewProject\English\Danish\Text_DA.docx
The issue here is, that I would need to take names of the "language" folder and create them in the NewProject folder.
How would I be able to take and remove everything before the \, so I end up with only having the "language" folders like English\ and English\Danish
If the goal it to just replace the 'OldProject' folder with 'NewProject' in the file path you could use replace to make the change to the file path:
$filePath = Get-ChildItem \\Server1\Projects\OldProject\English\Text_EN.docx
Copy-Item $filePath.FullName -Destination ($filepath.FullName -replace "\bOldProject\b", "NewProject")
The '\b' is used to do a regex EXACT match for anything inside the tags.
Try the following, which, for each input file:
constructs the target dir. path by replacing the old project dir.'s root path with the new one's, thereby effectively replicating the old dir.'s subdirectory structure.
makes sure that the target dir. exists
then copies the input file to the target dir.
$oldProjectRoot = '\\Server1\Projects\OldProject'
$newProjectRoot = '\\Server1\Projects\NewProject'
Get-ChildItem -Recurse -Filter *.docx -LiteralPath $oldProjectRoot |
ForEach-Object {
# Construct the target dir. path, with the same relative path
# as the input dir. path relative to the old project root.
$targetDir =
$newProjectRoot + $_.Directory.FullName.Substring($oldProjectRoot.Length)
# Create the target dir., if necessary (-Force returns any preexisting dir.)
$null = New-Item -Force -Type Directory $targetDir
$_ # Pass the input file through.
} |
Copy-Item -Destination { $targetDir } -WhatIf
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.

Copy-Item with overwrite?

Here is a section of code from a larger script. The goal is to recurse through a source directory, then copy all the files it finds into a destination directory, sorted into subdirectories by file extension. It works great the first time I run it. If I run it again, instead of overwriting existing files, it fails with this error on each file that already exists in the destination:
Copy-Item : Cannot overwrite the item with itself
I try, whenever possible, to write scripts that are idempotent but I havn't been able to figure this one out. I would prefer not to add a timestamp to the destination file's name; I'd hate to end up with thirty versions of the exact same file. Is there a way to do this without extra logic to check for a file's existance and delete it if it's already there?
## Parameters for source and destination directories.
$Source = "C:\Temp"
$Destination = "C:\Temp\Sorted"
# Build list of files to sort.
$Files = Get-ChildItem -Path $Source -Recurse | Where-Object { !$_.PSIsContainer }
# Copy the files in the list to destination folder, sorted in subfolders by extension.
foreach ($File in $Files) {
$Extension = $File.Extension.Replace(".","")
$ExtDestDir = "$Destination\$Extension"
# Check to see if the folder exists, if not create it
$Exists = Test-Path $ExtDestDir
if (!$Exists) {
# Create the directory because it doesn't exist
New-Item -Path $ExtDestDir -ItemType "Directory" | Out-Null
}
# Copy the file
Write-Host "Copying $File to $ExtDestDir"
Copy-Item -Path $File.FullName -Destination $ExtDestDir -Force
}
$Source = "C:\Temp"
$Destination = "C:\Temp\Sorted"
You are trying to copy files from a source directory to a sub directory of that source directory. The first time it works because that directory is empty. The second time it doesn't because you are enumerating files of that sub directory too and thus attempt to copy files over themselves.
If you really need to copy the files into a sub directory of the source directory, you have to exclude the destination directory from enumeration like this:
$Files = Get-ChildItem -Path $Source -Directory |
Where-Object { $_.FullName -ne $Destination } |
Get-ChildItem -File -Recurse
Using a second Get-ChildItem call at the beginning, which only enumerates first-level directories, is much faster than filtering the output of the Get-ChildItem -Recurse call, which would needlessly process each file of the destination directory.

Copying files to directory whilst retaining directory structure from list

Good afternoon all,
I'm guessing this is super easy but really annoying for me; I have a text file with a list of files, in the same folders there are LOTS of other files but I only need specific ones.
$Filelocs = get-content "C:\Users\me\Desktop\tomove\Code\locations.txt"
Foreach ($Loc in $Filelocs){xcopy.exe $loc C:\Redacted\output /s }
I figured this would go through the list which is like
"C:\redacted\Policies\IT\Retracted Documents\Policy_Control0.docx"
and then move and create the folder structure in a new place and then copy the file, it doesn't.
Any help would be appreciated.
Thanks
RGE
xcopy can't know the folder structure when you explicitly pass source file path instead of a source directory. In a path like C:\foo\bar\baz.txt the base directory could be any of C:\, C:\foo\ or C:\foo\bar\.
When working with a path list, you have to build the destination directory structure yourself. Resolve paths from text file to relative paths, join with destination directory, create parent directory of file and finally use PowerShell's own Copy-Item command to copy the file.
$Filelocs = Get-Content 'locations.txt'
# Base directory common to all paths specified in "locations.txt"
$CommonInputDir = 'C:\redacted\Policies'
# Where files shall be copied to
$Destination = 'C:\Redacted\output'
# Temporarily change current directory -> base directory for Resolve-Path -Relative
Push-Location $CommonInputDir
Foreach ($Loc in $Filelocs) {
# Resolve input path relative to $CommonInputDir (current directory)
$RelativePath = Resolve-Path $Loc -Relative
# Resolve full target file path and directory
$TargetPath = Join-Path $Destination $RelativePath
$TargetDir = Split-Path $TargetPath -Parent
# Create target dir if not already exists (-Force) because Copy-Item fails
# if directory does not exist.
$null = New-Item $TargetDir -ItemType Directory -Force
# Well, copy the file
Copy-Item -Path $loc -Destination $TargetPath
}
# Restore current directory that has been changed by Push-Location
Pop-Location
Possible improvements, left as an exercise:
Automatically determine common base directory of files specified in "locations.txt". Not trivial but not too difficult.
Make the code exception-safe. Wrap everything between Push-Location and Pop-Location in a try{} block and move Pop-Location into the finally{} block so the current directory will be restored even when a script-terminating error occurs. See about_Try Catch_Finally.

Rename a bulk of files based on a txt file

I am trying to rename some configuration files that reside into a folder. Some of them have the ".disabled" extentions, some don't.
My intention: foreach file in files-to-change.txt (relative path to the .config file, one under the other), if the file has the ".disabled" extension, remove it, if it doesn't have it, add it. This needs to apply only to the files in the .txt source file.
Basically the files
app_config\file1.config
app_config\CBS\file2.config.disabled
app_config\file3.config
app_config\CBD\Testing\file4.config.disabled
reside in the txt file and they need to match with the files in the destination folder in which I need to change the extension.
I miss the login in creating a proper script to have this completed.
where it says -path you will need to change this to reflect the location of your files.
You can use rename-item cmdlet, here is a simple script i created :
$name=(Get-ChildItem -Path 'C:\testing\New folder\').FullName
foreach ($item in $name) {
if ($item.Contains("disabled"))
{
rename-item -Path $item -NewName $item.Replace(".disabled","") -ErrorAction SilentlyContinue
}
else
{
rename-item -path $item -newname $item.Replace(".config.",".config.disabled.") -ErrorAction SilentlyContinue
}
}