GetFileSize と GetCompressedFileSize が違い始めるのはいつの日か。(NTFS 圧縮の話。)

ちなみにこれってよく考えたら psutil なくても ctypes だけでイケたよなぁと今更。

イキナリ「ちなみに」から始めてしまって、大変まことに恐縮至極。でもリンクした先とは別に無関係なわけではないよ、もちろん。


2021-04-16 23:50追記: 「ctypes でもイケたよなぁ」を言いっぱなしなのもなんなので:

 1 Python 3.9.2 (tags/v3.9.2:1a79785, Feb 19 2021, 13:44:55) [MSC v.1928 64 bit (AMD64)] on win32
 2 Type "help", "copyright", "credits" or "license" for more information.
 3 >>> import ctypes
 4 >>> f = ctypes.windll.kernel32.GetDiskFreeSpaceExW
 5 >>> fba = ctypes.c_longlong()
 6 >>> tit = ctypes.c_longlong()
 7 >>> fre = ctypes.c_longlong()
 8 >>> f("h:/", ctypes.byref(fba), ctypes.byref(tit), ctypes.byref(fre))
 9 1
10 >>> fba
11 c_longlong(13319245824)
12 >>> fba.value
13 13319245824
14 >>> tit.value
15 62075678720
16 >>> fre.value
17 13319245824
18 >>> fre.value/1024**3
19 12.40451431274414
20 >>> # 
21 >>> # 「python 3.3+ なら shutil でもいい」も一応
22 >>> import shutil
23 >>> shutil.disk_usage("h:/")
24 usage(total=62075678720, used=48758779904, free=13316898816)

目下、二つ困ったことに直面していて。

ひとつは 265-nize 内で触れた「disk_usage_graph.py」が、一定時間で「Fail to allocate bitmap」で落っこちるようになってしまったこと。tk が出してるということは(tk86t.dll に対する strings で)確認したのだが、措置が全然わからない。しかも、先日までちっとも起こらず、昨日から突然起こるようになったんで、余計にわからない。

で、ふたつ目というのがひとつ目と直結していて。たとえば:

1 #! /bin/sh
2 # h:/ は NTFS フォーマットした USB メモリで、h:/Videos は「圧縮してディスクを節約する」状態
3 # にしてある。h:/Videos/input.mkv は H.265 でエンコードしてあるビデオ。
4 tmpd=/tmp/cnv$$
5 mv -fv "h:/Videos/input.mkv" /tmp/${tmpd}
6 ffmpeg -y "${tmpd}/input.mkv" -vf fps=23 -c:v libx265 -c:a copy h:/Videos/input.mkv

みたいにして、「フレームレートを落とすことでビデオファイルのサイズを小さくしたい」なんてことをして、「ぐへへ、小さくなっててるぜ」と喜びたいとして。「NTFS 圧縮」を使っていると、この変換スクリプトの「変換中と変換後すぐ」は、「変換前よりも(小さくなっていても)ディスクサイズを余計に使用する」ように見えてしまうわけなのよ、変換元が圧縮されてるのに、変換中と変換後すぐの状態は未圧縮だからね。disk_usage_graph.py が何日間でも動かし続けることが出来るなら「いずれ圧縮されて結局は使用量を減らせたのだ」と喜べるんだけど、その前に死んでしまうのね、disk_usage_graph.py。

ではそもそも、「圧縮してディスクを節約する」は、いったいぜんたい、どういう振る舞いをしてるんだろうか、と。監視出来ない? これの監視さえ出来れば、リアルタイムで「わーおあめーじんぐ、へったへった」と喜べるんだけれど?

