The fix is to target the "Packaging" folders directly and move only their immediate children up one level — never enumerate the contents of the subfolders inside them. If you loop over every child directory and move its contents, you end up pushing files two levels up, which flattens your structure and causes "file already exists" errors when sibling folders contain identically named files.
Why the naive script flattens everything
A common first attempt looks like this:
$Root = "I:\root"
Get-ChildItem -LiteralPath $Root -Directory | ForEach-Object {
$ParentFolder = $_
Get-ChildItem -LiteralPath $ParentFolder.FullName -Directory | ForEach-Object {
$SubSubFolder = $_
Get-ChildItem -LiteralPath $SubSubFolder.FullName | Move-Item -Destination $ParentFolder.FullName
Remove-Item -LiteralPath $SubSubFolder.FullName
}
}
This fails for two reasons:
- Wrong level of enumeration. The inner loop grabs every directory under
Folder A— includingFolder A1andFolder A2— and moves their contents (the PNG files) straight intoFolder A. That is exactly the flattening you want to avoid. Only the children of the Packaging folder should move up one level; the subfolders themselves must move as whole units. - Name collisions. Because
Folder A1andFolder A2both contain files named1.pngand2.png, moving those files into the same destination producesMove-Item : Cannot create a file when that file already exists.This error is a symptom of the flattening, not a separate problem.
The correct approach
Instead of walking the tree level by level, filter for the folders whose name matches your packaging pattern (here, Packaging*) and move each of their immediate children up into the Packaging folder's parent. Because you move entire directories like Folder A1 as single items, their internal contents — including duplicate file names across siblings — travel untouched.
$Root = 'I:\root'
foreach ($parent in @(Get-ChildItem -LiteralPath $Root -Directory)) {
foreach ($pkg in @(Get-ChildItem -LiteralPath $parent.FullName -Directory -Filter 'Packaging*')) {
foreach ($item in @(Get-ChildItem -LiteralPath $pkg.FullName)) {
$dest = Join-Path $parent.FullName $item.Name
if (Test-Path -LiteralPath $dest) {
Write-Warning "Skipped, already exists: $dest"
} else {
Move-Item -LiteralPath $item.FullName -Destination $dest
}
}
# Optional: delete the Packaging folder once it's empty
# if (-not (Get-ChildItem -LiteralPath $pkg.FullName -Force)) { Remove-Item -LiteralPath $pkg.FullName }
}
}
Key details in this script:
- Explicit destination with a collision check. Rather than piping into
Move-Itemand letting it fail, the script builds the destination path withJoin-Pathand tests it first. If something already exists there, it warns and skips instead of aborting or overwriting. - Wrapping the
Get-ChildItemresults in@(...)snapshots each listing before you start moving things. This prevents the pipeline from re-reading a directory that is being modified mid-loop. -LiteralPatheverywhere. Folder names containing brackets, square brackets, or other wildcard characters are handled safely.- Optional cleanup. The commented-out
Remove-Itemblock deletes the empty Packaging folder only after verifying it is truly empty, using-Forceto catch hidden files.
A shorter alternative using wildcards
If your folder names are consistent, Get-ChildItem accepts wildcards in the path itself, so you can locate all Packaging folders in one call:
$root = 'I:\Root'
Get-ChildItem "$($root)\*\Packaging*" -Directory | ForEach-Object {
Get-ChildItem -LiteralPath $_.FullName | Move-Item -Destination $_.Parent.FullName -Verbose
# Remove-Item -LiteralPath $_.FullName -Verbose # optional cleanup
}
Here $_.Parent.FullName is the folder that contains the Packaging folder — exactly where the contents belong. The -Verbose switch prints every move so you can audit what happened. The trade-off versus the longer script: this version is more concise but has no per-item collision check, so it will stop with an error if a destination name already exists. Use the longer script if your data might contain conflicts.
Test before you commit
Batch moves are hard to undo, so protect yourself:
- Dry run first. Replace the
Move-Itemline withWrite-Host "Would move $($item.FullName) -> $dest"and review the output. - Back up the tree. A quick
Copy-Item -Recurseof the root to a temp location gives you a rollback path. - Verify with
tree. Runtree /Ffrom a command prompt before and after to confirm the structure changed the way you intended.
Frequently Asked Questions
Why did my script move the PNG files up instead of the folders?
Because you enumerated the directories inside the parent (which includes Folder A1, Folder A2, etc.) and moved their contents. You should enumerate the contents of the Packaging folder instead — those contents are the subfolders and zip files, and moving them as whole items preserves their internal structure.
How do I match a different folder name pattern?
Change the filter. In the long script, adjust -Filter 'Packaging*'; in the short one, adjust the wildcard path, e.g. "$root\*\*Pack*". The -Filter parameter matches against the folder name only.
What if a file with the same name already exists in the destination?
The longer script detects this with Test-Path and skips the item with a warning, leaving the original in place. If you want automatic renaming instead of skipping, you would need to generate a unique name (for example, appending a counter) before calling Move-Item.
Is it safe to delete the Packaging folder afterwards?
Only after confirming it is empty. Check with Get-ChildItem -LiteralPath $pkg.FullName -Force — the -Force flag reveals hidden and system files that a plain listing would miss. If the listing is empty, Remove-Item is safe.
Why use -LiteralPath instead of the default -Path?
The default -Path interprets [ and ] as wildcard characters, so folder names like Folder [2024] can silently match nothing or the wrong item. -LiteralPath treats the name exactly as written.
Does this work on Windows PowerShell 5.1 as well as PowerShell 7?
Yes. All the cmdlets used — Get-ChildItem, Move-Item, Join-Path, Test-Path, Remove-Item — behave the same in both versions for this scenario.