Powershell: Find folders with a specific name, and move contents up one level, rename if exists - powershell

I've got the first two-thirds of this one accomplished, but I'm stuck on the last part. I've got a script that searches for subfolders with a specific name, and moves their contents up one level. I have another script that moves files from one place to another, and renames them if the file already exists. What I'm trying to do now is merge the two. So here's the one that moves files up:
$sourceDir="E:\Deep Storage"
$searchFolder="Draft Materials"
Get-ChildItem -path $sourceDir -filter $searchFolder -Recurse |
ForEach-Object {
Get-ChildItem -File -Path $_.FullName |
ForEach-Object {
Move-Item -Path $_.FullName -Destination $(Split-Path -Parent $_.PSParentPath)
}
}
And here's the one that moves things while renaming if they already exist:
$sourceDir="E:\Test1"
$targetDir="E:\Deep Storage\Test1"
Get-ChildItem -Path $sourceDir -Filter *.* -Recurse | ForEach-Object {
$num=1
$nextName = Join-Path -Path $targetDir -ChildPath $_.name
while(Test-Path -Path $nextName)
{
$nextName = Join-Path $targetDir ($_.BaseName + "_$num" + $_.Extension)
$num+=1
}
$_ | Copy-Item -Destination $nextName -Verbose
}
And lastly, my attempt to hybridize the two:
$sourceDir="E:\Deep Storage"
$searchFolder="Draft Materials"
Get-ChildItem -path $sourceDir -filter $searchFolder -Recurse |
ForEach-Object {
Get-ChildItem -File -Path $_.FullName |
ForEach-Object {
$num=1
$nextName = Join-Path -Path $_.FullName -Destination $(Split-Path -Parent $_.PSParentPath)
while(Test-Path -Path $nextName)
{
$nextName = Join-Path -Path $_.FullName -Destination $(Split-Path -Parent $_.PSParentPath) ($_.BaseName + "_$num" + $_.Extension)
$num+=1
}
$_ | Copy-Item -Destination $nextName
}
}
I feel like I'm on the right track, but after two hours of attempts I haven't been able to get this to work.
EDIT: providing the exact syntax I'm giving it
$sourceDir="E:\Deep Storage\Projects"
$searchFolder="Draft Materials"
$destinationPath = "$($sourceDir)\.."
Write-Host "OPERATION: Search for Folders Named" -ForegroundColor White -BackgroundColor DarkGreen -NoNewLine;
Write-Host " '$searchFolder' " -ForegroundColor Yellow -BackgroundColor DarkGreen -NoNewLine;
Write-Host "and Move Contents Up One Level" -ForegroundColor White -BackgroundColor DarkGreen;
Write-Host "SEARCHDIR: $sourceDir" -ForegroundColor White -BackgroundColor DarkGreen;
# Get all directories in specific folders inside the source directory
$folders = Get-ChildItem -Path $sourceDir -Directory | Where-Object {$_.Name -like "$searchFolder*" -or $_.FullName -like "*\$searchFolder\*"}
foreach ($folder in $folders) {
# Get all files in the current folder
$files = Get-ChildItem -Path $folder.FullName
foreach ($file in $files) {
$destinationFile = Join-Path -Path $destinationPath -ChildPath $file.Name
if (Test-Path $destinationFile) {
# If a file with the same name exists in the destination directory, rename it
$name = $file.Name
$extension = $file.Extension
$i = 0
while (Test-Path $destinationFile) {
$i++
$name = "{0}_{1}{2}" -f ($file.BaseName, $i, $extension)
$destinationFile = Join-Path -Path $destinationPath -ChildPath $name
}
Write-Host "Renaming $($file.Name) to $name"
}
Move-Item $file.FullName $destinationFile -Verbose -WhatIf
}
}