この機能を担ってるのが Windows のデバイスドライバなのか、カーネルなのかなんなのかはわからんのだけど、「誰も内容を更新しないタイミングを見計らってから裏でこっそり圧縮開始」としてるだけなわけだから、何かそうしたスケジューリングに関するドキュメントってないのかなー、って少し探しては見たんだけれど、検索キーワードがよくわからんので、見つかる気がしなかった。ので、うん、そう、ここで見出しにした「GetFileSize と GetCompressedFileSize が違い始める」タイミング、これがわかればいいんじゃなかろうかと。ffmpeg で変換を始める前のサイズからはじめて、NTFS による圧縮が終わるタイミングで終えれば、「きゃー、こんなにサイズを減らせたよー、いいねあげるっ」と叫べろう:

getcompressedfilesize_expr.py
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 import sys
 4 import os
 5 import time
 6 import ctypes
 7 from datetime import datetime
 8 
 9 
10 def _getsizes(fn):
11     # 「GetFileSize」や「GetFileEx」を使う必要はない。python であれば os.stat
12     # が返すファイルサイズがそれだから。python にとって未知なのは「compressed」
13     # のほうで、こちらは kernel32 API に依存するしかない。
14     hi = ctypes.c_ulong()
15     lo = ctypes.windll.kernel32.GetCompressedFileSizeW(fn, ctypes.byref(hi))
16     return os.stat(fn).st_size, lo + (hi.value << 16)
17     # 2021-04-20 17:40追記:
18     #     圧縮後が大きい場合(か?) GetCompressedFileSizeW のほうがおかしい。
19     #     目下原因究明(措置検討)中...
20 
21 
22 if __name__ == '__main__':
23     fn = sys.argv[1]
24     sz_ini = _getsizes(fn)
25     print(" " * 10, datetime.now(), fn, sz_ini, sep=", ")
26     try:
27         while True:
28             sz = _getsizes(fn)
29             print(" " * 10, datetime.now(), fn, sz, sep=", ", end="\r", flush=True)
30             if sz[0] == sz_ini[0] and sz != sz_ini:
31                 print("", flush=True)
32                 sz_ini = sz
33             time.sleep(1)
34     except KeyboardInterrupt:
35         print("\n'd mornin...")

実際一つを監視してみているのだけれど、変換が終わっても全然圧縮し始めないのね。少なくとも今みてるやつは、(15:30 に変換が終了して)30分たっても圧縮開始してない。フォルダ単位で決めてる可能性もある? 色々観察しないとわからなそうだね。というかうーん、最悪なのが、「アクセスがあれば圧縮開始をキャンセル」としてるとしてその「アクセス」が「GetCompressedFileSize呼び出し」も含んでいる場合。この場合って、こうやって監視し続けるんではなくて、確認のインターバルを長くするしかないよなぁ…。

ひとまずしばらく放置して観察し続けてみる。今「input.mkv」相当のものがある同じフォルダにまだ処理対象のビデオがいて、そいつらの処理が終わらないと圧縮が始まらない可能性もあって、まぁ気長に待つしかないかなぁと諦めてる最中。


で。17:40。15:30 に監視対象にしてるビデオの変換が終了後およそ2時間10分、まったく動かずで、17:40 に、監視対象ファイルが含まれるフォルダ全体に対する変換処理を終えた。として。まだ変化なし…。うーん、いつまで待てば始まるであろうか? 21個のうちの18個目だから17個目までの圧縮が終わったら始まる、とか? てことは:

getcompressedfilesize_expr2.py
 1 # -*- coding: utf-8 -*-
 2 import sys
 3 import os
 4 import time
 5 import ctypes
 6 from glob import glob
 7 from datetime import datetime
 8 import curses
 9 
