I have this script which parses all shares on a file server to gather information on share size, ACLs, and count of files and folders. The script works great on smaller file servers but on hosts with large shares it consumes all RAM and crashes the host, I can't seem to figure out how to optimize the script during the Get-ChildItem portion to not consume all RAM.
I found a few articles which mentioned to use a foreach loop and pipe out what I need. I am a Powershell beginner, I can't figure out how to get it to work like that. What can I try next?
$ScopeName = Read-Host "Enter scope name to gather data on"
$SavePath = Read-Host "Path to save results and log to"
$SaveCSVPath = "$SavePath\ShareData.csv"
$TranscriptLog = "$SavePath\Transcript.log"
Write-Host
Start-Transcript -Path $TranscriptLog
$StartTime = Get-Date
$Start = $StartTime | Select-Object -ExpandProperty DateTime
$Exclusions = {$_.Description -ne "Remote Admin" -and $_.Description -ne "Default Share" -and $_.Description -ne "Remote IPC" }
$FileShares = Get-SmbShare -ScopeName $ScopeName | Where-Object $Exclusions
$Count = $FileShares.Count
Write-Host
Write-Host "Gathering data for $Count shares" -ForegroundColor Green
Write-Host
Write-Host "Results will be saved to $SaveCSVPath" -ForegroundColor Green
Write-Host
ForEach ($FileShare in $FileShares)
{
$ShareName = $FileShare.Name
$Path = $Fileshare.Path
Write-Host "Working on: $ShareName - $Path" -ForegroundColor Yellow
$GetObjectInfo = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue
$ObjSize = $GetObjectInfo | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
$ObjectSizeMB = "{0:N2}" -f ($ObjSize.Sum / 1MB)
$ObjectSizeGB = "{0:N2}" -f ($ObjSize.Sum / 1GB)
$ObjectSizeTB = "{0:N2}" -f ($ObjSize.Sum / 1TB)
$NumFiles = ($GetObjectInfo | Where-Object {-not $_.PSIsContainer}).Count
$NumFolders = ($GetObjectInfo | Where-Object {$_.PSIsContainer}).Count
$ACL = Get-Acl -Path $Path
$LastAccessTime = Get-ItemProperty $Path | Select-Object -ExpandProperty LastAccessTime
$LastWriteTime = Get-ItemProperty $Path | Select-Object -ExpandProperty LastWriteTime
$Table = [PSCustomObject]#{
'ScopeName' = $FileShare.ScopeName
'Sharename' = $ShareName
'SharePath' = $Path
'Owner' = $ACL.Owner
'Permissions' = $ACL.AccessToString
'LastAccess' = $LastAccessTime
'LastWrite' = $LastWriteTime
'Size (MB)' = $ObjectSizeMB
'Size (GB)' = $ObjectSizeGB
'Size (TB)' = $ObjectSizeTB
'Total File Count' = $NumFiles
'Total Folder Count' = $NumFolders
'Total Item Count' = $GetObjectInfo.Count
}
$Table | Export-CSV -Path $SaveCSVPath -Append -NoTypeInformation
}
$EndTime = Get-Date
$End = $EndTime | Select-Object -ExpandProperty DateTime
Write-Host
Write-Host "Script start time: $Start" -ForegroundColor Green
Write-Host "Script end time: $End" -ForegroundColor Green
Write-Host
$ElapsedTime = $(($EndTime-$StartTime))
Write-Host "Elapsed time: $($ElapsedTime.Days) Days $($ElapsedTime.Hours) Hours $($ElapsedTime.Minutes) Minutes $($ElapsedTime.Seconds) Seconds $($ElapsedTime.MilliSeconds) Milliseconds" -ForegroundColor Cyan
Write-Host
Write-Host "Results saved to $SaveCSVPath" -ForegroundColor Green
Write-Host
Write-Host "Transcript saved to $TranscriptLog" -ForegroundColor Green
Write-Host
Stop-Transcript
To correctly use the PowerShell pipeline (and preserve memory as each item is streamed separately), use the PowerShell ForEach-Object cmdlet (unlike the ForEach statement) and avoid assigning the pipeline to a variable (as you doing with $FileShares = ...) and don't use parenthesis ((...)) arround the the pipeline:
Get-SmbShare -ScopeName $ScopeName | Where-Object $Exclusions | ForEach-Object {
And replace all $FileShare variables in your loop with the current item: $_ variable (e.g. $FileShare.Name → $_.Name).
For the Get-Childitem part you might do the same thing (stream! meaning: use the mighty PowerShell pipeline rather than piling everything up in $GetObjectInfo):
$ObjSize = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
As an aside; you might simplify your 3 size properties to a single smarter size property, see: How to convert value to KB, MB, or GB depending on digit placeholders?
addition
"But isn't putting everything into $ObjSize just swapping one variable for another?"
No it is not, think of the PowerShell pipeline as an assembly line. In this case, at the first station you take each single file information and pass it to the next (last) station where you just sum the length property and the current (file) object disposed.
Where in your question example, you read the information of all files in once and store it into $GetObjectInfo and than go to the whole list to just use (add) the length property of the (quiet heavy) PowerShell file objects.
But why don't you try it?:
Open a new PowerShell session and run:
$Path = '.'
$GetObjectInfo = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue
$ObjSize = $GetObjectInfo | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
Get-Process -ID $PID
Now, open a new session again and use the PowerShell pipeline:
$Path = '.'
$ObjSize = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
Get-Process -ID $PID
Notice the difference in memory usage (WS(M)).
You are buffering the entire collection of [FileSystemInfo] on $FileShare into a variable with...
$GetObjectInfo = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue
So, if there's a million directories and files on that share then that's a million [FileSystemInfo] instances stored in a million-element array, none of which can be garbage collected during that iteration of the foreach loop. You can use Group-Object to improve that a bit...
$groupsByPSIsContainer = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
Group-Object -Property 'PSIsContainer' -AsHashTable
# $groupsByPSIsContainer is a [Hashtable] with two keys:
# - $true gets the collection of directories
# - $false gets the collection of files
$ObjSize = $groupsByPSIsContainer[$false] | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
$NumFiles = $groupsByPSIsContainer[$false].Count
$NumFolders = $groupsByPSIsContainer[$true].Count
...but that still ends up storing all of the [FileSystemInfo]s in the two branches of the [Hashtable]. Instead, I would just enumerate and count the results myself...
$ObjSize = 0L # Stores the total file size directly; use $ObjSize instead of $ObjSize.Sum
$NumFiles = 0
$NumFolders = 0
foreach ($fileSystemInfo in Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue)
{
if ($fileSystemInfo.PSIsContainer)
{
$NumFolders++
}
else
{
$NumFiles++
$ObjSize += $fileSystemInfo.Length
}
}
That stores only the current enumeration result in $fileSystemInfo and never the entire sequence.
Note that if you weren't summing the files' sizes Group-Object would work well...
$groupsByIsContainer = Get-Childitem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
Group-Object -Property 'PSIsContainer' -NoElement
$NumFiles = ($groupsByIsContainer | Where-Object -Property 'Name' -EQ -Value $false).Count
$NumFolders = ($groupsByIsContainer | Where-Object -Property 'Name' -EQ -Value $true ).Count
-NoElement prevents the resulting group objects from storing the grouped elements; we just care about the count of members in each grouping but not the members themselves. If we passed -AsHashTable then we'd lose the convenient Count property, hence why the two groups have to be accessed in this awkward way.
Related
$homefolder = (gci \\SERVER\homefolder | select fullname)
$outfile = "$env:USERPROFILE\Desktop\Homefolder_Desktop_Redirect.csv"
ForEach ($dir in $homefolder)
{If(Test-Path ($dir.FullName +"\Desktop")){write-host $dir.Fullname" contains desktop" -ForegroundColor Yellow
"{0:N2} GB" -f ((Get-ChildItem $dir.fullname -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum / 1GB)
}}
ForEach ($dir in $homefolder)
{If(Test-Path ($dir.FullName +"\Desktop")){}else{write-host $dir.Fullname" does not contain desktop" -ForegroundColor Red
"{0:N2} GB" -f ((Get-ChildItem $dir.fullname -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum / 1GB)
}}
I'm trying to get this to output to a file. If I put the pipe between the last 2 }} or after the last } (in each Foreach), I'm told it's empty. If I put IF inside another set of parentheses, like {(If I get If isn't valid.
If I try to write/append after 1GB) my outfile is just my script.
If I try making the Foreach($dir in $homefolder) a variable, the in is an unexpected token.
I'm sure this is something simple, but I haven't used PowerShell for much in the last 5 years... assistance is appreciated.
---UPDATE---
Thanks for the help, all!
This is what I have thanks to the assistance I've received.
$outfile = "$env:USERPROFILE\Desktop\Homefolder_Desktop_Redirect.txt"
Write-Output "Contains desktop:" | Set-Content $outfile -Force
(Get-ChildItem \\SERVER\homefolder).FullName | ForEach-Object {
if(Test-Path (Join-Path $_ -ChildPath Desktop)) {
Write-Host "$_ contains desktop" -ForegroundColor Yellow
"$_ [{0:N2} GB]" -f (
(Get-ChildItem $_ -Recurse |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
).Sum / 1GB)
}
} | Add-Content $outfile -Force
Write-Output "Contains NO desktop:" | Add-Content $outfile -Force
(Get-ChildItem \\SERVER\homefolder).FullName | ForEach-Object {
if(Test-Path (Join-Path $_ -ChildPath Desktop)) {}
else{
Write-Host "$_ contains no desktop" -ForegroundColor Red
"$_ [{0:N2} GB]" -f (
(Get-ChildItem $_ -Recurse |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
).Sum / 1GB)
}
} | Add-Content $outfile -Force
Invoke-Item $outfile
The main reason why PowerShell complains is because you're looking to pipe after a language keyword which is simply not possible. You can however, use ForEach-Object, a cmdlet designed to enumerate input objects from pipeline, and because it is a cmdlet and not a statement (foreach), you can pipe other cmdlets to it:
(Get-ChildItem \\SERVER\homefolder).FullName | ForEach-Object {
if(Test-Path (Join-Path $_ -ChildPath Desktop)) {
Write-Host "$_ contains desktop" -ForegroundColor Yellow
}
else {
Write-Host "$_ does not contain desktop" -ForegroundColor Red
}
$output = "$_ [{0:N2} GB]" -f (
(Get-ChildItem $_ -Recurse |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue
).Sum / 1GB)
# send output to the host
Write-Host $output
# send output to the success stream
$output
} | Set-Content path\to\export.txt
Generally, if you want to send the output from multiple statements to a single file, enclose them in & { ... } (or . { ... } to run directly in the caller's scope); a simplified example:
& {
foreach ($i in 1..5) { $i }
foreach ($i in 6..10) { $i }
} | Out-File test.txt
However, you can reformulate your code to a single pipeline, using the ForEach-Object cmdlet rather than the foreach loop statement:
$homefolder |
ForEach-Object {
$hasDesktop = Test-Path (Join-Path $_.FullName Desktop)
Write-Host ('{0} {1} desktop' -f $_.FullName, ('does not contain', 'contains')[$hasDesktop]) -ForegroundColor ('Red', 'Yellow')[$hasDesktop]
'Contains {0}desktop' -f ('NO ', '')[$hasDesktop]
'{0:N2} GB' -f ((Get-ChildItem $_.FullName -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum / 1GB)
} |
Out-File $outfile
Note:
Note the technique of letting a Boolean variable $hasDesktop select one of two values from an array, e.g., ('Red', 'Yellow')[$hasDesktop], which allows you to make do with a single Write-Host call to cover both the has-desktop and doesn't-have-desktop case.
This acts similar to the ternary conditional operator, available in PowerShell (Core) 7+ only; that is, the above is equivalent to: ($hasDesktop ? 'Yellow' : 'Red')
The output string is pieced together with the help of -f, the format operator.
As for your desire to print the calculated size to the display as well:
In Windows PowerShell, capture it in a variable first, write its value to the display with Write-Host, then output it to the success stream, as shown in Santiago Squarzon's helpful answer.
In PowerShell (Core) 7+, you can simply pipe to Tee-Object CON, as discussed in this answer.
New to PowerShell.
Some experience with Linux, bash, and programming (Java, C, HTML/CSS/JS). I just started an internship.
I was given a PowerShell script in order to do basic disk clean-up. Part of it pasted below. It writes to both console and a logfile. Some of the servers that I am cleaning having hundreds of thousands of files. I want to increase the performance of the script by only writing to the logfile. It usually starts out pretty strong, but once the console output gets large enough, things start to slow down drastically.
I attempted to simply remove the -verbose tags, but then it doesn't write to either. Then my understanding was that 'SilentlyContinue' would allow print to log, but not console. But the code already has SilentlyContinue flags? I then tried adding-Verbose to some of the for-each statements and that didn't work either.
I'm just kind of running in circles now.
Any ideas or pointers?
function global:Write-Verbose
(
[string]$Message
)
{ # check $VerbosePreference variable
if ( $VerbosePreference -ne 'SilentlyContinue' )
{ Write-Host " $Message" -ForegroundColor 'Yellow' }
}
Write-Verbose
$DaysToDelete = 7
$LogDate = get-date -format "MM-d-yy-HH"
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace(0xA)
Start-Transcript -Path C:\Windows\Temp\$LogDate.log
#Cleans all code off of the screen.
Clear-Host
$Before = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq "3" } | Select-Object SystemName,
#{ Name = "Drive" ; Expression = { ( $_.DeviceID ) } },
#{ Name = "Size (GB)" ; Expression = { "{0:N1}" -f ( $_.Size / 1gb) } },
#{ Name = "FreeSpace (GB)" ; Expression = { "{0:N1}" -f ( $_.Freespace / 1gb ) } },
#{ Name = "PercentFree" ; Expression = { "{0:P1}" -f ( $_.FreeSpace / $_.Size ) } } |
Format-Table -AutoSize | Out-String
## Stops the windows update service.
Get-Service -Name wuauserv | Stop-Service -Force -Verbose -ErrorAction SilentlyContinue
## Windows Update Service has been stopped successfully!
## Deletes the contents of windows software distribution.
Get-ChildItem "C:\Windows\SoftwareDistribution\*" -Recurse -Force -Verbose -ErrorAction SilentlyContinue |
Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays(-$DaysToDelete)) } |
remove-item -force -Verbose -recurse -ErrorAction SilentlyContinue
## The Contents of Windows SoftwareDistribution have been removed successfully!
## Deletes the contents of the Windows Temp folder.
Get-ChildItem "C:\Windows\Temp\*" -Recurse -Force -Verbose -ErrorAction SilentlyContinue |
Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays(-$DaysToDelete)) } |
remove-item -force -Verbose -recurse -ErrorAction SilentlyContinue
## The Contents of Windows Temp have been removed successfully!
## Deletes all files and folders in user's Temp folder.
Get-ChildItem "C:\users\$env:USERNAME\AppData\Local\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays(-$DaysToDelete)) } |
remove-item -force -Verbose -recurse -ErrorAction SilentlyContinue
## The contents of C:\users\$env:USERNAME\AppData\Local\Temp\ have been removed successfully!
## Remove all files and folders in user's Temporary Internet Files.
Get-ChildItem "C:\users\$env:USERNAME\AppData\Local\Microsoft\Windows\Temporary Internet Files\*" -Recurse -Force -Verbose -ErrorAction SilentlyContinue |
Where-Object { ($_.CreationTime -le $(Get-Date).AddDays(-$DaysToDelete)) } |
remove-item -force -recurse -ErrorAction SilentlyContinue
## All Temporary Internet Files have been removed successfully!
## Cleans IIS Logs if applicable.
Get-ChildItem "C:\inetpub\logs\LogFiles\*" -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { ($_.CreationTime -le $(Get-Date).AddDays(-60)) } |
Remove-Item -Force -Verbose -Recurse -ErrorAction SilentlyContinue
## All IIS Logfiles over x days old have been removed Successfully!
## deletes the contents of the recycling Bin.
$objFolder.items() | ForEach-Object { Remove-Item $_.path -ErrorAction Ignore -Force -Verbose -Recurse }
## The Recycling Bin has been emptied!
## Starts the Windows Update Service
Get-Service -Name wuauserv | Start-Service -Verbose
$After = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq "3" } | Select-Object SystemName,
#{ Name = "Drive" ; Expression = { ( $_.DeviceID ) } },
#{ Name = "Size (GB)" ; Expression = { "{0:N1}" -f ( $_.Size / 1gb) } },
#{ Name = "FreeSpace (GB)" ; Expression = { "{0:N1}" -f ( $_.Freespace / 1gb ) } },
#{ Name = "PercentFree" ; Expression = { "{0:P1}" -f ( $_.FreeSpace / $_.Size ) } } |
Format-Table -AutoSize | Out-String
## Sends some before and after info for ticketing purposes
Hostname ; Get-Date | Select-Object DateTime
Write-Host "Before: $Before"
Write-Host "After: $After"
Write-Verbose ( Get-ChildItem -Path C:\* -Include *.iso, *.vhd, *.vhdx -Recurse -ErrorAction SilentlyContinue |
Sort Length -Descending | Select-Object Name, Directory,
#{Name = "Size (GB)"; Expression = { "{0:N2}" -f ($_.Length / 1GB) } } | Format-Table |
Out-String )
## Completed Successfully!
Stop-Transcript
You want to look into Redirection: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_redirection?view=powershell-7.1
It's useful for logging your Catch output, wrapping it in ${} and specifying your stream output. Ex: ${ Write-Verbose ... } 4>&1 3>&1 2>&1 >> $logFile
Most of what you have though looks like you're trying to log data for reference. Like your Before:After & final Get-ChildItem statements. If you're doing it as you go, you can pipe to Out-File -Append to a log file. But since you have it in a block towards the end, you can simply wrap & redirect:
&{
Write-Host "Before: " $Before
Write-Host "After: " $After"
Get-ChildItem -Path C:\* -Include *.iso, *.vhd, *.vhdx -Recurse -ErrorAction SilentlyContinue |
Sort Length -Descending | Select-Object Name, Directory,
#{Name = "Size (GB)"; Expression = { "{0:N2}" -f ($_.Length / 1GB) } } | Format-Table |
Out-String
} *> C:\FilePath\File.txt
Notice you don't even need to wrap your Get-ChildItem in a Write-Verbose statement. Get cmdlets that spit out to console, will just write the output to the file when using redirection. The only time you should need to issue a Write statement is if you are adding a string of text / interpolating data.
On an unrelated note, I see a DRY opportunity. Your before & after statements are identical. Put a function towards the top of your file that returns the statement, that way you can just assign your $Before & $After vars to the return output.
Okay i am not a programmer and my Powershell experience is basic. But here goes. I have been asked to collect some info on a Directory we are migrating off our network.
It collects sub dirs names, size, #of files and folders and datestamp and exports to csv.
I cannot for the life of me make the folder creation date work so i gave up on that and have been looking to get the lastwritetime for the folders as i am trying to figure out what has been used recently. It only works for a few folders but the rest in excel have system.object[] in the cell. Super frustrating.
Here is the code. It uses a gui directory picker.
#Refresh network drives for session
net use i: /delete
net use m: /delete
net use i: "\\wfs.queensu.ca\ADV\Workgroups"
net use m: "\\wfs.queensu.ca\ADVMedia"
Function Get-Folder($initialDirectory)
{
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
$foldername = New-Object System.Windows.Forms.FolderBrowserDialog
$foldername.Description = "Select a folder"
$foldername.rootfolder = "MyComputer"
if($foldername.ShowDialog() -eq "OK")
{
$folder += $foldername.SelectedPath
}
return $folder
}
$forDir = Get-Folder
#Change this to the parent directory that you want counts for
#$forDir = "\\wfs.queensu.ca\adv\workgroups\ADV Services\$seldir"
$Dirs = Get-ChildItem $forDir -Directory -Name
$Tab = [char]9
$results = #()
Write-Host $forDir
foreach($Dir in $Dirs)
{
$dirSize = "{0:N2} MB" -f ((Get-ChildItem $forDir/$Dir -Recurse | Measure-Object -Property Length
-Sum -ErrorAction Stop).Sum / 1MB)
$dirFiles = Get-ChildItem $forDir/$Dir -Recurse -File | Measure-Object | %{$_.Count}
$dirFolders = Get-ChildItem $forDir/$Dir -Recurse -Directory | Measure-Object | %{$_.Count}
#$dirDate = (Get-ChildItem $forDir/$Dir).LastWriteTime.ToString
$dirDate = #(Get-ChildItem $forDir/$Dir | % {$_.LastWriteTime})
$details = [ordered] #{
dir = $Dir
No_Files = $dirFiles
No_Folders = $dirFolders
size = $dirSize
date = $dirDate
}
$results += New-Object PSobject -Property $details
}
#This line finds the last index of the slash and adding one char
$Dirlength = $forDir.LastIndexOf('\') + 1
#This line takes the entire length of the string minus the postion above leaving the directory name
$sublength = $forDir.length - $Dirlength
#Assigns the remaining characters from the substring to the varibale to be used as the filename
$DirName = $forDir.SubString($Dirlength, $sublength)
$results | Export-Csv "C:\$DirName.csv" -NoTypeInformation
Write-Host ("Complete WOW!")
Get-ChildItem .\dir gives you all files contained in the directory .\dir not the directory itself.
That is why the following line in your script creates an array of LastWriteTimes for all files that are contained in the directory that $forDir/$Dir resolves to in your foreach loop:
$dirDate = #(Get-ChildItem $forDir/$Dir | % {$_.LastWriteTime})
The array in $dirDate will return System.Object[] when its toString() method is called. This is the reason, why you see this string in your excel, where you expect the folder's timestamp.
I bet that those folders, that seem to work do have exactly one childitem...
To get the LastWriteTime of the directory itself use Get-Item instead of Get-ChildItem.
$dirDate = Get-Item $forDir/$Dir | Select-Object -Expand LastWriteTime
try this...
Get-ChildItem -Path 'D:\temp' -Recurse |
Where-Object { $_.PSIsContainer } |
Select-Object -Property Name, LastWriteTime
<#
# Results
Name LastWriteTime
---- -------------
est 17-Feb-20 15:50:53
LogFiles 11-Mar-20 11:37:28
NewFolder 06-Feb-20 14:56:48
ParentFolder 12-Feb-20 14:24:25
Reference 03-Feb-20 11:55:47
Source 06-Feb-20 14:56:48
Target 24-Feb-20 22:03:56
New folder 03-Feb-20 11:55:24
temp 20-Jan-20 11:17:42
ChildFolder 12-Feb-20 14:08:11
GrandchildFolder 12-Feb-20 14:08:32
#>
# Or in v3 and beyond
Get-ChildItem -Path 'D:\temp' -Directory -Recurse |
Select-Object -Property Name, LastWriteTime
<#
# Results
Name LastWriteTime
---- -------------
est 17-Feb-20 15:50:53
LogFiles 11-Mar-20 11:37:28
NewFolder 06-Feb-20 14:56:48
ParentFolder 12-Feb-20 14:24:25
Reference 03-Feb-20 11:55:47
Source 06-Feb-20 14:56:48
Target 24-Feb-20 22:03:56
New folder 03-Feb-20 11:55:24
temp 20-Jan-20 11:17:42
ChildFolder 12-Feb-20 14:08:11
GrandchildFolder 12-Feb-20 14:08:32
#>
I know this question has already been answered, but for completeness, here's another way of doing this by utilizing the GetFileSystemInfos method every DirInfo object has.
$rootFolder = 'X:\YourRootPath'
Get-ChildItem -Path $rootFolder -Directory -Recurse | ForEach-Object {
# GetFileSystemInfos() (needs .NET 4+) is faster than Get-ChildItem and returns hidden objects by default
# See: https://devblogs.microsoft.com/powershell/why-is-get-childitem-so-slow/
$fsObjects = $_.GetFileSystemInfos('*', 'TopDirectoryOnly') # TopDirectoryOnly --> do not recurse
# you can also use Get-ChildItem here of course.
# To also get hidden files, with Get-ChildItem you need to add the -Force switch
# $fsObjects = Get-ChildItem -Path $_.FullName -Filter '*' -Force
# from the $fsObjects array, filter out the files and directories in order to get the count
$files = $fsObjects | Where-Object { $_ -is [System.IO.FileInfo] } # or: !($_.Attributes -band 'Directory')
$folders = $fsObjects | Where-Object { $_ -is [System.IO.DirectoryInfo] } # or: $_.Attributes -band 'Directory'
# emit a PSObject with all properties you wish to collect
[PsCustomObject]#{
Path = $_.FullName
FileCount = $files.Count
DirCount = $folders.Count
DirSize = "{0:N2} MB" -f (($files | Measure-Object -Sum -Property Length).Sum / 1MB)
DirDate = $_.LastWriteTime
}
} | Export-Csv -Path "X:\YourFolder_Info.csv" -NoTypeInformation -UseCulture
Afternoon All,
I need to run a search across all of our servers.
I have the list of servers in a text document and a list of keywords in another
$Servers = get-content -path 'C:\support\Server Search\Server Test.txt'
$Keywords = get-content -path "C:\Support\Server Search\Keyword Test.txt"
Foreach ($Server in $Servers){
Foreach ($Keyword in $Keywords){
Get-ChildItem "$Server" -Recurse | Where-Object {$_.Name -like "$Keyword"}
$i++
Write-Host "$found: $i - Current $ $_"
New-Object -TypeName PSCustomObject -Property #{
Directory = $_.Directory
Name = $_.Name
Length = $_.Length /1024
CreationTime = $_.CreationTime
LastWriteTime = $_.LastWriteTime
LastAccessTime = $_.LastAccessTime}|
select Directory,Name,Length,CreationTime,LastWriteTime,LastAccessTime |
Export-Csv "C:\support\server search\$Server.csv" -Append -NoTypeInformation
}}
$i = 0
Is there a way to indicate when a Keyword has been located and total keywords found? I feel like I need to change this Line but I cannot fathom what I would actually put, I've tried $Keywords but that just changes keyword everytime the directory changes
$i++
Write-Host "$found: $i - Current $ $_"
I'm assuming your $server is set up something like "\\servername\c$\"
when a Keyword has been located and total keywords found:
$Servers = get-content -path 'C:\support\Server Search\Server Test.txt'
$Keywords = get-content -path "C:\Support\Server Search\Keyword Test.txt"
$num = 0 #Total Keyword files Found
Foreach ($Server in $Servers){
Foreach ($Keyword in $Keywords){
#Keyword found Check
$Found = Get-ChildItem -Path "$Server" -Recurse -Include "$Keyword"
if($Found){
Foreach($File in $Found){
$num++ #increment num of keyword files found by 1
Write-Host "found: $num - $File"
New-Object -TypeName PSCustomObject -Property #{
Directory = $File.Directory
Name = $File.Name
Length = $File.Length /1024
CreationTime = $File.CreationTime
LastWriteTime = $File.LastWriteTime
LastAccessTime = $File.LastAccessTime}|
select Directory,Name,Length,CreationTime,LastWriteTime,LastAccessTime |
Export-Csv "C:\support\server search\$Server.csv" -Append -NoTypeInformation
}
}
}
}
Please let me know if this helps you progress. I can assist further if requested.
I need a powershell script, which will go through all users in system and will find total size of all files which any user own... I have script which is going through all users, but then I've no idea to continue with counting total size which user owns for each user
Here is a script, which I`ve now:
$users = Get-WmiObject -class Win32_UserAccount
foreach($user in $users) {
$name = $user.Name
$fullName = $user.FullName;
if(Test-Path "C:\Users\$name") {
$path = "C:\Users\$name"
} else {
$path = "C:\Users\Public"
}
$dirSize = (Get-ChildItem $path -recurse | Measure-Object -property length -sum)
"{0:N2}" -f ($dirSize.sum / 1Gb) + " Gb"
echo "$dirSize"
Add-Content -path "pathototxt..." -value "$name $fullName $path"
}
I would be more than happy If somebody know the answer and tell me it...
Thank you
If there's a lot of files, you might want to consider:
$oSIDs = #{}
get-childitem <filespec> |
foreach {
$oSID = $_.GetAccessControl().Sddl -replace '^o:(.+?).:.+','$1'
$oSIDs[$oSID] += $_.length
}
Then resolve the SIDs when you're done. Parsing the owner SID or well-know security principal ID from the SDDL string saves the provider from having to do a lot of repetitive name resolution to give you back the "friendly" names.
I have no idea what you're asking for here.
"to continue with counting total size which user owns for each user". huh? Do want to check every file on the system or just the userfolder as you currently do?
Your script works fine if you just tweak it to include the filesize in the output. Personally I'd consider using a csv to store this because not all users will have e.g. a full name(admin, guest etc.). Also, atm. your script is counting the public folder multiple times(each time a user doesn't have a profile). E.g. admin(if it has never logged in), guest etc. might both get it specified.
Updated script that outputs both textfile and csv
$users = Get-WmiObject -class Win32_UserAccount
$out = #()
#If you want to append to a csv-file, replace the $out line above with the one below
#$out = Import-Csv "file.csv"
foreach($user in $users) {
$name = $user.Name
$fullName = $user.FullName;
if(Test-Path "C:\Users\$name") {
$path = "C:\Users\$name"
} else {
$path = "C:\Users\Public"
}
$dirSize = (Get-ChildItem $path -Recurse -ErrorAction SilentlyContinue | ? { !$_.PSIsContainer } | Measure-Object -Property Length -Sum)
$size = "{0:N2}" -f ($dirSize.Sum / 1Gb) + " Gb"
#Saving as textfile
#Add-Content -path "pathototxt..." -value "$name $fullName $path $size"
Add-Content -path "file.txt" -value "$name $fullName $path $size"
#CSV-way
$o = New-Object psobject -Property #{
Name = $name
FullName = $fullName
Path = $path
Size = $size
}
$out += $o
}
#Exporting to csv format
$out | Export-Csv "file.csv" -NoTypeInformation
EDIT: Another solution using the answer provided by #mjolinor and #C.B. modified to scan your c:\ drive while excluding some "rootfolders" like "program files", "windows" etc. It exports the result to a csv file ready for Excel.:
$oSIDs = #{}
$exclude = #("Program Files", "Program Files (x86)", "Windows", "Perflogs");
Get-ChildItem C:\ | ? { $exclude -notcontains $_.Name } | % { Get-ChildItem $_.FullName -Recurse -ErrorAction SilentlyContinue | ? { !$_.PSIsContainer } } | % {
$oSID = $_.GetAccessControl().Sddl -replace '^o:(.+?).:.+','$1'
$oSIDs[$oSID] += $_.Length
}
$out = #()
$oSIDs.GetEnumerator() | % {
$user = (New-Object System.Security.Principal.SecurityIdentifier($_.Key)).Translate([System.Security.Principal.NTAccount]).Value
$out += New-Object psobject -Property #{
User = if($user) { $user } else { $_.Key }
"Size(GB)" = $oSIDs[$_.Key]/1GB
}
}
$out | Export-Csv file.csv -NoTypeInformation -Delimiter ";"