Find the oldest file in each subdirectory with Powershell - powershell

My company recently moved to outlook365. We are entirely VDI based so our user profiles are stored on a single server. As a result our users all now have 2+ .ost files taking up storage space on the server. I'd like to write a script to find and delete the extraneous .ost files. In addition I'd like to schedule the script to run on a monthly basis to clean up any orphaned .ost's that occur for any other reason.
I've tried a few different solutions but can't seem to find the right syntax to identify just the oldest/original .ost in each subdirectory, all attempts have identified the oldest file from the whole directory or all .ost files in the directory.
$Path = "<path>"
$SubFolders = dir $Path -Recurse | Where-Object {$_.PSIsContainer} | ForEach-Object -Process {$_.FullName}
ForEach ($Folder in $SubFolders)
{
$FullFileName = dir $Folder | Where-Object {!$_.PSIsContainer} | Sort-Object {$_.LastWriteTime} -Descending | Select-Object -First 1
}

Inside of your loop, you could use the following to list the .ost file that has the oldest LastWriteTime value. Just add the -Descending flag to Sort-Object to list the newest file.
$FullFileName = foreach ($folder in $Subfolders) {
$Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property LastWriteTime |
Select-Object -Property FullName -First 1
}
$FullFileName
If there is only one .ost file found in the $folder path, it will still find that file. So you will need logic to not delete when there is only one file. This does not guarantee it is the oldest file. You probably want a combination of the oldest CreationTime and newest LastWriteTime. The following will list the oldest .ost file based on CreationTime.
$FullFileName = foreach ($folder in $Subfolders) {
Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property CreationTime |
Select-Object -Property FullName -First 1
}
$FullFileName
Another issue is setting the $FullFileName variable inside of the foreach loop. This means it will be overwritten through each loop iteration. Therefore, if you retrieve the value after the loop completes, it will only have the last value found. Setting the variable to be the result of the foreach loop output will create an array with multiple values.
To only output an OST file path when there are multiple OST files, you can do something like the following:
$FullFileName = foreach ($folder in $Subfolders) {
$files = Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property LastWriteTime -Descending
if ($files.count -ge 2) {
$files | Select-Object -Property FullName -First 1
}
$FullFileName

This one liner should do the job, keeping the ost file with the newest LastWriteTime
gci -Path $Path -directory | where {(gci -Path $_\*.ost).count -gt 1}|%{gci -Path $_\*.cmd|Sort-Object LastWriteTime -Descending|Select-Object -Skip 1|Remove-Item -WhatIf}
Longer variant follows.
$Path = '<path>'
$Ext = '*.ost'
Get-ChildItem -Path $Path -directory -Recurse |
Where-Object {(Get-ChildItem -Path "$_\$Ext" -File -EA 0).Count -gt 1} |
ForEach-Object {
Get-ChildItem -Path "$_\$Ext" -File -EA 0| Sort-Object LastWriteTime -Descending |
Select-Object -Skip 1 | Remove-Item -WhatIf
}
The first two lines evaluate folders with more than one .ost file
The next lines iterates those folders and sort them descending by LastWriteTime, skips the first (newest) and pipes the other to Remove-Item with the -WhatIf parameter to only show what would be deleted while testing.
You can of course also move them to a backup location instead.

Related

How to delete all but the most recent files in a directory of folders and files

I have a folder that has a bunch of backups in in separated by folder. I want a script to use the directory (C:\Users\user\Desktop\TEST) and in that directory I have any number of folders with any number of files in them, I only want to keep the latest in the folder for every folder in the directory and delete the rest.
I have this but it only does 1 folder at a time and it has to be hardcoded.
$path = "C:\Users\user\Desktop\TEST\Folder1"
$FileNumber = (get-childitem $path).count - 1
get-childitem -path $path | sort CreationTime -Descending | select -last $FileNumber | Remove-Item -Force -WhatIf
Is there any way to automate this?
Thanks,
You can try this:
$Path = "C:\Users\user\Desktop\TEST"
$Folders = Get-ChildItem $Path
foreach ($Folder in $Folders)
{
$FolderName = $Folder.FullName
$Files = Get-ChildItem -Path $FolderName
$FileNumber = $Files.Count - 1
$Files | sort CreationTime -Descending | select -last $FileNumber | Remove-Item -Force -WhatIf
}
You would need a loop of your choice to accomplish this, this example uses ForEach-Object. Instead of using Select-Object -Last N you could just use Select-Object -Skip 1 to skip the newest file. Also note the use of -Directory and -File with Get-ChildItem to filter only directories or only files.
$path = "C:\Users\user\Desktop\TEST\Folder1"
# Get all Directories in `$path`
Get-ChildItem $path -Directory | ForEach-Object {
# For each Directory, get the Files
Get-ChildItem $_.FullName -File |
# Sort them from Newest to Oldest
Sort-Object CreationTime -Descending |
# Skip the first 1 (the newest)
Select-Object -Skip 1 |
# Remove the rest
Remove-Item -Force -WhatIf
}

Powershell Copy Last Modified File

I'm trying to copy some files from a source to a destination, however the source has multiple folders and in the folders I'd like to copy just the last file.
I can isolate the last file with the folowing:
gci 'D:\Data' | sort LastWriteTime | select -last 1 -recurse
It only selects the last file in the folder, so I thought a loop would do it, such as:
$file = gci 'D:\Data' foreach ($files in $file) { sort LastWriteTime | select -last 1 | Copy-Item C:\Test\data}
However this keeps failing
Can someone point me in the right direction.
Get-ChildItem D:\Data -Directory | ForEach-Object {
Get-ChildItem $_.FullName -File -Recurse |
Sort-Object -Property LastWriteTime |
Select-Object -Last 1 |
Copy-Item -Destination C:\Test\data
}

Powershell Delete Files but keep last x version

I have a folder structure with, for example, 100 folders. Each folder has 200 files in it.
I would like to delete (via scheduled task) all files in each folder but keep the last 10 versions of it.
I am trying to upskill in Powershell so I am guessing that this should be pretty simple. I have created this script,
#Delete all files, keep last 10 versions#
$Directory = "D:\Octopus\Packages"
$Keep = "10"
Get-ChildItem $Directory| ?{ $_.PSIsContainer } | Select-Object FullName | Export-Csv $Directory\FolderList.csv
$FolderList = import-csv $Directory\FolderList.csv
ForEach ($row in $FolderList)
{
Get-ChildItem -Recurse | where{-not $_.PsIsContainer}| sort CreationTime -desc | select -Skip $Keep | Remove-Item -Force
}
It appears to be looping through each folder, but keeping the last 10 files for the entire folder structure, not per folder. So some folders have 0 files, some may have 2 files, some may have 8 files.
Any pointers would be appreciated
Thanks !
If you actually need to have that CSV then just modify Get-ChildItem -Recurse to Get-ChildItem $row -recurse. However, if you don't need to be creating the CSV, you can remove of that and just pipe the results of your first Get-ChildItem into the next action.
$Directory = "D:\Octopus\Packages"
$Keep = "10"
Get-ChildItem $Directory| ?{ $_.PSIsContainer } | Select-Object FullName |
ForEach-object {Get-ChildItem $_.fullname -Recurse |
where{-not $_.PsIsContainer}| sort CreationTime -desc |
select -Skip $Keep | Remove-Item -Force }

Powershell find folders, delete files leaving latest 5

We use software called Revit, files are saved as such: filename.rvt
Each time a user edits a file, Revit takes it upon itself to save the old file in the format filename.xxxx.rvt (where xxx is a number).
Over time when files are edited hundreds of times, we have many unnecessary files on the file server.
I am writing a script to:
Locate and folders containing Revit backup files
Delete all but the most recently modified 5 revit backup files
I have tried two approaches below
$searchpath = "e:\"
# Find a unique list of directories that contains a revit backup file (*.*.rvt)
$a = Get-ChildItem -Path $searchpath -Include *.*.rvt -Recurse | Select-object Directory -expandproperty FullName | Get-Unique -AsString
# For each folder that contains a single revit backup file (*.*.rvt)...
# - Sort by modified time
# - Select all except first 5
$a | Get-ChildItem -Include *.*.rvt | Sort-Object LastWriteTime -descending | select-object -skip 5 -property Directory,Name,CreationTime,LastWriteTime | Out-GridView -Title "Old Backups" -PassThru
The issue with this approach is that it only "skips" the first 5 files in the entire search result, not 5 in each folder.
Then I went about it using a loop, and this gets nowhere:
$searchpath = "e:\"
# Find a unique list of directories that contains a revit backup file (*.*.rvt)
$a = Get-ChildItem -Path $searchpath -Include *.*.rvt -Recurse | Select Directory | Get-Unique -AsString
# For each folder that contains a single revit backup file (*.*.rvt)...
# - Sort by modified time
# - Select all except first 5
$a | foreach {
$b += Get-ChildItem -Path $_.Directory.FullName -Include *.*.rvt | Sort-Object LastWriteTime -descending | select-object -skip 5 -property Directory,Name,CreationTime,LastWriteTime
}
$b | Out-GridView -Title "Old Backups" -PassThru
Any thoughts on the correct approach and whats going wrong?
try this:
get-childitem -file -recurse | group Directory | where Count -gt 5 | %{
$_.Group | Sort LastWriteTime -descending | select -skip 5 Directory,Name,CreationTime,LastWriteTime
} | Out-GridView -Title "Old Backups"
If you want delete you can do it (remove what if)
gci -file -recurse | group Directory | where Count -gt 5 | %{
$_.Group | Sort LastWriteTime -descending | select -skip 5 | remove-item -WhatIf
}
The key to do what you seek is to use the Group-Object cmdlet.
In your case, the group you want to create is a group containing all items in the same folder. This will give you something like this:
From there, you can perform actions on each group, such as selecting all the files while skipping the first 5 of each folders and deleting the remaining.
See this simple minimalist example:
$Path = 'C:\__TMP\1'
$Items = Get-ChildItem -Path "$path\*.rvt" -Recurse | Group-Object -Property PsparentPath
Foreach ($ItemsGroup in $Items) {
$SortedFiles = $ItemsGroup.Group | sort LastWriteTime -Descending
$SortedFiles | Select-Object -Skip 5 | % {Write-host "Deleting $($_.FullName)"; Remove-Item $_.FullName}
}
Try something like this:
$searchpath = "E:\"
$number = 5
$directories = Get-ChildItem -Path $searchpath -Include *.*.rvt -Recurse | Where-Object {$_.PsIsContainer}
foreach ($dir in $directories)
{
$files = Get-ChildItem -Path $dir.FullName | Where-Object {-not $_.PsIsContainer}
if ($files.Count -gt $number)
{
$files | Sort-Object CreationTime | Select-Object -First ($files.Count - $number) | Remove-Item -Force
}
}
Change the placeholders accordingly. I just gave you the logical approach.
An alternative solution that doesn't require grouping first and instead processes each directory separately:
& { Get-Item $path; Get-ChildItem -Directory -Recurse $path } | # get all dirs.
ForEach-Object { # for each dir.
Get-ChildItem -File $_.FullName/*.*.rvt | # get backup files in dir.
Sort-Object -Descending LastWriteTime | # sort by last-write time, newest first
Select-Object -Skip 5 | # skip the 5 newest
Remove-Item -Force -WhatIf # delete
}
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.

Add folder date to the foldername

I have a script which recursively modifies the "creation date" and the"modified date" of folders to match the oldest and newest files in folders (image files).
After that I want to add the creation date of each folder as a prefix to its name. I have written the code that creates the desired name but I don't know how to rename the folders.
$colFolder = Get-ChildItem -Recurse "." |
Where-Object {$_.mode -match "d"} |
Sort-Object Fullname -Descending
$VerbosePreference = "Continue"
foreach ($strFolder In $colFolder) {
Trap [Exception] {
Write-Verbose "TRAPPED:"
Write-Verbose $_.Exception.Message;
Write-Verbose $strFolder.FullName
Continue
}
$Path = $strFolder.FullName
$Folder = Get-Item $Path
# Get Newest file in folder
$strOldestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -First 1).LastWriteTime
$strNewestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -Last 1).LastWriteTime
# Change the date to match the newest file if it doesn't already
$Folder.CreationTime = $strOldestTime
$Folder.LastWriteTime = $strNewestTime
# add the date as the prefix of the folder (in which the files are)
$date = $strOldestTime.ToString('yyyy-MM-dd')
$foldername = $Folder.Name.split("\")[-1]
Write-Verbose "$date- $foldername"
#now rename the folder to above name
}
Renaming folders can be done as #onupdatecascade described in his answer.
Rename-Item -Path $Folder.FullName -NewName $newname
If you have at least PowerShell v3 (which I highly recommend) you can use the parameter -LiteralPath for the current folder path to avoid issues with special characters:
Rename-Item -LiteralPath $Folder.FullName -NewName $newname
With that said, there are a couple improvements to your code I'd like to suggest.
Get-ChildItem -Recurse ".":
Get-ChildItem uses the current working directory by default, so "." can be omitted.
Where-Object {$_.mode -match "d"}:
The objects you get from Get-ChildItem have a boolean property IsContainer that indicates whether you have a folder or not. No need to use a regex match on the mode. Also, in PowerShell v3 and newer you can omit this step entirely, because the Get-ChildItem cmdlet has an additional parameter -Directory that lists only folders.
Misused hungarian notation (pt.1):
foreach ($strFolder In $colFolder) {
The loop variable $strFolder does not contain strings, but DirectoryInfo objects. This is a prime example for why I consider hungarian notation a pointless waste of space.
Avoid unnecessary steps.
$Path = $strFolder.FullName
$Folder = Get-Item $Path
$strFolder already contains a DirectoryInfo object. The above two statements just convert it to a path string and back to a DirectoryInfo object. Simply make $Folder your loop variable (see above).
Avoid duplicate code (particularly duplicate disk operations):
$strOldestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -First 1).LastWriteTime
$strNewestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -Last 1).LastWriteTime
Instead of listing and sorting the child items multiple times run the statement just once and collect the result in a variable:
$items = Get-ChildItem $Path | Sort-Object LastWriteTime
$strOldestTime = ($items | Select-Object -First 1).LastWriteTime
$strNewestTime = ($items | Select-Object -Last 1).LastWriteTime
Also, I would simply expand the LastWriteTime property instead of running Select-Object in a subexpression:
$strOldestTime = $items | Select-Object -First 1 -Expand LastWriteTime
$strNewestTime = $items | Select-Object -Last 1 -Expand LastWriteTime
Misused hungarian notation (pt.2):
$strOldestTime and $strNewestTime contain DateTime objects, not strings.
$foldername = $Folder.Name.split("\")[-1]:
The Name property already contains the folder name without path, so there's nothing to split off.
Construct the new folder name from the date and the current name by using the format operator:
$newname = '{0:yyyy-MM-dd} - {1}' -f $oldestTime, $Folder.Name
If there's a possibility that you'll run the script multiple times you may want to remove an existing leading date from the folder name so that the dates won't "stack":
$Folder.Name -replace '^\d{4}-\d{2}-\d{2} - '
Simplified code:
$colFolder = Get-ChildItem -Recurse |
Where-Object { $_.PSIsContainer } |
Sort-Object Fullname -Descending
foreach ($folder in $colFolder) {
$items = Get-ChildItem $folder.FullName | Sort-Object LastWriteTime
$folder.CreationTime = $items | Select-Object -First 1 -Expand LastWriteTime
$folder.LastWriteTime = $items | Select-Object -Last 1 -Expand LastWriteTime
$newname = '{0:yyyy-MM-dd} - {1}' -f $folder.CreationTime, ($folder.Name -replace '^\d{4}-\d{2}-\d{2} - ')
Rename-Item -Path $folder.FullName -NewName $newname
}
or (PowerShell v3 and newer):
$colFolder = Get-ChildItem -Recurse -Directory |
Sort-Object Fullname -Descending
foreach ($folder in $colFolder) {
$items = Get-ChildItem $folder.FullName | Sort-Object LastWriteTime
$folder.CreationTime = $items | Select-Object -First 1 -Expand LastWriteTime
$folder.LastWriteTime = $items | Select-Object -Last 1 -Expand LastWriteTime
$newname = '{0:yyyy-MM-dd} - {1}' -f $folder.CreationTime, ($folder.Name -replace '^\d{4}-\d{2}-\d{2} - ')
Rename-Item -LiteralPath $folder.FullName -NewName $newname
}
Have you tried
Rename-Item oldname newname
It works for simple examples, but could break with folders nested in other folders, and recursion, because you might rename a folder before the script is done with it.
To manage that you can do them in order by length (paths to children being by definition longer than the paths of the parents). Good example here: How do I recursively rename folders with Powershell?