7

I want to reproduce behaviour exhibited in the Windows Explorer -> Properties dialog -> General property page for any given file. Specifically I want to reproduce the exact value of the "Size on disk" field.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
OnTheFly
  • 2,059
  • 5
  • 26
  • 61
  • 4
    Can you expand on what you mean when you say that `GetCompressedFileSize` is not the right function? – David Heffernan Mar 28 '12 at 08:28
  • @DavidHeffernan, i meant to advise an experts against stepping into the same seemingly obvious pit after me... – OnTheFly Mar 28 '12 at 09:10
  • Are you sure it doesn't work. Perhaps you just failed to combine the low and high DWORDs correctly. – David Heffernan Mar 28 '12 at 09:20
  • And this question should be closed because ______? – OnTheFly Mar 28 '12 at 11:08
  • 3
    I voted to close because I'm missing the question here. Instead you just post many information about how you figured out what's unusual with some file and how big is your cluster size. And still there is no info, what's wrong with the `GetCompressedFileSize`, or what you expect to get from such function (the best would be an example like I have a 0B file, in Explorer I can see 0B as file size on disk, but `GetCompressedFileSize` returns something else). – TLama Mar 28 '12 at 11:46
  • @TLama, ah, its you :-) Please focus on the question itself instead of jumping on the GetCompressedFileSize wagon. – OnTheFly Mar 28 '12 at 12:16
  • 2
    I Have to agree with @TLama. *What is the question*? (in plain English please) – kobik Mar 28 '12 at 12:32
  • @kobik, suppose there are no hint about GetCompressedFileSize. What is your move? – OnTheFly Mar 28 '12 at 12:42
  • Maybe this is a possible duplicate? http://stackoverflow.com/questions/3750590/c-sharp-get-file-size-on-disk – kobik Mar 28 '12 at 13:32
  • 2
    Question seems to have changed. Could you explain what you have tried and how it fails. – David Heffernan Mar 28 '12 at 13:45
  • +1. I think the change is good. Everyone was getting distracted by the stuff about how to get the compressed size, trying to solve the implicit problem of why `GetCompressedFileSize` supposedly wasn't working, when in fact it doesn't matter whether it works since it's the wrong function to call to get the desired information. – Rob Kennedy Mar 28 '12 at 14:52

5 Answers5

3

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.

Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
Ken White
  • 123,280
  • 14
  • 225
  • 444
  • Actually, no. There's a note that says it's available on XP, in the Header File section: WinBase.h (include Windows.h); FileExtd.h on Windows Server 2003 and **Windows XP**, with a library to link, so you could create a wrapper DLL in C++Builder and then call it via that wrapper. – Ken White Mar 28 '12 at 17:40
  • Or indeed any C or C++ compiler, e.g. the MS compilers distributed in the Platform SDK – David Heffernan Mar 28 '12 at 17:48
  • The FileName[1] thing is a variable length struct. You have to allocate the memory for the struct on the heap, once you know how long the file name is. – David Heffernan Mar 28 '12 at 17:58
3

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.

Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
3

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.

kobik
  • 21,001
  • 4
  • 61
  • 121
2

You may use the GetFileInformationByHandleEx function to obtain FILE_COMPRESSION_INFO structure, its CompressedFileSize field is the value you need (same as returned by GetCompressedFileSize).

Adriano Repetti
  • 65,416
  • 20
  • 137
  • 208
  • 2
    Although be warned that using this on XP is non-trivial – David Heffernan Mar 28 '12 at 08:53
  • Now i'm completely puzzled. FILE_COMPRESSION_INFO returned from successful call is filled with all zeros. However, i finally figured out whats unusual with offending file, updating the question. – OnTheFly Mar 28 '12 at 09:21
  • @user539484: what if you pass FILE_STANDARD_INFO and then read the AllocationSize? When that fails, try FILE_STREAM_INFO and read the StreamAllocationSize. Only works on Vista and higher. – The_Fox Mar 28 '12 at 13:23
  • Zero? Did you call _GetFileInformationByHandleEx_ with argument *FileCompressionInfo* for *FILE_INFO_BY_HANDLE_CLASS*? **UPDATE**: Original question changed a little bit too mich over time...what do you want to know/find? – Adriano Repetti Mar 29 '12 at 18:24
0

Posting a routine according to David's extract from Raymond's article. Feel free to improve it !

uses
  System.SysUtils, Windows;

function GetClusterSize(Drive: String): integer;
var
  SectorsPerCluster, BytesPerSector, dummy: Cardinal;
begin
  SectorsPerCluster := 0;
  BytesPerSector := 0;
  GetDiskFreeSpace(PChar(Drive), SectorsPerCluster, BytesPerSector, dummy, dummy);

  Result := SectorsPerCluster * BytesPerSector;
end;

function FindSizeOnDisk(Drive: String; AFilename: string): Int64;
var
  VolumeSerialNumber: DWORD;
  MaximumComponentLength: DWORD;
  FileSystemFlags: DWORD;
  HighSize: DWORD;
  FRec: TSearchRec;
  AClusterSize: integer;
  AFileSize, n: Int64;
begin
  Result := 0;
  Drive := IncludeTrailingPathDelimiter(ExtractFileDrive(Drive));
  GetVolumeInformation( PChar(Drive), nil, 0, @VolumeSerialNumber,
    MaximumComponentLength, FileSystemFlags, nil, 0);
  if ((FileSystemFlags AND FILE_FILE_COMPRESSION) <> 0) AND
    ((FileSystemFlags AND (FILE_VOLUME_IS_COMPRESSED OR
    FILE_SUPPORTS_SPARSE_FILES)) <> 0) then
  begin // Compressed or Sparse disk
    Result := GetCompressedFileSize(PChar(AFilename), @HighSize);
    // Not sure if this is correct on a sparse disk ??
  end
  else
  begin
    if (System.SysUtils.FindFirst(AFilename, faAnyFile, FRec) = 0) then
    begin
      AFileSize := FRec.Size;
      AClusterSize := GetClusterSize(Drive);
      n := AFileSize mod AClusterSize;
      if n > 0 then // Round up to nearest cluster size
        Result := AFileSize + (AClusterSize - n)
      else
        Result := AFileSize;
      System.SysUtils.FindClose(FRec);
    end;
  end;
end;
LU RD
  • 34,438
  • 5
  • 88
  • 296