Home > Software engineering >  PowerShell Copy Files in Source Folder to Matching Destination Fails With Destination Value
PowerShell Copy Files in Source Folder to Matching Destination Fails With Destination Value

Time:09-24

I am trying to copy files in one folder to another folder. Files should go in the same folder structure under their parent folders. For example, C:\Users\Boss\One\Two\Alpha\File1.txt should go to C:\Users\Boss\One\Two\Beta\File1.txt

It fails in the last line where I specify the Destination value in a Copy-Item:

$sourcedir = "C:\Users\Boss\One\Two\Alpha"
$localdir = "C:\Users\Boss\One\Two\Beta"

# Assumes folders are established by other processes
$sourceContents = Get-ChildItem $sourcedir -Recurse | Where-Object { -not $_.PsIsContainer }
$localContents = Get-ChildItem $localdir | Where-Object { -not $_.PsIsContainer }

Compare-Object $sourceContents $localContents -Property Name,Length,LastWriteTime -PassThru | 
    Where-Object {$_.SideIndicator -eq "<="} | 
    ForEach-Object { 
        Write-Output $_.FullName, $_.Name;
        Write-Output $localdir;
        $a = Join-Path $localdir $_.FullName.SubString($sourcedir.Length);
        Write-Output $a;
        Write-Output $_;
        # Write-Outputs are just to see what's going on
        # Works as expected up until here
        Copy-Item $_ -Destination "$a"
    }

I get, in this case, an error saying, "Cannot find path 'C:\Users\Boss\File1.txt because it does not exist."

I have tried a number of things and ended up with the above trying different ways to feed the Destination value.

When I accomplish this, the source needs to become a network share. I thought of using Robocopy early on, but I need to migrate this yet again to using a URL for the source.

Any thoughts would be greatly appreciated. I program in C# .NET but I've always found PowerShell a challenge to learn.


Using Theo's example, I simplified my code to the following. Note that I moved the folders to one outside of the Users and not in any synchronised folder.

$sourcedir = "C:\Unsynchronised\Alpha"
$localdir = "C:\Unsynchronised\Beta"
# I kept the "local" because I would like to use this for server-client some day

Get-ChildItem -Path $sourcedir -Recurse -File | ForEach-Object {
    $localDirPath = Join-Path -Path $localdir -ChildPath $_.DirectoryName.Substring($sourcedir.Length)
    $localDirFile = Join-Path -Path $localDirPath -ChildPath $_.Name

    Write-Output $_.DirectoryName
    Write-Output $localDirPath
    Write-Output $localDirFile
    $_ | Copy-Item -Destination %localDirPath
}

Output gives...

C:\Unsynchronised\Alpha
C:\Unsynchronised\Beta\
C:\Unsynchronised\Beta\TestFile.txt
C:\Unsynchronised\Alpha\Folder1
C:\Unsynchronised\Beta\Folder1
C:\Unsynchronised\Beta\Folder1\Subfile_1_1.txt

Files in Beta remain unchanged despite newer ones of the same name in Alpha.

CodePudding user response:

I wouldn't use Compare-Object on this, but an if() condition inside a simple loop to test if a file already exists in the destination path and if not, copy it from the source path, creating subfolders where needed:

$sourcedir   = "C:\Users\Boss\One\Two\Alpha"
$destination = "C:\Users\Boss\One\Two\Beta"

# get a string array of files already in the destination path (FullNames only)
$existingFiles = (Get-ChildItem -Path $destination -Recurse -File).FullName

Get-ChildItem -Path $sourcedir -Recurse -File | ForEach-Object {
    $targetPath = Join-Path -Path $destination -ChildPath $_.DirectoryName.Substring($sourcedir.Length)
    $targetFile = Join-Path -Path $targetPath -ChildPath $_.Name
    # test if the file is not already present in the destination
    if ($existingFiles -notcontains $targetFile) {
        # create the destination subfolder path if this does not yet exist
        $null = New-Item -Path $targetPath -ItemType Directory -Force
        $_ | Copy-Item -Destination $targetPath
    }
}

Since your question did not show you want to only copy files that are not already present in the destination, but also newer versions of existing files, please see the edited version below:

$sourcedir   = 'C:\Unsynchronised\Alpha'
$destination = 'C:\Unsynchronised\Beta'

Get-ChildItem -Path $sourcedir -Recurse -File | ForEach-Object {
    $targetPath = Join-Path -Path $destination -ChildPath $_.DirectoryName.Substring($sourcedir.Length)
    $targetFile = Join-Path -Path $targetPath -ChildPath $_.Name
    # test if the file is not already present in the destination
    $existingFile = Get-Item -Path $targetFile -ErrorAction SilentlyContinue
    # if there is no such file or that file is older than the one we have in the source directory
    if (!$existingFile -or $_.LastWriteTime -gt $existingFile.LastWriteTime) {
        # create the destination subfolder path if this does not yet exist
        $null = New-Item -Path $targetPath -ItemType Directory -Force
        $_ | Copy-Item -Destination $targetPath -Force
    }
} 

And knowing this, why not use robocopy for it?

Something like

robocopy.exe $sourcedir $destination /S /XO /COPYALL /R:0 /W:0

where

/S       Do not copy empty directories
/XO      Excludes older files (do not overwrite existing files that are newer in the destination)
/COPYALL Copy ALL file info
/R:0     Number of retries on failures (default 1 million)
/W:0     Wait time between retries in seconds (default is 30 seconds)
  • Related