Here's what I came up with reading OP's post, code, and comments:
$sourcePath = 'E:\Deep Storage\Projects'
$searchFolder = 'Draft Materials'
Write-Host "OPERATION: Search for Folders Named" -ForegroundColor White -BackgroundColor DarkGreen -NoNewLine;
Write-Host " '$searchFolder' " -ForegroundColor Yellow -BackgroundColor DarkGreen -NoNewLine;
Write-Host "and Move Contents Up One Level" -ForegroundColor White -BackgroundColor DarkGreen;
Write-Host "SEARCHDIR: $sourcePath" -ForegroundColor White -BackgroundColor DarkGreen;
# Get all directories in specific folders inside the source directory
$folders = (Get-ChildItem -LiteralPath $sourcePath -Directory -Recurse) | Where-Object Name -match $searchFolder
### Check selected folders
$Folders.FullName | Out-GridView -Title 'Selected Folders'
Read-Host 'Paused. Check selected folders in GridView. Press <enter> to continue '
###
ForEach ($folder in $folders)
{
# Get all files in the current folder
$filesToCopy = $folder | Get-ChildItem -File
# Get list of names for exising files in target (parent folder)
$targetPath = $folder.Parent.FullName
$filesInTarget = (Get-ChildItem -LiteralPath $targetPath -File).Name
ForEach ($file in $filesToCopy)
{
If ($file.Name -notIn $filesInTarget)
{
$file | Move-Item -Destination $targetPath -Verbose -WhatIf
}
Else
{
$i = 0
Do
{
$newName = '{0}_{1}{2}' -f ($file.BaseName, $i++, $file.Extension)
} Until ( $newName -notIn $FilesInTarget )
Write-Host ('Renaming "{0}" to "{1}"...' -f $file.Name , $newName)
$file | Move-Item -Destination (Join-Path $targetPath $newName) -Verbose -WhatIf
}
}
# Delete (hopefully empty) folder
If (!($folder | Get-ChildItem -Force))
{
$folder | Remove-Item -WhatIf
}
Else
{
Write-Host ('Items still exist in "{0}". Folder not deleted.' -f $folder.FullName)
}
}
Syntax choice: For any cmdlet that has Path/LiteralPath parameter sets (gci, Copy, Move, Rename, etc.), the System.IO.FileSystemInfo | <Cmdlet> syntax succeeds with items that would fail in the <Cmdlet> -Path (System.IO.FileSystemInfo).FullNaame form becasue special characters in their name would require the -LiteralPath parameter.
In many cases replacing -Path with -LiteralPath (or its alias: -lp) will work as well. But the pipelined format reads "cleaner" (IMHO) when scanning code and, if you're just learning PowerShell, reminds you to think in terms of pipelining whenever possible and avoiding intermediate variables. Just for grins, here's a version of the above code where items are piped as much as possible, using ForEach-Object:
(Get-ChildItem -LiteralPath $sourcePath -Directory -Recurse) |
where Name -match $searchFolder |
ForEach-Object {
# Get list of names for exising files in target (parent folder)
$targetPath = $_.Parent.FullName
$filesInTarget = (Get-ChildItem -LiteralPath $targetPath -File).Name
$_ | Get-ChildItem -File | ForEach-Object {
If ($_.Name -notIn $filesInTarget)
{
$_ | Move-Item -Destination $targetPath -Verbose -WhatIf
}
Else
{
$i = 0
Do
{
$newName = '{0}_{1}{2}' -f ($_.BaseName, $i++, $_.Extension)
} Until ( $newName -notIn $FilesInTarget )
Write-Host ('Renaming "{0}" to "{1}"...' -f $_.Name , $newName)
$_ | Move-Item -Destination (Join-Path $targetPath $newName) -Verbose -WhatIf
}
}
# Delete (hopefully empty) folder
If (!($_ | Get-ChildItem -Force))
{
$_ | Remove-Item -WhatIf
}
Else
{
Write-Host ('Items still exist in "{0}". Folder not deleted.' -f $_.FullName)
}
}

