How do I query "Size on disk" file information?

As others have said, you need to use GetFileInformationByHandleEx, but it looks like you need to use FILE_STANDARD_INFO or FILE_ID_BOTH_DIR_INFO. The information you want is returned in the AllocationSize member of each, but the second is for directory handles, to list files within instead of the directory itself (note: not recursive, just top-level). To make it easier, FILE_STANDARD_INFO has a Directory boolean, so call it first if you're not sure. According to the documentation for FILE_ID_BOTH_DIR_INFO,

AllocationSize Contains the value that specifies how much space is allocated for the file, in bytes. This value is usually a multiple of the sector or cluster size of the underlying physical device.

This seems to give you the Size on Disk information.

I haven't found a Delphi translation of the FILE_ID_BOTH_DIR_INFO structure. The difficulty would seem to be the final member, WCHAR FileName[1], which is described as:

FileName[1]
Contains the first character of the file name string. This is followed in memory by the remainder of the string.

I'm not sure how this would be handled in Delphi.


Raymond Chen's article on Windows Confidential describes how that value is calculated. The most pertinent paragraph states:

The Size on disk measurement is more complicated. If the drive supports compression (as reported by the FILE_FILE_COMPRESSION flag returned by the Get­Volume­Information function) and the file is compressed or sparse (FILE_ATTRIBUTE_COMPRESSED, FILE_ATTRIBUTE_SPARSE_FILE), then the Size on disk for a file is the value reported by the Get­Compressed­File­Size function. This reports the compressed size of the file (if compressed) or the size of the file minus the parts that were de-committed and logically treated as zero (if sparse). If the file is neither compressed nor sparse, then the Size on disk is the file size reported by the Find­First­File function rounded up to the nearest cluster.


Since GetCompressedFileSize will return the actual size for both normal/compressed/spares files of any volume type, you can rely on this function to return the File Size on Disk (Windows Explorer is displaying this value as factor of volume Cluster Size), and get the File Size by using GetFileSize function.

From MSDN docs about GetCompressedFileSize:

If the file is not located on a volume that supports compression or sparse files, or if the file is not compressed or a sparse file, the value obtained is the actual file size, the same as the value returned by a call to GetFileSize.

So the logic is described by the following code (tested on Windows XP with FAT32/FAT/CDfs files):

procedure FileSizeEx(const FileName: string; out Size, SizeOnDisk: UINT);
var
  Drive: string;
  FileHandle: THandle;
  SectorsPerCluster,
  BytesPerSector,
  Dummy: DWORD;
  ClusterSize: DWORD;
  SizeHigh, SizeLow: DWORD;
begin
  Assert(FileExists(FileName));
  Drive := IncludeTrailingPathDelimiter(ExtractFileDrive(FileName));
  if not GetDiskFreeSpace(PChar(Drive), SectorsPerCluster, BytesPerSector, Dummy, Dummy) then
    RaiseLastOSError;

  ClusterSize := SectorsPerCluster * BytesPerSector;

  FileHandle := CreateFile(PChar(FileName), 0, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
    nil, OPEN_EXISTING, 0, 0);
  if (FileHandle = INVALID_HANDLE_VALUE) then
    RaiseLastOSError;
  try
    SizeLow := Windows.GetFileSize(FileHandle, @SizeHigh);
    if (GetLastError <> NO_ERROR) and (SizeLow = INVALID_FILE_SIZE) then
      RaiseLastOSError;
    Size := UINT(SizeHigh shl 32 or SizeLow);
  finally
    if (FileHandle <> INVALID_HANDLE_VALUE) then
      CloseHandle(FileHandle);
  end;

  SizeLow := GetCompressedFileSize(PChar(FileName), @SizeHigh);
  if (GetLastError <> NO_ERROR) and (SizeLow = INVALID_FILE_SIZE) then
    RaiseLastOSError;

  SizeOnDisk := UINT(SizeHigh shl 32 or SizeLow);
  if (SizeOnDisk mod ClusterSize) > 0 then
    SizeOnDisk := SizeOnDisk + ClusterSize - (SizeOnDisk mod ClusterSize);
end;

We could check the Get­Volume­Information for compression/sparse support, and then GetFileAttributes to test for FILE_ATTRIBUTE_COMPRESSED or FILE_ATTRIBUTE_SPARSE_FILE, BUT since the GetCompressedFileSize does that internally for us (by Calling NtQueryInformationFile), I see no point in these tests.