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.
- 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)
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.
<#
.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
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Source | string[] | — | The folders to move, like C:\Media, D:\Projects. Must be folders on a local drive, not the root of a drive. |
-DestinationRoot | string | — | Where they go, like E:\ or E:\Migrated. Each source keeps its path under here. |
-IncludeDriveLetter | switch | — | Put each source under a folder named for its old drive (E:\Migrated\C\Media). Needed when two sources share a name. |
-Copy | switch | — | Copy instead of move. Sources are left alone. |
-LogFolder | string | current folder | Where the robocopy logs go, one per source. |
-Threads | int | 8 | robocopy'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:\ -WhatIfCopy 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\MigrationLogsTwo folders with the same name on different drives.
.\Move-FolderToDrive.ps1 -Source C:\Media, D:\Media -DestinationRoot E:\Migrated -IncludeDriveLetterFeed 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 8What you'll see
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
- Plan everything first. Each source path is resolved and split into drive letter and the rest (
D:+Projects). The destination isDestinationRoot\Projects, orDestinationRoot\D\Projectswith-IncludeDriveLetter. Drive roots and UNC paths are rejected. - 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.
- Hand each folder to robocopy. The arguments are
/E /COPY:DAT /DCOPY:DAT /XJ /R:2 /W:5 /MT:<threads> /NP /LOG:<file>, plus/MOVEunless you asked for-Copy. Two retries, five seconds apart, beats robocopy's default of a million retries 30 seconds apart. - 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.
- Respect -WhatIf. Each folder goes through
ShouldProcess, so-WhatIfprints the whole plan, destinations and all, without starting robocopy once.
Take it further
- Do a real dry run. Temporarily add
/Lto 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
.lnkfiles 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.