• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 2011
  • Last Modified:

Using PowerShell to Total My Documents for ALL Domain Usres

Ok,

I'd like to use powershell to scan ALL of the workstations in my domain and total up the amount of space both the My Documents and Desktop folders user up.

I can provide a scipt a list of machines to check, but then I'd need to have it figure out all of the users on each machine and provide a total (probably in gigabytes).

This is what I have so far:

$path = [environment]::GetFolderPath([environment+SpecialFolder]::MyDocuments)

 $totalSize = Get-ChildItem -path $path -recurse -errorAction "SilentlyContinue" |
 Measure-Object -property length -sum

$path2 = [environment]::GetFolderPath([environment+SpecialFolder]::Desktop)

 $totalSize2 = Get-ChildItem -path $path2 -recurse -errorAction "SilentlyContinue" |
 Measure-Object -property length -sum

 IF(($totalSize.Sum+$totalSIze2.Sum) -ge 1GB)
   {
      "{0:n2}" -f  (($totalSize.Sum+$totalSIze2.Sum) / 1GB) + " GigaBytes"
   }
 ELSEIF($totalSize2.sum -ge 1MB)
    {
      "{0:n2}" -f  (($totalSize.Sum+$totalSIze2.Sum) / 1MB) + " MegaBytes"
    }
 ELSE
    {
      "{0:n2}" -f  (($totalSize.Sum+$totalSIze2.Sum) / 1KB) + " KiloBytes"

Anyone have a script that does what I want?  ...or can you suggest modificaitons to read a file, scan a remote computer, and perform the above for all users on that computer?
0
gerhardub
Asked:
gerhardub
  • 9
  • 8
1 Solution
 
Chris DentPowerShell DeveloperCommented:

Bit of a multi-stage one this, while I figure out how to reliably get from one item to the next :)

Profiles on a machine, we can discover those with:

Get-WMIObject Win32_UserProfile -Filter "Special='$False'"

That gets the profile but not the folder paths, they may or may not be under the "LocalPath" property returned by that query.

/me goes away to think

Chris
0
 
Chris DentPowerShell DeveloperCommented:
Hmm the next step is actually quite hard.

If everything is straight-forward we can assume that "Desktop" and "My Documents" appear directly beneath the path returned above.

The trouble comes when that isn't the case, then we're in a bit of a mess. This value:

[environment]::GetFolderPath([environment+SpecialFolder]::MyDocuments)

Ultimately you get that from HKEY_CURRENT_USER. When we're asking for all users we're no longer looking at the current user, HKEY_USERS is more appropriate. The trouble with that is you'll only see registry hives which have been loaded; users that have logged on since the last reboot.

Having PowerShell load the hive is extremely unlikely to happen at this stage (as far as I've seen), too many issues with permissions before we touch on trying to wrap up an API so it can be used in a .NET language (like PowerShell).

In short... are you quite happy to play guessing games with the paths for My Documents and Desktop given that we have the path to the profile?

Chris
0
 
gerhardubAuthor Commented:
Go ideas.

Let's make the assumption that all of the computers that we are going to check are Window XP.

(So that means c:\Documents and Settings\<User Profile>)

So yeah, the guessing game part is fine!
0
Problems using Powershell and Active Directory?

Managing Active Directory does not always have to be complicated.  If you are spending more time trying instead of doing, then it's time to look at something else. For nearly 20 years, AD admins around the world have used one tool for day-to-day AD management: Hyena. Discover why

 
gerhardubAuthor Commented:
(I ment to say good ideas!)

FYI:

I see this as something like:

1) Go to the computers in the domain, probably by providing a computer.txt document with all of the computers you want to scan.

2) Find the user profiles listed under C:\Document and Settings

3) Look at the total size of My Documents and Desktop

4) Write the agreegate total for all users on all workstations to the screen or in a file.
0
 
Chris DentPowerShell DeveloperCommented:
Okie dokie. Lets give this a whirl then.

Only tested in PowerShell 2, gave up on 1 quite a while ago.

I've tried to account for the different in "My Documents" between XP /2003 and Vista / 7 / 2008.

Output is to the screen at the moment, but we can redirect to lots of things, using any of these:

Out-GridView
Export-CSV
Out-File
ConvertTo-Html
etc