please try this:
$sourcePath = "C:\temp\test\Test"
$destinationPath = "$($sourcePath)\.."
# Get all files in the source directory
$files = Get-ChildItem -Path $sourcePath
foreach ($file in $files) {
$destinationFile = Join-Path -Path $destinationPath -ChildPath $file.Name
if (Test-Path $destinationFile) {
# If a file with the same name exists in the destination directory, rename it
$name = $file.Name
$extension = $file.Extension
$i = 0
while (Test-Path $destinationFile) {
$i++
$name = "{0}_{1}{2}" -f ($file.BaseName, $i, $extension)
$destinationFile = Join-Path -Path $destinationPath -ChildPath $name
}
Write-Host "Renaming $($file.Name) to $name"
}
Move-Item $file.FullName $destinationFile
}
This will not delete the original location and will move everything from the sourcePath to the parent location.
To delete the original location just add at the end:
Remove-Item -Path $sourcePath -Force -Confirm:$false
UPDATE1:
$sourcePath = "C:\temp\test\Test"
$destinationPath = "$($sourcePath)\.."
# Get all directories in specific folders inside the source directory
$folders = Get-ChildItem -Path $sourcePath -Directory | Where-Object {$_.Name -like "Folder1*" -or $_.FullName -like "*\Folder2\*"}
foreach ($folder in $folders) {
# Get all files in the current folder
$files = Get-ChildItem -Path $folder.FullName
foreach ($file in $files) {
$destinationFile = "$($folder.FullName)\.."
if (Test-Path $destinationFile) {
# If a file with the same name exists in the destination directory, rename it
$name = $file.Name
$extension = $file.Extension
$i = 0
while (Test-Path $destinationFile) {
$i++
$name = "{0}_{1}{2}" -f ($file.BaseName, $i, $extension)
$destinationFile = Join-Path -Path $destinationPath -ChildPath $name
}
Write-Host "Renaming $($file.Name) to $name"
}
Move-Item $file.FullName $destinationFile
}
}

Related

How to log copied items during the backup script?

I need to make basic / or more advanced backup script that would copy items from folder A to folder B and then log what it did.
This copies the files just fine:
$source = 'path\gamybinis\*'
$dest = 'path\backup'
Get-ChildItem -Path $source -Recurse | Where-Object { $_.LastWriteTime -gt [datetime]::Now.AddMinutes(-5)
}| Copy-Item -Destination $dest -Recurse -Force
Write-Host "Backup started"
Pause
But after this I can't write the log with | Out-File, So I've tried this:
$source = 'path\gamybinis\*'
$dest = 'path\backup'
$logFile = 'path\log.txt'
$items = Get-ChildItem -Path $source -Recurse | Where-Object { $_.LastWriteTime -gt [datetime]::Now.AddMinutes(-5)
}
foreach($item in $items){
Out-File -FilePath $logFile -Append
Copy-Item -Path "$source\$item" -Destination $dest -Recurse -Force
}
Write-Host "Backup started"
Pause
This one does absolutely nothing, what exactly am I doing wrong?
(Advanced script part would be: backing up recently modified files then files should be archived to .rar/.zip, log file have to have structure that is easily readable and log file should have information which user was working on the device during the backup) - For those who are wondering.
If you can't use robocopy, in pure PowerShell code you could do this
$source = 'path\gamybinis' # no need for '\*' because you're specifying -Recurse
$dest = 'path\backup'
$logFile = 'path\log.txt'
# test if the destination path exists. If not, create it first
if (!(Test-Path -Path $dest -PathType Container)) {
$null = New-Item -Path $dest -ItemType Directory
}
Write-Host "Backup started"
Get-ChildItem -Path $source -Recurse |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-5) } |
ForEach-Object {
$_ | Copy-Item -Destination $dest -Recurse -Force
Add-Content -Path $logFile -Value "$((Get-Date).ToString("yyyy-MM-dd HH:mm:ss")) - Copied file '$($_.FullName)'"
}
Write-Host "Backup done"
From your comments, I understand you have problems when using the -Container switch.
Below code does not use that and creates the folder structure of the copied files in the backup folder, strictly using Powershell code:
$source = 'path\gamybinis' # no need for '\*' because you're specifying -Recurse
$dest = 'path\backup'
$logFile = 'path\log.txt'
Write-Host "Backup started"
Get-ChildItem -Path $source -File -Recurse |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-5) } |
ForEach-Object {
$target = Join-Path -Path $dest -ChildPath $_.DirectoryName.Substring($source.Length)
if (!(Test-Path $target -PathType Container)) {
# create the folder if it does not already exist
$null = New-Item -Path $target -ItemType Directory
}
$_ | Copy-Item -Destination $target -Force
Add-Content -Path $logFile -Value "$((Get-Date).ToString("yyyy-MM-dd HH:mm:ss")) - Copied file '$($_.FullName)'"
}
Write-Host "Backup done"