10 
11 def _getsizes(fn):
12     hi = ctypes.c_ulong()
13     lo = ctypes.windll.kernel32.GetCompressedFileSizeW(fn, ctypes.byref(hi))
14     try:
15         return os.stat(fn).st_size, lo + (hi.value << 16)
16     except Exception:
17         # lost file, maybe.
18         return -1, -1
19     # 2021-04-20 17:40追記:
20     #     圧縮後が大きい場合(か?) GetCompressedFileSizeW のほうがおかしい。
21     #     目下原因究明(措置検討)中...
22 
23 
24 def _main(stdscr):
25     stdscr.timeout(0)
26     curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
27     curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE)
28     # 単体のファイルではなくて、フォルダ全体をば
29     files = list(
30         filter(
31             lambda fn: os.path.isfile(fn),
32             glob(os.path.join(sys.argv[1], "*"))))
33     sz_ini = [_getsizes(fn) for fn in files]
34     cnt = 0
35     sz_last = sz_ini
36     while True:
37         kc = stdscr.getch()
38         if kc in (ord('q'), ord('Q')):
39             break
40         stdscr.clear()
41         if cnt > 60:
42             sz = [_getsizes(fn) for fn in files]
43             sz_last = sz
44             cnt = 0
45         else:
46             sz = sz_last
47         cnt += 1
48         for i, fn in enumerate(files):
49             bn = os.path.basename(fn)
50             col = 0
51             for c in bn:
52                 stdscr.addstr(i, col, c)
53                 col += 2 if len(c.encode("utf-8")) > 1 else 1
54             stdscr.addstr(i, 50, "{:3,d}".format(sz_ini[i][0]).rjust(18))
55             
56             stdscr.addstr(i, 70, "{:3,d}".format(sz[i][0]).rjust(18),
57                           curses.color_pair(1 if sz_ini[i][0] != sz[i][0] else 2))
58             
59             stdscr.addstr(i, 90, "{:3,d}".format(sz[i][1]).rjust(18),
60                           curses.color_pair(
61                               1 if sz[i][0] == sz[i][1] or sz[i][1] < 0 else 3))
62         stdscr.refresh()
63         time.sleep(0.5)
64 
65 
66 if __name__ == '__main__':
67     curses.wrapper(_main)

ひとまず、最初に監視してたファイルはまだなんだけど、ほかのやつらは順不同で圧縮済みなんだよね。確かに一つ一つが数百Mなので、時間がかかるのは理解出来るけれど、こうやって実際やってみると、ほんと思わぬ時間がかかってるんだな。まぁそりゃそーか。

今時点でまだ「監視してることを理由に圧縮しようとしない」疑いが晴れてなくて、そうなると「FindFirstChangeNotification」とかでフォルダ変更通知にだけ反応するようにする、とかまでやらんといけないかもしれなくて、それを恐れている。が、とりあえず待ってみる…。


何時間後だ? 17:40…、からの、今 22:00。上で相手にしてたヤツはまだ圧縮されてない。ただ、頭切り替えて別のフォルダ相手に処理するのを上のスクリプトで監視してたら、最初の対象ファイルの変換終了後4分くらいで圧縮されたみたい。

てわけで、「監視してることを理由に圧縮しようとしない」ではないことは確定して一安心。

二つ考えられる。

一つは NTFS 圧縮のなにがしかプロセッサが圧縮を行うスケジューリングが、エンドユーザ目線でみれば「気まぐれである」てこと。まぁ膨大なファイル群があるシステム全体の中での一つが、いつ圧縮されるか、だからなぁ、何時間経っても対象にならない、なんてのはありうるのかも、とは思う。

もう一つの説の方がもっともらしいかも。compressed としてのサイズとそうでないサイズの実際のところの比較をしてみればわかる通り、NTFS 圧縮って、「非常に圧縮率が低い(悪い)」のね。今みてるやつだと、「310,748,866」というサイズが compressed で「310,747,136」、つまり 1,730、そう、たかだか 1~2K くらいしか減ってないわけね。それと、とても小さいファイルに関しては、そもそも圧縮処理の対象になってないようにも思える。10K 未満のサイズのファイルが軒並み圧縮されていないっぽい。つまり、考えられる説は「圧縮の効果がないと判断されて、圧縮されてない」もしくは「圧縮してみたが同じサイズになっちゃったー」。これはありうるね。

どちらの説だろうとまぁいいのだが、今新たに疑問に感じ始めてることがあって、困りごとが増えてしまってて弱ってる。

