How to Access Windows API Functions with PowerShell


So you want to access Windows API functions in PowerShell?  P/Invoke is how you do it.  The Add-Type cmdlet, which is available in PowerShell 2.0 or above, is used in the following example to retrieve the SizeOnDisk of files on an NTFS file system.

A project at a client site is to move several terabytes of data from an old storage system to a new NAS system. There are several hundred thousand files that have been damaged by an archive system that misbehaved so that the data clusters have been deleted but the file name is still in the Master File Table in the file system.  In Windows Explorer these damaged files can be seen but they cannot be opened or copied.  Robocopy is hanging when it encounters these files while trying to copy them to the new NAS system.  The script listed below was used to identify these damaged files and then another script was used to delete these bad files so the migration to the new NAS system could be completed.

The P/Invoke is first defined in the following script to establish the access to the Windows API that will retrieve the actual size of the file on disk.  PowerShell compiles the source code of the P/Invoke into a real .NET class when this script is executed.  The second part of the script uses the P/Invoke to identify the damaged files.
PowerShell Script:
# Define the P/Invoke using the Add-Type cmdlet
# GetDiskFreeSpace is the function that is being defined.  See execution example below

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

namespace Win32Functions
{
  public class ExtendedFileInfo
  {   
    public static long GetFileSizeOnDisk(string file)
    {
        FileInfo info = new FileInfo(file);
        uint dummy, sectorsPerCluster, bytesPerSector;
        int result = GetDiskFreeSpaceW(info.Directory.Root.FullName, out sectorsPerCluster, out bytesPerSector, out dummy, out dummy);
        if (result == 0) throw new Win32Exception();
        uint clusterSize = sectorsPerCluster * bytesPerSector;
        uint hosize;
        uint losize = GetCompressedFileSizeW(file, out hosize);
        long size;
        size = (long)hosize << 32 | losize;
        return ((size + clusterSize - 1) / clusterSize) * clusterSize;
    }

    [DllImport("kernel32.dll")]
    static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
       [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh);

    [DllImport("kernel32.dll", SetLastError = true, PreserveSig = true)]
    static extern int GetDiskFreeSpaceW([In, MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName,
       out uint lpSectorsPerCluster, out uint lpBytesPerSector, out uint lpNumberOfFreeClusters,
       out uint lpTotalNumberOfClusters); 
  }
}
'@
 

# To use the P/Invoke that was created above use the Get-ChildItem command with the
# –Recurse option to traverse the file system.  Skip directories with the !$_.PSIsContainer 

# confitional (we only want to look at files not directories)
# The file size on disk is returned with the following:
# $SizeOnDisk = [Win32Functions.ExtendedFileInfo]::GetFileSizeOnDisk( $_.FullName )
# Damaged files are identified as having a Size On Disk = 0 and a file length not equal 0

Get-ChildItem O:\ -Recurse | foreach-object {
   if(!$_.PSIsContainer){
      $SizeOnDisk = [Win32Functions.ExtendedFileInfo]::GetFileSizeOnDisk( $_.FullName )
      if( $SizeOnDisk -eq 0 -and $_.length -ne 0 ){
         write-output “This is a bad file $_.FullName”
      }
   }
}

 
If you are interested in more information about P/Invoke visit www.pinvoke.net.