Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Move a List of Folders to a New Drive with PowerShell and Robocopy

Hand a list of folders from several drives to robocopy in one go, keep their structure on the new drive, catch name collisions before anything moves, and get a log per folder.

AT A GLANCEMove-FolderToDrive.ps1
What it does
Plans a destination for every source folder, refuses to run if two would land in the same place, then moves (or copies) each one with robocopy and returns a result per folder with a plain-English meaning of robocopy's exit code.
Requires
  • PowerShell 7+ or Windows PowerShell 5.1
  • robocopy (built into Windows)
Permissions
Read and delete rights on the source folders and write rights on the destination. Run elevated if you're moving system-owned or other users' data.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run with a mocked robocopy in PowerShell 7.4 (planning, collision check, drive-root check, exit-code mapping and -WhatIf)

Part 3 of the thread File wrangling

New drive day. You've got a list of folders scattered across C: and D:, and they all need to end up on E: in the same shape they're in now. Dragging them over in Explorer works until it hits a path that's too long, a file that's locked, or a folder with 400,000 tiny files and a progress bar that says "about 3 days remaining."

The original script on this page did the move file by file with Move-Item. It also had a sneaky bug: it dropped the drive letter, so C:\Media and D:\Media would both pour into E:\Media and quietly merge. And it had a couple of broken .NET calls, so the version here didn't even run.

This version hands the heavy lifting to robocopy, which was built for exactly this: retries, long paths, multithreading, and a proper log. The PowerShell part does what robocopy doesn't. It plans every destination up front, stops if two sources would collide, and gives you back one tidy result per folder.

Move-FolderToDrive.ps1Download
<#
.SYNOPSIS
    Moves (or copies) a list of folders to another drive with robocopy, keeping their structure.
.DESCRIPTION
    For each source folder, works out a matching path under -DestinationRoot and hands the job
    to robocopy, which is far better than Move-Item at big trees, long paths, retries and
    logging. Each source gets its own log file and its own result object, and one failure
    doesn't stop the rest. Supports -WhatIf.
.PARAMETER Source
    One or more folders to move, e.g. C:\Media, D:\Projects.
.PARAMETER DestinationRoot
    Where they go, e.g. E:\ or E:\Migrated.
.PARAMETER IncludeDriveLetter
    Put each source under a folder named for its drive (E:\C\Media, E:\D\Media). Use this when two
    sources share a name on different drives; the script refuses to merge them otherwise.
.PARAMETER Copy
    Copy instead of move. Sources are left alone.
.PARAMETER LogFolder
    Where robocopy logs go. Default: the current folder.
.PARAMETER Threads
    robocopy /MT value. Default 8. Use 1 for USB drives and old spinning disks.
.EXAMPLE
    .\Move-FolderToDrive.ps1 -Source C:\Media, D:\Projects -DestinationRoot E:\ -WhatIf
.EXAMPLE
    .\Move-FolderToDrive.ps1 -Source C:\Media, D:\Media -DestinationRoot E:\Migrated -IncludeDriveLetter -Copy
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)][ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })][string[]]$Source,
    [Parameter(Mandatory)][string]$DestinationRoot,
    [switch]$IncludeDriveLetter,
    [switch]$Copy,
    [string]$LogFolder = (Get-Location).Path,
    [ValidateRange(1, 128)][int]$Threads = 8
)

$exitMeaning = @{
    0 = 'Nothing to do'; 1 = 'Files copied'; 2 = 'Extra files at destination'; 3 = 'Files copied, extras at destination'
    4 = 'Mismatches found'; 5 = 'Files copied, mismatches found'; 6 = 'Extras and mismatches'; 7 = 'Files copied, extras and mismatches'
}

