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.