Problem :
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.)
Solution :
(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 (DocumentsWindowsPowershell)
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("$ProfileDirntfs.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:tempmyfile.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.
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")
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
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.
Note that this doesn’t return the “size on disk” that Windows Explorer displays, esp. for small files.
The (almost) correct way to get that information is described at Getting “size on disk” for small files in Powershell
$s=(compact /q C:whatever.dat | where-object {$_.contains(‘total bytes’)}).split()};$s[8].padleft(20)+$s[0].padleft(20)