Skip to content

Commit 214a61a

Browse files
chenquanclaude
andauthored
fix: report allocated disk usage for sparse files (#182) (#191)
diskusage reported each file's apparent/logical size via fs.FileInfo.Size(). For sparse files such as Docker Desktop's Docker.raw that is the max-allocatable limit (~1 TB) rather than the bytes actually on disk (~30 GB), causing diskusage to disagree wildly with `du` on the same path. Use allocated blocks (st_blocks * 512) on linux/darwin and GetCompressedFileSizeW on Windows, matching `du`'s default behavior. For non-sparse files the result is unchanged (block-rounding only). Fixes #182 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9616d6a commit 214a61a

4 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎internal/stat.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func find(dir string, filter func(info fs.FileInfo) bool) ([]*file, error) {
286286
if !entry.IsDir() {
287287
fileChan <- &file{
288288
name: entry.Name(),
289-
size: fileInfo.Size(),
289+
size: diskSize(fileInfo, filepath.Join(dir, entry.Name())),
290290
}
291291
continue
292292
}

‎internal/sys_darwin.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616

1717
package internal
1818

19+
import (
20+
"os"
21+
"syscall"
22+
)
23+
1924
func sysFilter(dir string) bool {
2025
return "/dev" != dir
2126
}
27+
28+
// diskSize returns the actual number of bytes allocated on disk for the file,
29+
// i.e. allocated blocks (st_blocks * 512), matching `du`'s default behavior.
30+
// For sparse files this is smaller than the apparent logical size.
31+
func diskSize(info os.FileInfo, _ string) int64 {
32+
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
33+
return stat.Blocks * 512 // st_blocks is always in 512-byte units (POSIX)
34+
}
35+
return info.Size()
36+
}

‎internal/sys_linux.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616

1717
package internal
1818

19+
import (
20+
"os"
21+
"syscall"
22+
)
23+
1924
func sysFilter(dir string) bool {
2025
return dir != "/proc"
2126
}
27+
28+
// diskSize returns the actual number of bytes allocated on disk for the file,
29+
// i.e. allocated blocks (st_blocks * 512), matching `du`'s default behavior.
30+
// For sparse files this is smaller than the apparent logical size.
31+
func diskSize(info os.FileInfo, _ string) int64 {
32+
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
33+
return stat.Blocks * 512 // st_blocks is always in 512-byte units (POSIX)
34+
}
35+
return info.Size()
36+
}

‎internal/sys_windows.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,39 @@
1616

1717
package internal
1818

19+
import (
20+
"os"
21+
"syscall"
22+
"unsafe"
23+
)
24+
25+
var (
26+
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
27+
procGetCompressedFileSizeW = modKernel32.NewProc("GetCompressedFileSizeW")
28+
)
29+
1930
func sysFilter(_ string) bool {
2031
return true
2132
}
33+
34+
// diskSize returns the actual number of bytes allocated on disk for the file
35+
// via GetCompressedFileSizeW, matching `du`'s default behavior. For sparse or
36+
// compressed files this excludes unallocated holes, so it is smaller than the
37+
// apparent logical size returned by os.FileInfo.Size().
38+
func diskSize(info os.FileInfo, name string) int64 {
39+
p, err := syscall.UTF16PtrFromString(name)
40+
if err != nil {
41+
return info.Size()
42+
}
43+
44+
var high uint32
45+
low, _, _ := procGetCompressedFileSizeW.Call(
46+
uintptr(unsafe.Pointer(p)),
47+
uintptr(unsafe.Pointer(&high)),
48+
)
49+
if uint32(low) == 0xFFFFFFFF { // INVALID_FILE_SIZE → call failed
50+
return info.Size()
51+
}
52+
53+
return int64(uint64(high)<<32 | uint64(uint32(low)))
54+
}

0 commit comments

Comments
 (0)