Public/Invoke-FlattenFolders.ps1
function Invoke-FlattenFolders { <# .SYNOPSIS Moves files from all sub-directories to the parent directory and optionally delete sub-directories. .DESCRIPTION Moves files from all sub-directories to the parent directory. If files with duplicate names are found then their file name will have a guid appended to make them unique. Unless the Force parameter is used there will be a prompt for confirmation before both the renaming of any files (if required) and the moving of any files. Can be run against: > a single directory > a collection of directories piped into the module. .PARAMETER Directory Optional. The parent directory where files from all sub-directories will be moved. If neither this nor the Directories parameter are set then the current location will be used. .PARAMETER Directories Optional. A collection of parent directories where files from all sub-directories will be moved. If neither this nor the Directory parameter are set then the current location will be used. .PARAMETER Force Optional. If supplied this bypasses the confirmation prompt before both renaming and moving files. .PARAMETER DeleteSubDirectories Optional. If supplied all subdirectories will be deleted once all files have been moved. .EXAMPLE All files in all sub-directories in the current location (C:\) will be moved to the current location (C:\) with a confirmation prompt before moving: PS C:\> Invoke-FlattenFolder .EXAMPLE All files in all sub-directories in C:\Videos\ will be moved to C:\Videos\ without a confirmation prompt: PS C:\> Invoke-FlattenFolder -Directory "C:\Videos" -Force .EXAMPLE All files in all sub-directories in C:\Videos\ will be moved to C:\Videos\ without a confirmation prompt and all sub-directories will be deleted once the files have been moved: PS C:\> Invoke-FlattenFolder -Directory "C:\Videos" -Force -DeleteSubDirectories .EXAMPLE All files in all sub-directories in the piped array of directories (C:\Videos\ and C:\Music\) will be moved to their respective parents with a confirmation prompt before moving: PS C:\> "C:\Videos\","C:\Music\" | Invoke-FlattenFolder #> [CmdletBinding()] Param( [Parameter(Mandatory=$false, Position=0)] [Alias("D")] [String]$Directory, [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]]$Directories, [Parameter(Mandatory=$false)] [Alias("F")] [Switch]$Force, [Parameter(Mandatory=$false)] [Alias("DS")] [Switch]$DeleteSubDirectories ) Begin { $dirs = @() $isValid = $true } Process { # Check if a directory list was supplied if ($PSBoundParameters.ContainsKey('Directories')) { # validate directory exists if (-Not (Test-Path -Path $Directories)) { $isValid = $false Write-Error("One or more of the supplied directories does not exist.") return } $dirs += $Directories return } # Check if a file was supplied if ($PSBoundParameters.ContainsKey('Directory')) { if (Test-Path -Path $Directory) { $dirs += $Directory return } $isValid = $false Write-Error("The supplied directory does not exist.") return } #$dirs += (Get-Location).Path } End { if (-Not $isValid) { return } $subDirs = @() $files = @() foreach ($dir in $dirs) { # enumerate subdirectories Get-ChildItem $dir -Recurse -Directory | Select-Object FullName | Foreach-Object { $subdirs += $_.FullName } # enumerate files Get-ChildItem $dir -Recurse -File | Group-Object FullName | Select-Object Name | ForEach-Object { $files += $_.Name } } # check we've found some files if ($files.Length -eq 0) { Write-Host "No files found" return } # check to see if we'll have any naming conflicts $duplicates = @() foreach ($dir in $dirs) { $duplicates += (Get-DuplicateFiles -dir $dir) } # if we have naming conflicts and the Force parameter has not been passed prompt the user to confirm file renaming if (($duplicates.Length -gt 0) -and (-Not $PSBoundParameters.ContainsKey('Force'))) { # the total number of duplicates is the number reported plus the count of unique instances within those duplicates # one instance from each group is not returned from the Compare-Object call $cDuplicates = ($duplicates | Select-Object -Unique).Length + $duplicates.Length $header = "$cDuplicates files with the same name were found. These files will have a guid appended to the file name to make them unique." $question = "Are you happy to continue?" if (-Not (Get-YesNoAsBool($header, $question))) { return } } # if the Force parameter has not been passed calculate affected files and prompt the user to confirm the file move if (-Not $PSBoundParameters.ContainsKey('Force')) { # ask for confirmation $cDirs = $dirs.Length $cSubDirs = $subDirs.Length $cFiles = $files.Length $nDir = "directory" if ($cDirs -gt 1) { $ndir = "directories" } $nSub = "" if ($PSBoundParameters.ContainsKey('DeleteSubDirectories')) { $nSub = "and delete all sub-directories" } $header = "You are about to move $cFiles files from $cSubDirs sub-directories into $cDirs parent $nDir $nSub" $question = "Are you sure you want to continue?" if (-Not (Get-YesNoAsBool($header, $question))) { return } } $i = 0 foreach ($dir in $dirs) { # enumerate files Write-Progress -Activity "Moving file $($i + 1) of $($files.Length)" -PercentComplete ((($i + 1) / $files.Length) * 100) # get just the duplicates in this directory $duplicates = (Get-DuplicateFiles -dir $dir) Get-ChildItem $dir -Recurse -File | Group-Object FullName | Select-Object Name | ForEach-Object { Write-Host "" if ($duplicates -contains [io.path]::GetFileName($_.Name)) { $newName = (([io.path]::GetFileNameWithoutExtension($_.Name)) + "_" + [GUID]::NewGuid().ToString('D') + [io.path]::GetExtension($_.Name)) Rename-Item -Path $_.Name -NewName $newName Move-Item -Path (Join-Path -Path (Split-Path $_.Name) -ChildPath $newName) -Destination $dir } else { Move-Item -Path $_.Name -Destination $dir } } # delete sub-directories if requested if ($PSBoundParameters.ContainsKey('DeleteSubDirectories')) { Get-ChildItem $dir -Directory | ForEach-Object { Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } } } } } |