views:

2884

answers:

6

Hello:

I am writing an administrative script, and I need to calculate the size of files on disk.

These files are on a compressed NTFS volume.

I can't use FileInfo.Length, because that is file size and not size on disk. For example, if I have a 100MB file, but it is only using 25MB due to NTFS compression, I need my script to return 25MB.

Is there a way to do this in Powershell?

(I know about the GetCompressedFileSize() Win32 call, but I was hoping that this is already wrappered at some level.)

A: 

Parse the output of compact.exe.

ssg
+6  A: 

Load up the Managed Windows API (http://mwinapi.sourceforge.net/) and check out the ExtendedFileInfo class. There is a method GetPhysicalFileSize() that will return the size a file requires on disk.

public static ulong GetPhysicalFileSize(string filename)

Aternatively, you could compile your own DLL and load the assembly for that one function:

using System;
using System.Runtime.InteropServices;

namespace NTFS {
  public class ExtendedFileInfo
  {
    [DllImport("kernel32.dll", SetLastError=true, EntryPoint="GetCompressedFileSize")]
    static extern uint GetCompressedFileSizeAPI(string lpFileName, out uint lpFileSizeHigh);
    public static ulong GetCompressedFileSize(string filename)
    {
      uint high;
      uint low;
      low = GetCompressedFileSizeAPI(filename, out high);
      int error = Marshal.GetLastWin32Error();
      if (high == 0 && low == 0xFFFFFFFF && error != 0)
      {
        throw new System.ComponentModel.Win32Exception(error);
      }
      else
      {
        return ((ulong)high << 32) + low;
      }
    }
  }
}

Then to compile:

csc /target:library /out:ntfs.extendedfileinfo.dll ntfs.extendedfileinfo.cs

And finally, to load and run in PowerShell:

PS C:\> [System.Reflection.Assembly]::LoadFile("C:\ntfs.extendedfileinfo.dll")
PS C:\> [NTFS.ExtendedFileInfo]::GetCompressedFileSize("C:\sample.txt")
Goyuix
+2  A: 

If you can't find managed API you like, in PowerShell V2 it's much easier to P/Invoke a Win32 API. Read PowerShell P/Invoke Walkthrough for instructions.

Jay Bazuzi
+6  A: 

(edit)

I figured out how to dynamically add a property (called a "script property") to the Fileobject, so now, I can use the syntax: $theFileObject.CompressedSize to read the size.

(end of edit)

Read Goyuix's response, and I thought "Cool, but isn't there some kind of type-extension capability in Powershell?". So then I found this Scott Hanselman post: http://www.hanselman.com/blog/MakingJunctionsReparsePointsVisibleInPowerShell.aspx

And I created a Script Property for the FileInfo object: CompressedSize.

Here's what I did: (note: I'm quite new to Powershell, or at least I don't use it much. This could probably be made a lot better, but here's what I did:

First, I compiled the Ntfs.ExtendedFileInfo from Goyuix's post. I put the DLL in my Powershell profile directory (Documents\WindowsPowershell)

Next, I created a file in my profile directory named My.Types.ps1xml.

I put the following XML into the file:

<Types>
<Type>
    <Name>System.IO.FileInfo</Name>
    <Members>
        <ScriptProperty>
          <Name>CompressedSize</Name>
          <GetScriptBlock>
            [Ntfs.ExtendedFileInfo]::GetCompressedFileSize($this.FullName)
          </GetScriptBlock>
        </ScriptProperty>
    </Members>
</Type>
</Types>

That code (once merged into the type system) will dynamically add a property named CompressedSize to the FileInfo objects that are returned by get-childitem/dir. But Powershell doesn't know about the code yet, and it doesn't know about my DLL yet. We handle that in the next step:

Edit Profile.ps1. in the same directory. Now, my Profile file happens to already have some stuff in it because I have the Community Extensions for powershell installed. Hopefully, I'm including everything that you need in this next code snippet, so that it will work even on a machine that does not have the extensions. Add the following code to Profile.ps1:

#This will load the ExtendedfileInfo assembly to enable the GetCompressedFileSize method.  this method is used by the
#PSCompressedSize Script Property attached to the FileInfo object.
$null = [System.Reflection.Assembly]::LoadFile("$ProfileDir\ntfs.extendedfileinfo.dll") 

#merge in my extended types
$profileTypes = $ProfileDir | join-path -childpath "My.Types.ps1xml"
Update-TypeData $profileTypes

Now, the $ProfileDir variable that I reference is defined earlier in my Profile.ps1 script. Just in case it's not in yours, here is the definition:

$ProfileDir = split-path $MyInvocation.MyCommand.Path -Parent

That's it. The next time that you run Powershell, you can access the CompressedSize property on the FileInfo object just as though it is any other property. Example:

$myFile = dir c:\temp\myfile.txt

$myFile.CompressedSize

This works (on my machine, anyway), but I would love to hear whether it fits in with best practices. One thing I know I'm doing wrong: in the Profile.ps1 file, I return the results of LoadFile into a variable that I'm not going to use ($null = blah blah). I did that to suppress the display of result of load file to the console. There is probably a better way to do it.

JMarsch
+4  A: 

Easy to do using V2 Add-Type and Pinvoke.NET:

add-type -type  @'
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace Win32Functions
{
    public class ExtendedFileInfo
    {
        [DllImport("kernel32.dll", SetLastError=true, EntryPoint="GetCompressedFileSize")]
        static extern uint GetCompressedFileSizeAPI(string lpFileName, out uint lpFileSizeHigh);

        public static ulong GetCompressedFileSize(string filename)
        {
            uint high;
            uint low;
            low = GetCompressedFileSizeAPI(filename, out high);
            int error = Marshal.GetLastWin32Error();
            if (high == 0 && low == 0xFFFFFFFF && error != 0)
            throw new Win32Exception(error);
            else
            return ((ulong)high << 32) + low;
        }
    }
}
'@

[Win32Functions.ExtendedFileInfo]::GetCompressedFileSize( "C:\autoexec.bat")

Experiment! Enjoy! Engage!

Jeffrey Snover [MSFT] Windows Management Partner Architect Visit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShell Visit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

Jeffrey Snover - MSFT
A: 

$s=(compact /q C:\whatever.dat | where-object {$_.contains('total bytes')}).split()};$s[8].padleft(20)+$s[0].padleft(20)