そもそもの発端の「23fps にして小さくしたはずなのに、「変換前よりも(小さくなっていても)ディスクサイズを余計に使用する」ように見えてしまう」の「見かけ上増えて見えるサイズ」が、感覚的に「NTFS 圧縮がいったん未圧縮になることで増えるサイズ」だけで説明出来てないような気がしてて。disk_usage の監視で増えるサイズって、「たかだか 1~2K」ではなかった気がするんだよね。となるとこれって、「何か隠しファイル的な一時ファイル」の存在でも仮定しないと説明できない、てことにならんけ? と。けれども ADS もなさげだし、ほんとに隠しファイルがあるようには思えなくて。

てわけで、今かなり悶々としてる。たぶん今絶賛更新中のこの USB メモリの更新を止めてから数日後に観察すれば、これは「確かに 23fps にしたのでちっさくなった、やったねっ!」と喜べるんであろうとは思うんだけどね…。とりあえず、本日のところは深追いはここまでにしとく。


翌日追記:
単体のファイルに関しての「GetFileSize と GetCompressedFileSize が違い始める」件に関しては、昨日と言えることは変わらない。つまり「4分くらい放置で圧縮開始されたり、何十時間経っても圧縮されなかったりと色々」「NTFS圧縮の効果は極めて小さい」。一応「絶賛更新中だろうと圧縮しようとはしてるみたい」て知識が増えたけれども。(それと、「NTFS圧縮の効果は極めて小さい」について、元ファイルが 300MB くらいの場合に「1~2K」は小嘘だった。ものによっては 100~200K 程度減らせる場合もあるみたい。まぁ圧縮率は内容依存なので「そりゃそーだ」とは思うけれど。)

問題の、「df で取得できるディスク使用量(つまり GetDiskFreeSpaceEx で取得可能な値)の見かけ上の増分」が「NTFS 圧縮がかかってるかかかってないかだけでは説明出来ない気がする」に関してなんだけれど、やはり「気がする」ではなくて、確定だ、これは。

たとえば、300MB くらいずつの10個のビデオがあって、これを fps=23 にダウングレードしてサイズ圧縮すると、実サイズが 250MB くらいずつになる。つまり、「50*10 = 500MB 減ったぁぐへへ」となるとハッピーなのに、なぜかむしろ増えて「3GB ぶんだけ、より圧迫するようになった」みたいなくらいだったのよ。すなわちこれ、この例だと「この 3.5GB はどこにあるんだ?」てことになる。なんでだぁ、と思ってたんだけど、ふと、同じドライブ h: にある別の「いらなくなったビデオ(300MB)」を消したら、なぜか df での報告が一気に変化して、「どこにいったかわからなかった 3.5 GB」がどこかへ霧散してしまった。(要は「300MBを消したらなぜか未使用領域が 4GB 増えた」てこと。)

てことは…。

結局本当のところはマイクロソフト自身による説明や誰かによる内部解析資料などによる説明がない限りは真相はわからないけれど、「おかしいのは GetDiskFreeSpaceEx での報告サイズのほう」である可能性が出てきた、てこと。エンドユーザにはみえない内部データが圧迫していた可能性も皆無ではないけれど、たとえばディスクサイズの報告が「非リアルタイム」で行われているものなのだとしたら、今みたいな不可解なことも理解できないこともなくて。今の場合、ワタシの「いらなくなったビデオ(300MB)削除」がトリガーとなって再計算が行われた…、とか? あるいは、トリガーなんかなくて、単に再計算の時間を偶然目撃しただけ、て可能性もあるか。

うん、まぁよくわからん、てことではあるんだけれど、以後これに類することをする際は、「何か監視対象以外のファイルを移動したり削除したりしてみる」といいのかもしれない、と思った。


翌日追記 (2):
「何か監視対象以外のファイルを移動したり削除したりしてみる」はどうやら正解らしい。フォルダ監視スクリプトを以下のようにしてみた:

