How to get a selected file to break down to path and file for robocopy transfer?

45 Views Asked by At

I am writing a script that opens a file explorer window for any user to select a file to then copy to a preselected machine into a temp folder. I want to be able to select a file anywhere on the machine and break down the path to the file and the file itself for robocopy to then copy it to \WIN10\C$\TEMP however I cannot seem to get it to break down properly.

function Copy-FileToMachines {
    Add-Type -AssemblyName System.Windows.Forms
    $fileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
        InitialDirectory = [Environment]::GetFolderPath('Desktop')
    }

    $result = $fileBrowser.ShowDialog()

    if ($result -eq 'OK') {
        $file = $fileBrowser.SafeFileName
        $source = $fileBrowser.InitialDirectory

        $deviceList = @('Computer1', 'Computer2', 'Computer3')  # Add the names of destination computers here

        foreach ($device in $deviceList) {
            try {
                robocopy "$source" "\\$device\c$\Temp" "$file" /Z /NP /R:3 /W:5 /BYTES /LOG+:`"$env:TEMP\robocopy.log`" | Out-Null
                Write-Host "File successfully copied to $device"
            }
            catch {
                Write-Host "An error occurred while copying the file to $device: $_"
            }
        }
  }

The above lets me select a file in D:\SOFTWARE but when I read the .log file it shows the source as C:\USERS\ADMINISTRATORS\DESKTOP\

I understand that to be because of InitialDirectory, however I have changed it to Split-Path

$file = Split-Path $(FileBrowser.Filename) -Leaf
$source = Split-Path $(FileBrowser.File) -Parent

Now when I run it the .log shows the destination of \\WIN10\C$\TEMP as the source now and the file is now showing as *.* in the log.

What am I missing?

2

There are 2 best solutions below

2
lit On BEST ANSWER

Use $fileBrowser.FileName in Split-Path. Remove the COLON character after $device.

function Copy-FileToMachines {
    Add-Type -AssemblyName System.Windows.Forms
    $fileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
        InitialDirectory = [Environment]::GetFolderPath('Desktop')
    }

    $result = $fileBrowser.ShowDialog()

    if ($result -eq 'OK') {
        $file = $fileBrowser.SafeFileName
        $source = Split-Path -Path $fileBrowser.FileName -Parent     # InitialDirectory

        $deviceList = @('Computer1', 'Computer2', 'Computer3')  # Add the names of destination computers here

        foreach ($device in $deviceList) {
            try {
                robocopy "$source" "\\$device\c$\Temp" "$file" /Z /NP /R:3 /W:5 /BYTES /LOG+:`"$env:TEMP\robocopy.log`" | Out-Null
                Write-Host "File successfully copied to $device"
            }
            catch {
                Write-Host "An error occurred while copying the file to $device $_"
            }
        }
    }
}
0
mklement0 On

Indeed, .InitialDirectory is the wrong property to use, because it doesn't reflect the directory path of the file ultimately selected by the user.

.FileName contains the full path of the selected file, and splitting it into directory path and file name - which your question boils down to - can be achieved in several ways:

  • PowerShell (Core) 7+ solution, using the ability of the -split operator to limit the number of tokens from the end of the input string, using a negative token count:

    # Note: [\\/] matches both a "\" and a "/", for cross-platform
    #       compatibility.
    $dirPath, $fileName = $fileBrowser.FileName -split '[\\/]', -2
    
  • Windows PowerShell solution (too), using Split-Path[1] (which is implicitly fcross-platform in PowerShell 7+):

    $dirPath  = Split-Path $fileBrowser.FileName # -Parent implied
    $fileName = Split-Path -Leaf $fileBrowser.FileName 
    
    • Alternatively, you can adapt the -split solution, although the required regex is a bit obscure:

      $dirPath, $fileName = $fileBrowser.FileName -split '[\\/](?=[^\\/]+$)'
      

E.g., with D:\SOFTWARE\file.txt returned by $fileBrowser.FileName, $dirPath ends up containing verbatim D:\SOFTWARE, and $fileName contains file.txt.


Since I've used different variable names for conceptual clarity, here's your robocopy command spelled out, with some improvements:

$null = 
  robocopy $dirPath "\\$device\c$\Temp" $fileName /Z /NP /R:3 /W:5 /BYTES /LOG+:$env:TEMP\robocopy.log

Note:

  • There is no need to enclose stand-alone variable references such as $dirPath and $fileName in "..." - just use them as-is.

  • Even enclosing \\$device\c$\Temp in "..." isn't technically necessary here, but is generally advisable in compound arguments, for conceptual clarity and to avoid edge cases.

    • Strictly speaking, the $ in c$ should be escaped (c`$) to prevent interpretation of the $ as starting a variable reference, as in $device; however, if the character following $ isn't legal as part of a variable name (except if the name as a whole is enclosed in {...}), such as / here, that isn't necessary.
  • By contrast, the partial double-quoting you've attempted, /LOG+:`"$env:TEMP\robocopy.log`", using escaped " chars., actually relies on broken behavior in Windows PowerShell and PowerShell (Core) up to v7.2.x, and would itself break in PowerShell 7.3+, unless you explicitly (temporarily) set $PSNativeCommandArgumentPassing = 'Legacy' first - see this answer for background information.

    • However, since the value of $env:TEMP can be assumed not to contain spaces, again no quoting is needed at all, as shown above, or - following the advice in the previous bullet point, by enclosing the whole argument in unescaped ": "/LOG+:$env:TEMP\robocopy.log"; when PowerShell constructs the process command line behind the scenes, it removes these " in the absence of embedded spaces in the value.
  • $null = ... is used as an alternative to ... | Out-Null for (stdout) output suppression - see this answer for a discussion of these approaches.


As an aside:

  • It would be nice if PowerShell provided a way to parse a given path in a manner that returns all its constituent parts (drive spec, directory path, file name, base file name, extension) in a single operation.

  • GitHub issue #6606 proposes just that, e.g. in the form of a future -All switch to be supported by Split-Path, which would return an object whose properties reflect the constituent parts.


[1] In this particular case, Split-Path -Leaf $fileBrowser.FileName can be replaced with $fileBrowser.SafeFileName, given that the - obscurely named - .SafeFileName property contains just the file name too.