I have this simple cmdlet that correctly copies files and folders to a second directory:
Copy-Item -Path 'G:\xyz\Test\A' -Recurse -Destination 'G:\xyz\Test\B\'
However I am unable to tweak it to only copy the latest file in each folder within its folder (i.e. also copying the folder structure). I have written the following, but this doesn't copy folder names and does not go down all the hierarchies of sub-folders.
Get-ChildItem -Path 'G:\xyz\Test\A' -Directory | ForEach-Object {
Get-ChildItem -Path 'G:\xyz\Test\A' -File -Recurse |
Sort-Object LastWriteTime | Select-Object -Last 1 |
Copy-Item -Destination 'G:\xyz\Test\B\'
}
Could someone please identify my errors!
A slightly different approach of collecting the directories first then iterating through them to retrieve the files.
If ( -not (Test-Path -Path "G:\Test\B")) {
$Null = New-Item -ItemType "Directory" "G:\Test\B"
}
Get-ChildItem -Path 'G:\Test\A' -Directory -Recurse |
#Sort to insure dirs are created shortest to longest
Sort-Object FullName |
ForEach-Object {
$DestPath = $($_.FullName).Replace("`\Test`\A","`\Test\`B")
If ( -not (Test-Path -Path "$DestPath")) {
$Null = New-Item -ItemType "Directory" "$DestPath"
}
Get-ChildItem -Path "$($_.FullName)" -File |
Sort-Object LastWriteTime |
Select-Object -Last 1 |
Copy-Item -Destination "$DestPath"
}
First error is that you don't pass the current directory from the outer Get-ChildItem to the inner one. The inner one currently always loops over the same sub directory.
Also, when you copy individual files in a Get-ChildItem pipeline, you have to build up the destination path on your own, in order to keep the relative source directory structure:
$source = 'G:\xyz\Test\A'
$destination = 'G:\xyz\Test\B\'
# Set base directory for outer Get-ChildItem and Resolve-Path -Relative
Push-Location $source
try {
Get-ChildItem -Directory | ForEach-Object {
Get-ChildItem -Path $_.Fullname -File -Recurse |
Sort-Object LastWriteTime | Select-Object -Last 1 |
ForEach-Object {
# Make the current file path relative to $source
$relativePath = Resolve-Path $_.Fullname -Relative
# Build up the full destination file path
$destinationFullPath = Join-Path $destination $relativePath
# Create destination directory if not already exists (-force)
$null = New-Item (Split-Path $destinationFullPath -Parent) -ItemType Directory -Force
# Copy the current file
Copy-Item -Path $_.Fullname -Destination $destinationFullPath
}
}
}
finally {
Pop-Location # Restore the current directory
}
Resolve-Path -Relative makes the given path relative to the current directory. In order to get paths relative to the source directory, we set the current directory to the source directory path using Push-Location.
The try and finally blocks make sure the current directory is restored even in case of a script-terminating error (exception).
Related
Here is my current script and it works fine. Not efficient running same code twice but I don't know how to combine the wildcards... anyway on to the bigger issue.
The below code searches through my $sourceDir, excludes the files listed in $ExclusionFiles, copies all folders and structure as well as any .jpg or any .csv files, then puts them into the $targetDir.
$sourceDir = 'c:\sectionOne\Graphics\Data'
$targetDir = 'C:\Test\'
$ExclusionFiles = #("InProgress.jpg", "input.csv", "PCMCSV2.csv")
# Get .jpg files
Get-ChildItem $sourceDir -filter "*.jpg" -recurse -Exclude $ExclusionFiles | `
foreach{
$targetFile = $targetDir + $_.FullName.SubString($sourceDir.Length);
New-Item -ItemType File -Path $targetFile -Force;
Copy-Item $_.FullName -destination $targetFile
}
# Get .csv files
Get-ChildItem $sourceDir -filter "*.csv" -recurse -Exclude $ExclusionFiles | `
foreach{
$targetFile = $targetDir + $_.FullName.SubString($sourceDir.Length);
New-Item -ItemType File -Path $targetFile -Force;
Copy-Item $_.FullName -destination $targetFile
}
My list of files in the main $sourceDir that I need to exclude is getting longer and there are folders I want to exclude as well. Can someone tell me how to,
Copy only a list of specific files in the $sourceDir
Exclude certain folders in $sourceDir from copying
Combine the wildcard search for .jpg and .csv into one statement
I'm still learning so any help would be greatly appreciated!
This is a case where a little bit of Regex will go a long way:
You can filter multiple extensions by using a pretty basic match:
$extensions = 'jpg', 'csv'
$endsWithExtension = "\.(?>$($extensions -join '|'))$"
Get-ChildItem -Recurse |
Where-Object Name -Match $endsWithExtension
You can exclude a list of specific files with one more Where-Object and the -In parameter:
$extensions = 'jpg', 'csv'
$endsWithExtension = "\.(?>$($extensions -join '|'))$"
$ExcludeFileNames = #("InProgress.jpg", "input.csv", "PCMCSV2.csv")
Get-ChildItem -Recurse |
Where-Object Name -Match $endsWithExtension |
Where-Object Name -NotIn $ExcludeFileNames
From there on in, your Foreach-Object is basically correct (nice touch making sure the file exists by using New-Item, though I'd personally assign it's output to null and -PassThru the Copy-Item).
Get-ChildItem $sourceDir -Recurse |
Where-Object Name -Match $endsWithExtension |
Where-Object Name -NotIn $ExcludeFileNames |
Foreach-Object {
$targetFile = $targetDir + $_.FullName.SubString($sourceDir.Length);
New-Item -ItemType File -Path $targetFile -Force;
Copy-Item $_.FullName -destination $targetFile
}
I am trying to copy the latest file from every folder/sub-folder into a same file structure on a different drive.
Latest file from source copied to the same name corresponding destination.
The destination folder hierarchy already exists & cannot be copied over or recreated. This & other versions are not behaving. Can anyone help?
$sourceDir = 'test F Drive\Shares\SSRSFileExtract\'
$destDir = 'test X Drive\SSRSFileExtract\'
$date = Get-Date
$list = Get-ChildItem -Path $sourceDir | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1
foreach ($item in $list)
{
Copy-Item -Verbose -LiteralPath $item.FullName -Destination $destDir -Force |
Get-Acl -Path $item.FullName | Set-Acl -Path $destDir\$(Split-Path -Path $item.FullName -Leaf)
}
Get-ChildItem –Path $destDir -Recurse | Where-Object {($_.LastWriteTime -lt (Get-Date).AddDays(-5))} | Remove-Item -Verbose -Recurse -Force
I found a solution for this which copies/moves all the files from all sub folders in to all corresponding sub folders:
Powershell: Move all files from folders and subfolders into single folder
The way your code retrieves the list of files will only return one single object because of Select-Object -First 1. Also, because you don't specify the -File switch, Get-ChildItem will also return DirectoryInfo objects, not just FileInfo objects..
What you could do is get an array of FileInfo objects recursively from the source folder and group them by the DirectoryName property
Then loop over these groups of files and from each of these groups, select the most recent file and copy that over to the destination folder.
Try:
$sourceDir = 'F:\Shares\SSRSFileExtract'
$destDir = 'X:\SSRSFileExtract'
Get-ChildItem -Path $sourceDir -File -Recurse | Group-Object DirectoryName | ForEach-Object {
# the $_ automatic variable represents one group at a time inside the loop
$newestFile = $_.Group | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1
# construct the target sub directory
# you could also use $_.Name (the name of this group) instead of $newestFile.DirectoryName here, because
# we grouped on that DirectoryName file property.
$targetDir = Join-Path -Path $destDir -ChildPath $newestFile.DirectoryName.Substring($sourceDir.Length)
# if you're not sure the targetpath exists, uncomment the next line to have it created first
# if (!(Test-Path -Path $targetDir -PathType Container)) { $null = New-Item -Path $target -ItemType Directory }
# copy the file
$newestFile | Copy-Item -Destination $targetDir -Force -Verbose
# copy the file's ACL
$targetFile = Join-Path -Path $targetDir -ChildPath $newestFile.Name
$newestFile | Get-Acl | Set-Acl -Path $targetFile
}
Apparently you would also like to clean up older files in the destination folder
Get-ChildItem –Path $destDir -File -Recurse |
Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-5).Date} |
Remove-Item -Verbose -Recurse -Force
Be aware that the final code to remove older files could potentially remove all files from a subfolder if all happen to be older than 5 days..
I currently have 20000+ folders that where given a random string of characters when created. I would like to rename each folder with the name of the last PDF modified within each folder. I'm definitely in over my head. The current script seems to just move the PDF and/or folder without renaming it or creating a folder with the PDF name.
Get-ChildItem -Path $SourceFolder -Filter *.pdf |
ForEach-Object {
$ChildPath = Join-Path -Path $_.Name.Replace('.pdf','') -ChildPath $_.Name
[System.IO.FileInfo]$Destination = Join-Path -Path $TargetFolder -ChildPath $ChildPat
if( -not ( Test-Path -Path $Destination.Directory.FullName ) ){
New-Item -ItemType Directory -Path $Destination.Directory.FullName
}
Copy-Item -Path $_.FullName -Destination $Destination.FullName
}
Welcome, Robert! There's a few things going on with your script:
There's a typo: $ChildPat
You don't need a FileInfo object to create the new directory, and you can't create one from a non-existent path. $Destination = Join-Path $_.Directory $_.BaseName will get the new folder name more reliably, in the unusual case where the file name has embedded '.pdf'
It doesn't get the latest PDF.
Assuming you only want to get folders that have a PDF, you should have a nested Get-ChildItem for each folder, as #Lee_Dailey recommended:
Push-Location $SourceFolder
Foreach ($dir in (Get-ChildItem *.pdf -Recurse | Group-Object Directory | Select Name )){
Push-Location $dir.Name
$NewestPDF = Get-ChildItem *.pdf | Sort-Object ModifiedDate | Select -Last 1
$Destination = Join-Path $dir.Name "..\$($NewestPDF.BaseName)"
If(!(Test-Path $Destination)){New-Item $Destination -ItemType Directory}
Copy-Item *.PDF $Destination
Pop-Location
#Remove-Item $dir.Name #uncomment to remove the old folder (is it empty?)
}
All my files are in specific folders:
17\1\1\PRO
17\1\2\PRO
17\2\1\PRO
xx\xx\xx\PRO
17 is the year (so 18 for next year etc)
the first 1 is the folder specifying the case number (can be up to 100).
The second 1 is the sub parts on the case number.
That last 1 has a folder PRO in it where all data resides.
We need to move these files, but the files need to stay inside their respective "PRO" folders.
For example:
a file in 17\1\1\pro\xxx\www\ needs to go to 17\1\1\pro\movies
a file in 17\2\2\pro\xxdfsdf\eeee\ needs to go to 17\2\2\pro\movies.
The movies folder should get created if there are files to move.
I need to get a part of the full name of a file and move the file there to the "movie" folder. The problem is I do not know how to split the full name, add \movies to it and move the files there.
This is my code so far:
Get-ChildItem -Path $mypath -Recurse -File -Filter $extension | select $_Fullname |
Move-Item -Force -Destination ($_Fullname.Split("pro"))
If the destination is always "movies subdirectory of the grandparent directory of the file's directory" you can build the destination path relative to the file's location:
Get-ChildItem ... | ForEach-Object {
$dst = Join-Path $_.Directory '..\..\movies'
if (-not (Test-Path -LiteralPath $dst -PathType Container)) {
New-Item -Type Directory -Path $dst | Out-Null
}
Move-Item $_.FullName -Destination $dst
}
If the PRO directory is your anchor you could use a regular expression replacement like this instead:
Get-ChildItem ... | ForEach-Object {
$dst = $_.Directory -replace '^(.*\\\d+\\\d+\\\d+\\PRO)\\.*', '$1\movies'
if (-not (Test-Path -LiteralPath $dst -PathType Container)) {
New-Item -Type Directory -Path $dst | Out-Null
}
Move-Item $_.FullName -Destination $dst
}
If you don't know how many directories there are, I would do something like this:
Get-ChildItem -Path $mypath -Recurse -File -Filter $extension | ForEach-Object {
if ($_.FullName.IndexOf('\PRO\') -gt 0) {
$Destination = Join-Path -Path $_.FullName.Substring(0,$_.FullName.IndexOf('\PRO\') + 5) -ChildPath 'movies';
New-Item $Destination -ItemType Directory -ea Ignore;
$_ | Move-Item -Destination $Destination;
} else {
throw ("\PRO\ path not found in '$($_.FullName)'");
}
}
This will work fine as long as your paths only have \pro\ once. If they have it more than once like customer\pro\17\pro\17\1\1\pro\xx\yy\zz\www and you need the last index, then use $_.FullName.LastIndexOf('\pro\').
If you've got \pro\ directories both before and after the directory that .\pro\movies\ is in, well, you're in trouble. You'll probably have to find a different point of reference.
With a set of folders
17\1\1\PRO
17\1\2\PRO
17\2\1\PRO
You could try the following
$RootPaths = Get-ChildItem -Path C:\folder\*\*\*\pro
$RootPaths will then contain all 3 paths mentioned above and the code below will move all files to the appropriate directory.
ForEach( $Path in $RootPaths)
{
$Movies = Join-Path $Path -Child "Movies"
If( -not (Test-Path $Movies ) ) { New-Item -Path $Movies -ItemType Directory }
Get-ChildItem -Path $Path -Recurse -File -Filter $Extension |
Move-Item -Path $_.FullName -Destination "$( $Path )\Movies"
}
This way it doesn't matter how many levels down your files are. They always get moved to the same directory.
I need to copy a large number of files to a backup folder but I want to maintain their relative paths. I only need specific files; i.e.
C:\scripts\folder\File.ext1
C:\scripts\folder2\file2.ext2
C:\scripts\file3.ext1
But I only need to copy the ext1 files like so:
C:\backup\folder\File.ext1.bak
C:\backup\file3.ext1.bak
The source paths are of multiple depths.
This is what I have to copy the files:
$files = gci -path C:\scripts\ -recurse -include *.ext1
$files | % { Copy-Item $_ "$($_).bak"; move-item $_ -destination C:\backup\ }
This just dumps all the files into C:\backup\ and does not appear to get any of the paths. Not sure how that part would be done.
Something like this could work:
gci -path C:\scripts\ -recurse -include *.ext1 |
% { Copy-Item $_.FullName "$($_.FullName).bak"
move-item $_.FullName -destination ($_.FullName -replace 'C:\\scripts\\','C:\backup\') }
It is not clever, but it's quick & dirty and works without a lot of effort.
get-childitem returns absolute paths, but you can make them relative to the current working directory as follows:
resolve-path -relative
So to copy a filtered set of files from the current directory recursively to a destination directory:
$dest = "c:\dest"
$filter = "*.txt"
get-childitem -recurse -include $filter | `
where-object { !$_.PSIsContainer } | `
resolve-path -relative | `
% { $destFile = join-path $dest $_; new-item -type f $destFile -force | out-null; copy-item $_ $destFile; get-item $destfile; }
new-item is needed to create the parent directories
get-item provides a display of all the new files it created
Of course robocopy does all this, but there will be times when you want to do more special filtering or filename mangling...
Use robocopy.
robocopy c:\scripts c:\backup *.ext1 /s
Oops. I failed to notice you wanted to add the .bak extension too. I still think it is a good idea to use robocopy to copy the files then:
dir c:\backup -recurse -include *.ext1 | % { ren $_ "$_.bak" }
You can try this
Clear-Host
$from = "'C:\scripts\"
$to = "'C:\backup\"
$inc = #('*.ext1', '*.extx')
$files = get-childItem -path $from -include $inc -Recurse
$files | % {$dest = (Join-Path $to $($_.FullName+".bak").SubString($from.length)); $dum = New-Item -ItemType file $dest -Force; Copy-Item -Path $_ -Destination $dest -Recurse -Force }
the new-item is there in order to force path creation.
Jean Paul