getcompressedfilesize_expr3.py
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 import sys
 4 import os
 5 import time
 6 import ctypes
 7 from glob import glob
 8 from datetime import datetime
 9 import curses
10 
11 
12 def _getsizes(fn):
13     hi = ctypes.c_ulong()
14     lo = ctypes.windll.kernel32.GetCompressedFileSizeW(fn, ctypes.byref(hi))
15     try:
16         return os.stat(fn).st_size, lo + (hi.value << 16)
17     except Exception:
18         # lost file, maybe.
19         return -1, -1
20 
21 
22 def _diskusage_free(p):
23     import psutil
24     u = psutil.disk_usage(p)
25     return ("{:.1f}".format(u.free / 1024**3) + " G").rjust(18)
26 
27 
28 def _files(dir):
29     # 単体のファイルではなくて、フォルダ全体をば
30     files = list(
31         filter(
32             lambda fn: os.path.isfile(fn),
33             glob(os.path.join(dir, "*"))))
34     return files
35 
36 
37 def _main(stdscr):
38     stdscr.timeout(0)
39     curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
40     curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE)
41     files = _files(sys.argv[1])
42     sz_ini = {fn: _getsizes(fn) for fn in files}
43     dt_ini = datetime.now()
44     dt = dt_ini
45     cnt = 0
46     sz_last = sz_ini
47     duf_ini = _diskusage_free(sys.argv[1])
48     duf = duf_ini
49     while True:
50         kc = stdscr.getch()
51         if kc in (ord('q'), ord('Q')):
52             break
53         stdscr.clear()
54         if cnt > 60:
55             files = _files(sys.argv[1])
56             sz = {fn: _getsizes(fn) for fn in files}
57             sz_last = sz
58             dt = datetime.now()
59             duf = _diskusage_free(sys.argv[1])
60             cnt = 0
61         else:
62             sz = sz_last
63         cnt += 1
64         stdscr.addstr(0, 0, dt_ini.strftime("%m-%d %H:%M:%S").rjust(18))
65         stdscr.addstr(0, 18, dt.strftime("%m-%d %H:%M:%S").rjust(18))
66         stdscr.addstr(1, 0, duf_ini)
67         stdscr.addstr(1, 18, duf)
68         for i, fn in enumerate(files):
69             v_ini = sz_ini.get(fn, (-1, -1))
70             v_now = sz.get(fn, (-1, -1))
71             bn = os.path.basename(fn)
72             stdscr.addstr(i + 2, 0, "{:3,d}".format(v_ini[0]).rjust(18))
73             stdscr.addstr(i + 2, 18, "{:3,d}".format(v_now[0]).rjust(18),
74                           curses.color_pair(1 if v_ini[0] != v_now[0] else 2))
75             stdscr.addstr(i + 2, 36, "{:3,d}".format(v_now[1]).rjust(18),
76                           curses.color_pair(
77                               1 if v_now[0] == v_now[1] or v_now[1] < 0 else 3))
78             col = 56
79             for c in bn:
80                 stdscr.addstr(i + 2, col, c)
81                 col += 2 if len(c.encode("utf-8")) > 1 else 1
82         stdscr.refresh()
83         time.sleep(0.5)
84 
85 
86 if __name__ == '__main__':
87     curses.wrapper(_main)

これで GetFileSize と GetCompressedFileSize と GetDiskFreeSpaceEx 全部を一気に眺めていられる、てわけなんだけれど、監視中のドライブ内にある小さなファイルを「コピーして即削除」したら、どうやらすぐに GetDiskFreeSpaceEx が返す値が変化した。11.2 GB、と報告されていた直後に 12.2 GB に増えたが、ffmpeg による変換真っ最中でありディスク使用量が激変するはずのないタイミングだったので、やはり「削除に反応してアップデートされた」と解釈して良いように思える。