# Work out every destination first, so collisions are caught before anything moves.
$plan = foreach ($src in $Source) {
    $full = (Resolve-Path -LiteralPath $src).ProviderPath.TrimEnd('\')
    if ($full -notmatch '^([A-Za-z]):\\(.+)$') { throw "$full isn't a folder on a local drive. Point the script at folders like D:\Media, not drive roots or UNC paths." }
    $child = if ($IncludeDriveLetter) { '{0}\{1}' -f $Matches[1].ToUpper(), $Matches[2] } else { $Matches[2] }
    # Plain string join: Join-Path insists the destination drive already exists, which breaks -WhatIf planning.
    [pscustomobject]@{ Source = $full; Destination = '{0}\{1}' -f $DestinationRoot.TrimEnd('\'), $child }
}

$dupes = $plan | Group-Object Destination | Where-Object Count -gt 1
if ($dupes) {
    throw "These sources would land in the same place: $(($dupes.Group.Source) -join ', '). Use -IncludeDriveLetter."
}

$verb = if ($Copy) { 'Copy' } else { 'Move' }
foreach ($item in $plan) {
    if (-not $PSCmdlet.ShouldProcess($item.Source, "$verb to $($item.Destination)")) { continue }

    $log  = Join-Path $LogFolder ('robocopy_{0}_{1:yyyyMMdd-HHmmss}.log' -f ($item.Source -replace '[:\\ ]+', '_').Trim('_'), (Get-Date))
    $rcArgs = @($item.Source, $item.Destination, '/E', '/COPY:DAT', '/DCOPY:DAT', '/XJ', '/R:2', '/W:5', "/MT:$Threads", '/NP', "/LOG:$log")
    if (-not $Copy) { $rcArgs += '/MOVE' }

    Write-Verbose "robocopy $($rcArgs -join ' ')"
    try {
        robocopy @rcArgs | Out-Null
        $code = $LASTEXITCODE
        [pscustomobject]@{
            Source      = $item.Source
            Destination = $item.Destination
            ExitCode    = $code
            Result      = if ($code -ge 8) { 'FAILED - see log' } else { $exitMeaning[$code] }
            Log         = $log
        }
        if ($code -ge 8) { Write-Warning "$($item.Source): robocopy exit code $code. Check $log" }
    }
    catch {
        Write-Warning "$($item.Source): $($_.Exception.Message)"
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Sourcestring[]—The folders to move, like C:\Media, D:\Projects. Must be folders on a local drive, not the root of a drive.
-DestinationRootstring—Where they go, like E:\ or E:\Migrated. Each source keeps its path under here.
-IncludeDriveLetterswitch—Put each source under a folder named for its old drive (E:\Migrated\C\Media). Needed when two sources share a name.
-Copyswitch—Copy instead of move. Sources are left alone.
-LogFolderstringcurrent folderWhere the robocopy logs go, one per source.
-Threadsint8robocopy's /MT value. Drop to 1 for USB drives and old spinning disks.

Run it

Preview the plan. Nothing is copied or moved.

.\Move-FolderToDrive.ps1 -Source C:\Media, D:\Projects, D:\Games -DestinationRoot E:\ -WhatIf

Copy first, check it, then run again without -Copy to do the real move.

.\Move-FolderToDrive.ps1 -Source C:\Media, D:\Projects -DestinationRoot E:\ -Copy -LogFolder C:\Temp\MigrationLogs

Two folders with the same name on different drives.

.\Move-FolderToDrive.ps1 -Source C:\Media, D:\Media -DestinationRoot E:\Migrated -IncludeDriveLetter

Feed it a list from a text file and keep only the failures.

.\Move-FolderToDrive.ps1 -Source (Get-Content .\folders.txt) -DestinationRoot E:\ | Where-Object ExitCode -ge 8

What you'll see

Example outputvalues are illustrative
WARNING: D:\Games: robocopy exit code 16. Check C:\Temp\MigrationLogs\robocopy_D_Games_20260929-091502.log

Source      Destination  ExitCode Result                     Log
------      -----------  -------- ------                     ---
C:\Media    E:\Media            1 Files copied               C:\Temp\MigrationLogs\robocopy_C_Media_20260929-091502.log
D:\Projects E:\Projects         3 Files copied, extras at... C:\Temp\MigrationLogs\robocopy_D_Projects_20260929-091744.log
D:\Games    E:\Games           16 FAILED - see log           C:\Temp\MigrationLogs\robocopy_D_Games_20260929-091502.log

How it works

  1. Plan everything first. Each source path is resolved and split into drive letter and the rest (D: + Projects). The destination is DestinationRoot\Projects, or DestinationRoot\D\Projects with -IncludeDriveLetter. Drive roots and UNC paths are rejected.
  2. Check for collisions. If two sources would end up at the same destination, the script stops before it touches anything and tells you which ones.
  3. Hand each folder to robocopy. The arguments are /E /COPY:DAT /DCOPY:DAT /XJ /R:2 /W:5 /MT:<threads> /NP /LOG:<file>, plus /MOVE unless you asked for -Copy. Two retries, five seconds apart, beats robocopy's default of a million retries 30 seconds apart.
  4. Translate the exit code. Each folder comes back as an object with the code, what it means, and where the log is. Anything 8 or above gets a warning.
  5. Respect -WhatIf. Each folder goes through ShouldProcess, so -WhatIf prints the whole plan, destinations and all, without starting robocopy once.

Take it further

  • Do a real dry run. Temporarily add /L to the robocopy arguments and it'll list what it would copy, with counts, without copying anything. Great for estimating how long the real thing will take.
  • Copy, verify, then delete. Run with -Copy, compare the two sides with a hash check, then run again without it. Slower, but you never have a moment where the data exists in only one half-moved place.
  • Update shortcuts and mapped drives afterwards. A quick search for .lnk files pointing at the old paths saves a week of "where did my stuff go?" messages.

Things that'll trip you up

  • Robocopy exit codes below 8 are good news. They're bit flags, not error numbers. 1 means files were copied, 2 means the destination had extra files, 4 means some files didn't match. 8 and above means something actually failed, and that's what the script warns on.
  • Locked files stay behind. /MOVE deletes each source file only after it's copied. Anything robocopy couldn't copy, like a file an app has open, is still in the source when it's done. Check the log and the source before you celebrate.
  • Junctions are skipped on purpose. /XJ stops robocopy following junction points, which avoids infinite loops in things like old profile folders. The junctions themselves don't get recreated at the destination.
  • Make sure it'll fit. robocopy doesn't check free space up front. It'll happily fill the new drive and then fail. Add up the source sizes first.
  • Moving folders apps depend on breaks the apps. Game libraries, VM folders, and anything a program has a hard-coded path to will need that program pointed at the new location. Move data folders freely; move application folders with a plan.