Error While Copying files using Powershell

I simply want to copy some files from one Directory to another. The Problem is, that the files were indeed copied, but for every file I get an error saying, that I am not authorized to Access Destination Directory.The Destination Directory shown in the error is not a Directory but a filename, actually the Name of the file actually being copied
For Example:
"C:\Users\Administrator\Desktop\temp\FileExport\Sample\archive\Arc9wa\samplesys\data\UPR00000\0000003E.bmp"
How can I Prevent this Happening? What am I doing wrong?
My Copy-File Command:
Copy-Item "$File" -Destination "$Destination" -Recurse -force
Complete Code Looks like:
[string]$samplepath = Resolve-Path /*\config\serversetup2
$first_letter= $samplepath.SubString(0,1)
$dpath=$first_letter+":"
$path = Get-Location
Copy-Item '/*\config\serversetup2' -Destination $dpath'\temp\serversetup2\' -recurse -force
$samplepath -match'(?<content>.*)config'
$respath= $matches['content']
$fileexport= "\FileExport"
[String[]]$Destination = Join-Path -Path $path -ChildPath $fileexport
[String[]]$Source = $respath
[String[]]$FilePattern = "*"
$Result = #()
Write-Host "Copying '$($Source -join ", ")' to '$($Destination -join ", ")'..." -ForegroundColor Green
$FileCount = (Get-ChildItem $Source -Recurse -File -Include $FilePattern -ErrorAction SilentlyContinue).Count
$i = 0
$NotFound = $true
$Duration = Measure-Command {
foreach ($File in (Get-ChildItem -Path $Source -Recurse -File -Include $FilePattern -ErrorAction SilentlyContinue)) {
global:$i++
Write-Progress -Activity "Copying '$($Source -join ", ")' to '$($Destination.FullName)'" -Status "Copied $i out of $FileCount..." -PercentComplete ($i/$FileCount*100)
Write-Verbose "Copying '$($Source -join ", ")' to '$($Destination.FullName)'"
foreach ($File in $Source) {
Copy-Item "$File" -Destination "$Destination" -Recurse -force
}
}
}
Write-Host "Finished Export"
Instead of using Copy-Item I am using Robocopy. There were also many Bugs in my Code. Here is my new Code working fine:
[string]$samplepath = Resolve-Path /*\temp
$samplepath -match'(?<content>.*)temp'
$respath= $matches['content']
$first_letter= $samplepath.SubString(0,1)
$dpath=$first_letter+":"
$path = Get-Location
$exppath = Join-Path -Path "$path\" -ChildPath "\FileExport"
New-Item -Path "$dpath\Sample" -ItemType Directory -force
$newpath = Join-Path -Path "$dpath" -ChildPath "Sample"
Write-Host "Starting import"
robocopy "$exppath" "$newpath" /e
Write-Host "Process finished"

Powershell Move Files To Directory - Maintain Paths?

Trying to make a simple backup script that will do as below:
Move the following
\source\example1.txt
\source\path\example2.txt
To
\dest\example1.txt
\dest\path\example2.txt
At the same time, renaming any files that already exist in dest.
My Code:
$src = "C:\Users\User\Desktop\test1"
$dest = "C:\Users\User\Desktop\test2"
Get-ChildItem -Path $src -Filter *.txt -Recurse | ForEach-Object {
$num=1
$nextName = Join-Path -Path $dest -ChildPath $_.name
while(Test-Path -Path $nextName)
{
$nextName = Join-Path $dest ($_.BaseName + " ($num)" + $_.Extension)
$num+=1
}
$_ | Move-Item -Destination $nextName
}
This almost works but it flattens all files into one folder in the dest.
(\source\path\example.txt becomes \dest\example.txt)
How to fix?
Following commented code snippet could do the job:
Get-ChildItem -Path $src -Filter *.txt -Recurse |
ForEach-Object {
$num=1
# ChildPath ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
$nextName = Join-Path -Path $dest -ChildPath $_.FullName.Replace("$src\", '')
# effective destination for current file
$destNew = $nextName | Split-Path
# create effective destination silently if necessary
if ( -not (Test-Path -Path $destNew) ) {
$null = New-Item $destNew -ItemType Directory
}
while(Test-Path -Path $nextName)
{
# ↓↓↓↓↓↓↓↓
$nextName = Join-Path $destNew ($_.BaseName + " ($num)" + $_.Extension)
$num+=1
}
$_ | Move-Item -Destination $nextName
}
Other method :
function Get-ItemNameWithNumberIfExist($NewPath, $BaseName, $Extension, $rang)
{
#build a new file name
if ($rang -eq 0)
{
$NewFileName="{0}{1}" -f $NewPath, $BaseName, $Extension
}
else
{
$NewFileName="{0}({1}){2}" -f $NewPath, $BaseName, $rang, $Extension
}
#build a new path file name (use combine for work on every SE)
$NewPathFile=[System.IO.Path]::Combine($NewPath, $NewFileName)
#recursive call if file exist
if (Test-Path -Path $NewPathFile)
{
$rang++
Get-ItemNameWithNumberIfExist $NewPath $BaseName $Extension $rang
}
else
{
$NewPathFile
}
}
$OldPath='C:\temp\tmp1\'
$NewPath='C:\temp\tmp2\'
Get-ChildItem $OldPath -file -Recurse | %{
$NewPath=$_.DirectoryName.Replace($OldPath, $NewPath)
#create directory without error if exist
New-Item -ItemType Directory -Path $NewPath -Force
#move item and rename if exist
move-item $_.FullName (Get-ItemNameWithNumberIfExist $NewPath $_.BaseName $_.Extension 0)
}

How do I show progress of copying files older than 1 day from one folder to other another?

We need to know in code what files were copied and some files were old and were not copied.
$date = (get-date).AddDays(-1)
get-childitem -File c:\t\*.*,c:\f\*.*,c:\u\*.*,c:\s\*.* | where-object {$_.LastWriteTime -gt $date} |
Copy-Item -Destination c:\t\1 ```
If you're on PowerShell 4.0 or newer, you could use the .Where({}) extension method in "Split" mode to split new and old files into two groups:
$new,$old = #(Get-ChildItem -File C:\t\*.*).Where({$_.LastWriteTime -gt $date}, 'Split')
# Write file names to log files
$new.Name > newfiles.txt
$old.Name > oldfiles.txt
$new | Copy-Item -Destination C:\t\1\
If by 'showing progress' you mean writing some info to the console, then this could be what you want.
$date = (Get-Date).AddDays(-1)
$dest = 'C:\t\1'
# if the destination folder does not exist, create it first
if (!(Test-Path $dest -PathType Container)) {
New-Item -Path $dest -ItemType Directory | Out-Null
}
Get-ChildItem -Path 'C:\t','C:\f','C:\u','C:\s' -File | ForEach-Object {
if ($_.LastWriteTime -gt $date) {
Write-Host "Copying file '$($_.FullName)'" -ForegroundColor Green
$_ | Copy-Item -Destination $dest
}
else {
Write-Host "File '$($_.FullName)' is too old.. Skipped" -ForegroundColor Yellow
}
}

File renaming with powershell

Script is working almost how it is intended, still struggling with renaming duplicate files. I cannot figure out how to get it to name the files like
filename(1).ext
filename(2).ext
the closest I have gotten was
filename(1).ext
filename(1)(2).ext
#region actual script
$srcRoot = "C:\srcLocation"
$dstRoot = "C:\dstLocation"
$fileList = Get-ChildItem -Path $srcRoot -File -Force -Recurse
foreach ($file in $fileList) {
$fileName = $file.Name.ToUpper()
$fileExt = $file.Extension.ToUpper()
$dstFileName = $null
switch -Regex ($fileName)
{
'[A-Z]{4}-[0-9]{3}' { $dstFileName = $fileName }
'[A-Z]{4} [0-9]{3}' { $dstFileName = $fileName -replace '([A-Z]{4})\s([0-
9]{3})','$1-$2' }
'[A-Z]{4}[0-9]{3}' { $dstFileName = $fileName -replace '([A-Z]{4})([0-9]
{3})','$1-$2'}
Default { Write-Warning -Message "$fileName is not an expected filename"
}
}
if ($dstFileName) {
$dstDir = $dstFileName.Split('.')[0].Substring(0,8)
$dstPath = Join-Path -Path $dstRoot -ChildPath $dstDir
if (-not (Test-Path -Path $dstPath)) {
New-Item -Path $dstPath -ItemType Directory
}
$i = 1
if (test-path $dstPath\$dstFileName){
$dstFileName = $dstFileName.Split('.')[0] + "($i)" + $fileExt
While (test-path $dstPath\$dstFileName){
$i +=1
$dstFileName = $dstFileName -replace
}
}
Write-Verbose "Moving $($file.FullName)"
Move-Item -Path $($file.FullName) -Destination $dstPath\$dstFileName -
ErrorAction Continue
}
}
#endregion
You can simply use the Replace method of string objects in PowerShell. To verify your input, you can use a RegEx. Move-Item will throw an error, if the file already exists in the destination anyways. The complete script would look like this.
#region setup
New-Item -Path C:\srcpath,C:\dstpath -ItemType Directory
Set-Location C:\srcpath
New-Item 'ABCD123.txt','ABCD 123.txt','AbCD-123.txt','AAAA111.txt','BBBB 222.jpg','BBBB-222.txt' -ItemType File
#endregion
#region actual script
$srcRoot = "C:\srcpath"
$dstRoot = "C:\dstpath"
$fileList = Get-ChildItem -Path $srcRoot -File -Force -Recurse
foreach ($file in $fileList) {
$fileName = $file.Name.ToUpper()
$dstFileName = $null
switch -Regex ($fileName)
{
'[A-Z]{4}-[0-9]{3}' { $dstFileName = $fileName }
'[A-Z]{4} [0-9]{3}' { $dstFileName = $fileName -replace '([A-Z]{4})\s([0-9]{3})','$1-$2' }
'[A-Z]{4}[0-9]{3}' { $dstFileName = $fileName -replace '([A-Z]{4})([0-9]{3})','$1-$2'}
Default { Write-Warning -Message "$fileName is not an expected filename" }
}
if ($dstFileName) {
$dstDir = $dstFileName.Split('.')[0]
$dstPath = Join-Path -Path $dstRoot -ChildPath $dstDir
if (-not (Test-Path -Path $dstPath)) {
New-Item -Path $dstPath -ItemType Directory
}
Write-Verbose "Moving $($file.FullName)"
Move-Item -Path $($file.FullName) -Destination $dstPath\$dstFileName -ErrorAction Continue
}
}
#endregion
#region result
Write-Host '----- Result -----' -BackgroundColor DarkYellow
Get-ChildItem C:\dstpath -Recurse | Select-Object -ExpandProperty FullName
#endregion
Gets the Files in Source folder (Get-ChildItems)
Renames The File to include a - instead of a " " (Rename-Item)
Sets the Child Name property to the new name ($File.Name)
Creates new Folder in source based on first 4 Chars
Moves-Item to new created folder (move-item)
$Source = "C:\Start"
$Destination = "C:\End"
foreach($File in (Get-ChildItem -Path $Source -File -Recurse)){
Rename-Item $File.Fullname ($File.Name -replace " ", "-")
$file.Name = ($File.Name -replace " ", "-")
New-Item "$($Destination)\$($File.Name.Substring(0,3))" -ItemType directory
move-item $File.FullName -force -destination $Destination\$($File.Name.Substring(0,3))
}