PC 全体としての応答性を担保するためには、ファイルアクセスのたびにあらゆる情報を更新するわけにはいかず、ときどき情報更新する、てことなんだろう、とは思う。そして、ファイルを削除することは、情報更新のトリガーになりうる、てことかなぁと。まぁ「たぶん」としか言えないよ、たった二回の観察結果に過ぎないから。


2021-04-18追記:
フォルダ監視スクリプト、更新:

getcompressedfilesize_expr4.py
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 import sys
 4 import os
 5 import time
 6 import ctypes
 7 from glob import glob
 8 from datetime import datetime
 9 import curses
10 
11 
12 def _getsizes(fn):
13     hi = ctypes.c_ulong()
14     lo = ctypes.windll.kernel32.GetCompressedFileSizeW(fn, ctypes.byref(hi))
15     try:
16         return os.stat(fn).st_size, lo + (hi.value << 16)
17     except Exception:
18         # lost file, maybe.
19         return -1, -1
20 
21 
22 def _diskusage_free(p):
23     import psutil
24     u = psutil.disk_usage(p)
25     return ("{:.1f}".format(u.free / 1024**3) + " G").rjust(18)
26 
27 
28 def _files(dir):
29     # 単体のファイルではなくて、フォルダ全体をば
30     files = list(
31         filter(
32             lambda fn: os.path.isfile(fn),
33             glob(os.path.join(dir, "*"))))
34     return files
35 
36 
37 def _main(stdscr):
38     stdscr.timeout(0)
39     curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
40     curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE)
41     files = _files(sys.argv[1])
42     sz_ini = {fn: _getsizes(fn) for fn in files}
43     dt_ini = datetime.now()
44     dt = dt_ini
45     cnt = 0
46     sz_last = sz_ini
47     duf_ini = _diskusage_free(sys.argv[1])
48     duf = duf_ini
49     height, width = stdscr.getmaxyx()
50     pagesize = min(height - 3, len(files))
51     sl = slice(0, pagesize)
52     while True:
53         kc = stdscr.getch()
54         if kc in (ord('q'), ord('Q')):
55             break
56         elif kc == ord(' '):
57             sl = slice(
58                 min(sl.start + pagesize, len(files)),
59                 min(len(files), sl.stop + pagesize))
60             if sl.start == sl.stop:
61                 sl = slice(0, pagesize)
62         stdscr.clear()
63         if cnt > 60:
64             files = _files(sys.argv[1])
65             sz = {fn: _getsizes(fn) for fn in files}
66             sz_last = sz
67             dt = datetime.now()
68             duf = _diskusage_free(sys.argv[1])
69             cnt = 0
70         else:
71             sz = sz_last
72         cnt += 1
73         stdscr.addstr(0, 0, dt_ini.strftime("%m-%d %H:%M:%S").rjust(18))
74         stdscr.addstr(0, 18, dt.strftime("%m-%d %H:%M:%S").rjust(18))
75         stdscr.addstr(1, 0, duf_ini)
76         stdscr.addstr(1, 18, duf)
77         for i, fn in enumerate(files[sl]):
78             v_ini = sz_ini.get(fn, (-1, -1))
79             v_now = sz.get(fn, (-1, -1))
80             bn = os.path.basename(fn)
81             stdscr.addstr(i + 2, 0, "{:3,d}".format(v_ini[0]).rjust(18))
82             stdscr.addstr(i + 2, 18, "{:3,d}".format(v_now[0]).rjust(18),
83                           curses.color_pair(1 if v_ini[0] != v_now[0] else 2))
84             stdscr.addstr(i + 2, 36, "{:3,d}".format(v_now[1]).rjust(18),
85                           curses.color_pair(
86                               1 if v_now[0] == v_now[1] or v_now[1] < 0 else 3))
87             col = 56
88             for c in bn:
89                 stdscr.addstr(i + 2, col, c)
90                 col += 2 if len(c.encode("utf-8")) > 1 else 1
91         if sl.stop < len(files):
92             stdscr.addstr(height - 1, 0, "-- more --")
93         stdscr.refresh()
94         time.sleep(0.5)
95 
96 
97 if __name__ == '__main__':
98     curses.wrapper(_main)