Chris
# Read the import file
Get-Content SomeFile.txt | %{
  # Store computer name in a named variable for access later
  $ComputerName = $_
  # Get the operating system version
  $Version = (Get-WMIObject Win32_OperatingSystem -Computer $ComputerName).Version

  # Get all configured user profiles (ignore special accounts)
  Get-WMIObject Win32_UserProfile -Computer $ComputerName -Filter "Special='$False'" | %{

    # Convert the local path to a UNC path
    $UNCPath = "\\$ComputerName\$($_.LocalPath -Replace ':\\', '$\')"

    # Initialise the size values
    $DesktopSize = 0; $MyDocumentsSize = 0
    # Attempt to get the desktop size
    If (Test-Path "$UNCPath\Desktop")
    {
      $DesktopSize = (Get-ChildItem "$UNCPath\Desktop\" -Recurse | Measure-Object Length -Sum).Sum
    }

    # Check version then get the My Documents size
    If ($Version -Like "6*")
    {
      If (Test-Path "$UNCPath\Documents")
      {
        $MyDocumentsSize = (Get-ChildItem "$UNCPath\Documents\" -Recurse | Measure-Object Length -Sum).Sum
      }
    }
    Else
    {
      If (Test-Path "$UNCPath\My Documents")
      {
        $MyDocumentsSize = (Get-ChildItem "$UNCPath\My Documents\" -Recurse | Measure-Object Length -Sum).Sum
      }
    }

    # Leave this object in the output pipeline
    $_ | Select-Object `
      @{n='ComputerName';e={ $ComputerName }},
      LocalPath, SID,
      @{n='AccountName';e={ 
        $Account = [WMI]"Win32_SID.SID='$($_.SID)'"
        If ($Account.AccountName -ne $Null) {
          "$($Account.ReferencedDomainName)\$($Account.AccountName)"
        }
      }},
      @{n='LastUseTime';e={ [Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) }},
      @{n='TotalSize';e={ $DesktopSize + $MyDocumentsSize }},
      @{n='DesktopSize';e={ $DesktopSize }},
      @{n='MyDocumentsSize';e={ $MyDocumentsSize }}
  }
}

Open in new window

0
 
gerhardubAuthor Commented:
Sweeeet!  

I just played with this, and the only part I'm having a little issue with is the 'Leave this object in the output pipepline" section.

Seems to be giving me an editor error.  Humph.  (Back to you shortly.)

GB
0
 
Chris DentPowerShell DeveloperCommented:
It may be an inconsistency between PS versions.

If it's trying to parse it for PS1 you'll need a ` at the end of each of the property lines, otherwise it won't drop down and continue it as a single command. An example of that below.

Bit of a complex section though, I'd hate to say it was entirely down to that.

Chris
# Leave this object in the output pipeline
    $_ | Select-Object `
      @{n='ComputerName';e={ $ComputerName }}, `
      LocalPath, SID, `
      @{n='AccountName';e={ 
        $Account = [WMI]"Win32_SID.SID='$($_.SID)'"
        If ($Account.AccountName -ne $Null) {
          "$($Account.ReferencedDomainName)\$($Account.AccountName)"
        }
      }}, `
      @{n='LastUseTime';e={ [Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) }}, `
      @{n='TotalSize';e={ $DesktopSize + $MyDocumentsSize }}, `
      @{n='DesktopSize';e={ $DesktopSize }}, `
      @{n='MyDocumentsSize';e={ $MyDocumentsSize }}

Open in new window

0
 
gerhardubAuthor Commented:
Friggen back tick!  -grin-  That problem solved, it's a neat solution.

I'm getting a lot of Invalid Class errors from WMI, so I'm trying to track that down:

Get-WmiObject : Invalid class
At C:\Users\<USERID>\Documents\Scripts\CheckMyDocsDesktopFromComputerList.ps1:21 char:16
+   Get-WMIObject <<<<  Win32_UserProfile -Computer $ComputerName -Filter "Special='$False'" | %{
    + CategoryInfo          : InvalidOperation: (:) [Get-WmiObject], ManagementException
    + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

I wonder what makes that an Invalid Operation?
0
 
Chris DentPowerShell DeveloperCommented:

Ah... damn... minimum supported client for Win32_UserProfile is Vista:

http://msdn.microsoft.com/en-us/library/ee886409%28VS.85%29.aspx

Let me see if I can find an alternative that'll run for Windows XP.

Chris
0
 
Chris DentPowerShell DeveloperCommented:

We can pull equivalent data out of HKEY_LOCAL_MACHINE ... ProfileList, I just need to prod the code a bit again.

Chris
0
 
gerhardubAuthor Commented:
Crap!

That's explains SO much.  So I was able to get it to work, by accident, on a bunch of systems.

(It turned out I already had a file named 'computer.txt,' which was populated with servers... so 2003 and some 2008.  Any it failed on the non-2008 servers... -smile-)

Thanks for all of your help, I really appricate this, as, in general, it's been kicking me in the butt.
0
 
Chris DentPowerShell DeveloperCommented:

Okay this script is a tad more complicated now. If you are not running PowerShell 2 I strongly urge you to upgrade (it's worth it), the function I wrote requires that for "Get-Help Get-UserProfile" to work. If you can't at all, yell and I'll strip out everything I know to be PS 2 specific.

Otherwise, it works but...

If you need to authenticate the connection it's going to get a bit messy at the moment.

To be really irritating, my function, Get-UserProfile only half-works for Vista / 7 / 2008 (and I've only tested it against 2003), the newer systems seem to store LastUseDate somewhere else. So the OS test is used, and if it's old it'll use my function, if it's new it'll use Win32_UserProfile.

The function could do with a few more comments, but I must run off home now. I hope it works for you in the meantime :)

Chris
Function Get-UserProfile
{
  <#
    .SYNOPSIS
    Returns information from ProfileList in the Registry to simulate Win32_UserProfile
    .DESCRIPTION
    Get-UserProfile reads the registry on the target system using WMI and returns information from:
    HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\ProfileList
    .PARAMETER ComputerName
    The computer to run this operation against
    .PARAMETER Credential
    Credentials to use for this operation
  #>
  [CmdLetBinding()]
  Param(
    [String]$ComputerName = ".",
    [Management.Automation.PSCredential]$Credential
  )
  
  $ConnectionOptions = New-Object Management.ConnectionOptions
  If ($Credential -ne $Null)
  {
    $ConnectionOptions.Username = $Credential.Username
    $ConnectionOptions.SecurePassword = $Credential.Password
  }
  
  $WMIScope = New-Object Management.ManagementScope("\\$ComputerName\root\default", $ConnectionOptions)
  $WMIPath = New-Object Management.ManagementPath("StdRegProv")
  $Registry = New-Object Management.ManagementClass($WMIScope, $WMIPath, $(New-Object Management.ObjectGetOptions))

  # Enumerate the subkeys of ProfileList
  $InParams = $Registry.GetMethodParameters("EnumKey")
  $InParams["sSubKeyName"] = "Software\Microsoft\Windows NT\CurrentVersion\ProfileList"
  $OutParams = $Registry.InvokeMethod("EnumKey", $InParams, $Null)
  # Loop through subkeys
  $OutParams["sNames"] | %{

    # Input Paramters are the same no matter which Get method is invoked
    $InParams = $Registry.GetMethodParameters("GetStringValue")
    $InParams["sSubKeyName"] = "Software\Microsoft\Windows NT\CurrentVersion\ProfileList\$_"

    $InParams["sValueName"] = "ProfileImagePath"
    $Path = ($Registry.InvokeMethod("GetStringValue", $InParams, $Null))["sValue"]

    $InParams["sValueName"] = "Sid"
    $SIDBytes = ($Registry.InvokeMethod("GetBinaryValue", $InParams, $Null))["uValue"]
    If ($SIDBytes)
    {
      $SID = (New-Object Security.Principal.SecurityIdentifier($SIDBytes, 0)).Value
    }
    Else
    {
      $SID = ""
    }
    
    $InParams["sValueName"] = "ProfileLoadTimeHigh"
    $TimeHigh = ($Registry.InvokeMethod("GetDWORDValue", $InParams, $Null))["uValue"]
    $Inparams["sValueName"] = "ProfileLoadTimeLow"
    $TimeLow = ($Registry.InvokeMethod("GetDWORDValue", $InParams, $Null))["uValue"]
    $LastUseTime = (Get-Date "01/01/1601").AddDays((($TimeHigh * [Math]::Pow(2, 32) + $TimeLow) / 600000000) / 1440)

    "" | Select-Object `
      @{n='LocalPath';e={ $Path }},
      @{n='SID';e={ $SID }}, `
      @{n='LastUseTime';e={ $LastUseTime }}
  }
}

# Read the import file
Get-Content SomeFile.txt | %{
  # Store computer name in a named variable for access later
  $ComputerName = $_
  # Get the operating system version
  $Version = (Get-WMIObject Win32_OperatingSystem -Computer $ComputerName -Credential $c).Version

  # Get all configured user profiles (ignore special accounts)
  If ($Version -Like "6*")
  {
    $Profiles = Get-WMIObject Win32_UserProfile -Computer $ComputerName -Filter "Special='$False'"
  }
  Else
  {
    $Profiles = Get-UserProfile -Computer $ComputerName -Credential $c
  }
  
  $Profiles | %{

    # Convert the local path to a UNC path
    $UNCPath = "\\$ComputerName\$($_.LocalPath -Replace ':\\', '$\')"

    # Initialise the size values
    $DesktopSize = 0; $MyDocumentsSize = 0
    # Attempt to get the desktop size
    If (Test-Path "$UNCPath\Desktop")
    {
      $DesktopSize = (Get-ChildItem "$UNCPath\Desktop\" -Recurse | Measure-Object Length -Sum).Sum
    }

    # Check version then get the My Documents size
    If ($Version -Like "6*")
    {
      If (Test-Path "$UNCPath\Documents")
      {
        $MyDocumentsSize = (Get-ChildItem "$UNCPath\Documents\" -Recurse | Measure-Object Length -Sum).Sum
      }
    }
    Else
    {
      If (Test-Path "$UNCPath\My Documents")
      {
        $MyDocumentsSize = (Get-ChildItem "$UNCPath\My Documents\" -Recurse | Measure-Object Length -Sum).Sum
      }
    }
    
    # Leave this object in the output pipeline
    $_ | Select-Object `
      @{n='ComputerName';e={ $ComputerName }}, `
      LocalPath, SID, `
      @{n='AccountName';e={ 
        $Account = [WMI]"Win32_SID.SID='$($_.SID)'"
        If ($Account.AccountName -ne "") {
          "$($Account.ReferencedDomainName)\$($Account.AccountName)"
        }
      }}, `
      @{n='LastUseTime';e={ If ($_.LastUseTime -is [System.DateTime])
        {
          $_.LastUseTime
        }
        Else
        {
          [Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) 
        } }}, `
      @{n='TotalSize';e={ $DesktopSize + $MyDocumentsSize }}, `
      @{n='DesktopSize';e={ $DesktopSize }}, `
      @{n='MyDocumentsSize';e={ $MyDocumentsSize }}
  }
}

Open in new window

0
 
gerhardubAuthor Commented:
Dude... this is really great!

Now I just have to figure out how to solve the authentication issue so that I don't have to logon 400 times!

Still, it's a great start!

Lemme know if you come up with an authentication solution.  
0
 
gerhardubAuthor Commented:
Excellent solution.

Anyone have any thoughts on how to not have to authenticate for each machine?

GB
0
 
Chris DentPowerShell DeveloperCommented:
The simplest solution would be Run As :)

But if that's no good we can stick credentials in a variable and pass those for the WMI connections.

Putting them into a variable is as simple as adding this to the beginning of the script:

$Credential = Get-Credential

Each of the Get-WMIObject commands, and my Get-UserProfile function, would need that added with the credential parameter:

Get-WMIObject .... -Credential $Credential
Get-UserProfile .... -Credential $Credential

Now all we have to deal with is authenticating against the file system so we can pull the file sizes. Naturally that's harder to do and I'll have to have a think about that one. Worst case scenario will have us temporarily mapping a drive to it using "net use" and taking it from there.

Chris
0
 
gerhardubAuthor Commented:
Chris,

Lemme see if I can ask you another question:

If you where going to output this to a CSV file, maybe one line per workstation checked, how would you do it?

GB
0
 
Chris DentPowerShell DeveloperCommented:

If I were to dump it in it's current state to CSV it's as simple as adding "| Export-CSV" after the very last curly brace. The downside to that is you'll get multiple lines per machine.

Moving everything onto a single line is pretty hard, it's the kind of thing that lends itself better to a hierarchical structure like XML more than CSV. That doesn't mean it's impossible of course, it's just a case of defining exactly what you want to see and making that possible. What format is the most useful to you?

If it were mine I would leave the format as it is, but my queries against the data are quite likely to be within PowerShell, or perhaps SQL if I chuck it into a database. If I wanted to display it I would perhaps be tempted to use ConvertTo-Html, eventually making something similar to the ACL report generated by the script that came out of this thread:

http:Q_25066855.html

Chris
0
 
TimberWestCommented:
Hey Chris-dent

I'm a novice with powershell with that said I tried to run your script and it complained about your SomeFile.txt missing from S:  so after I created the file the script ran fine but that file is blank. I also had it export to a csv file on my desktop but its blank to. Any help would be much appreciated.

Thanks
0

Featured Post

SMB Security Just Got a Layer Stronger

WatchGuard acquires Percipient Networks to extend protection to the DNS layer, further increasing the value of Total Security Suite.  Learn more about what this means for you and how you can improve your security with WatchGuard today!

  • 9
  • 8
Tackle projects and never again get stuck behind a technical roadblock.
Join Now