最初からわかってて放置してた問題を措置しただけ。curses の addstr はスクリーン領域溢れで何も詳細を伝えずに「ただ死ぬ」。わかってたよ。ワタシのニーズでとりあえずは問題なかっただけ。で、今領域溢れるのに巡りあっちゃったので、「ページング」出来るようにしただけ。スペースキーでページ送り出来る。

「まだわかってて放置」してるよ。分かる人はわかると思うんだけれど、「スクリーン領域溢れ」のうち、措置したのは行方向についてのみ。桁方向の措置はしてない。ま、必要になったらワタシの自分のは直すけど、ここに改めて上げ直すかどうかは気分次第。見出しネタとしての本題ではないからね、これ。


2021-04-18追記 (2):
フォルダ監視スクリプト、更新、なのだが、例によって「結構日常使い出来るでな」につき gist にて:


2021-04-19追記:
MSYS の df 出力も同時にみてると、どうにも値がワタシのスクリプトで出してるのと違ってて、何か違うことしてんのかなぁ、って思って、調べてはみた。みた、んだけれども:

msysCORE-1.0.19-1-msys-1.0.19-src/source/winsup/cygwin/syscalls.cc
1833 extern "C" int
1834 statfs (const char *fname, struct statfs *sfs)
1835 {
1836   TRACE_IN;
1837   sigframe thisframe (mainthread);
1838   if (!sfs)
1839     {
1840       set_errno (EFAULT);
1841       return -1;
1842     }
1843 
1844   path_conv full_path (fname, PC_SYM_FOLLOW | PC_FULL);
1845   char *root = rootdir (full_path);
1846 
1847   syscall_printf ("statfs %s", root);
1848 
1849   DWORD spc, bps, freec, totalc;
1850 
1851   if (!GetDiskFreeSpace (root, &spc, &bps, &freec, &totalc))
1852     {
1853       __seterrno ();
1854       return -1;
1855     }
1856 
1857   _ULARGE_INTEGER fba, tnb, tfa;
1858 
1859   if (!GetDiskFreeSpaceEx (root, &fba, &tnb, &tfa))
1860     {
1861       fba.QuadPart = (unsigned long long)freec * spc * bps;
1862       tnb.QuadPart = (unsigned long long)totalc * spc * bps;
1863       tfa.QuadPart = (unsigned long long)freec * spc * bps;
1864     }
1865 
1866   DWORD vsn, maxlen, flags;
1867 
1868   if (!GetVolumeInformation (root, NULL, 0, &vsn, &maxlen, &flags, NULL, 0))
1869     {
1870       __seterrno ();
1871       return -1;
1872     }
1873   sfs->f_type = flags;
1874   sfs->f_bsize = spc*bps;
1875   sfs->f_blocks = tnb.QuadPart / sfs->f_bsize;
1876   sfs->f_bavail = fba.QuadPart / sfs->f_bsize;
1877   sfs->f_bfree = tfa.QuadPart / sfs->f_bsize;
1878   sfs->f_files = -1;
1879   sfs->f_ffree = -1;
1880   sfs->f_fsid = vsn;
1881   sfs->f_namelen = maxlen;
1882   return 0;
1883 }

本質的に違ってるのは GetDiskFreeSpace を呼び出してるかどうかだけだと思うのよね。もとは GetDiskFreeSpace のみだったのを、あとから「2GB 超えファイル対応」として GetDiskFreeSpaceEx も呼び出すようにした、という経緯らしいけれど、結果的には GetDiskFreeSpaceEx 呼び出しが失敗しない限りは、ワタシのスクリプトと同じになる、はず、なんだけどなぁ…。うーん、これについてはよくわからん。