Windows XP でファイルの更新日付やサイズが欲しい場合は syscall.GetFileInformationByHandle() を使おう

はじめに

ひょんなことから Windows XP で ファイルの更新日付やサイズを取得して一覧で表示しないといけないミッションがありました。Windows 10 でそのようなプログラムをGoで書いていたので、それを使えばXPでも動くだろうと高をくくっていたら、File.Stat() 関数内でRuntimeエラーが発生して落ちてしまう事態に。調べてみると、File.Stat() 内で windows.GetFileInformationByHandleEx() を使用していて、それが XPではサポートされていない関数だったことがわかりました。

go/types_windows.go at master · golang/go · GitHub

もう少し調べてみると、windows.GetFileInformationByHandleEx() 関数で取得した値では、ファイルの更新日付やサイズを取得しておらず、その前段の windows.GetFileInfomationByHandle() 関数で取得しているようでした。

go/types_windows.go at master · golang/go · GitHub

今回はファイルの更新日付とサイズが取得できればいいので、File.Stat() を使うのをやめて、自前で windows.GetFileInformationByHandle() 関数を使って情報を取得するようにしました。

コード

ModTIme()Size() のメソッドを https://github.com/golang/go/blob/master/src/os/types_windows.go#L136, https://github.com/golang/go/blob/master/src/os/types_windows.go#L108 から拝借して、tinyStat() として取得できるようにしました。

package main

import (
    "fmt"
    "os"
    "syscall"
    "time"
)

type fileStat struct {
    LastWriteTime syscall.Filetime
    FileSizeHigh  uint32
    FileSizeLow   uint32
}

func (fs *fileStat) ModTime() time.Time {
    return time.Unix(0, fs.LastWriteTime.Nanoseconds())
}

func (fs *fileStat) Size() int64 {
    return int64(fs.FileSizeHigh)<<32 + int64(fs.FileSizeLow)
}

func tinyStat(filename string) (*fileStat, error) {
    fp, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    var d syscall.ByHandleFileInformation
    h := syscall.Handle(fp.Fd())
    err = syscall.GetFileInformationByHandle(h, &d)
    if err != nil {
        return nil, err
    }
    return &fileStat{
        LastWriteTime: d.LastWriteTime,
        FileSizeHigh:  d.FileSizeHigh,
        FileSizeLow:   d.FileSizeLow,
    }, nil
}

func main() {
    fs, err := tinyStat("main.go")
    if err != nil {
        panic(err)
    }

    fmt.Printf("ModTime %v, Size %v bytes\n", fs.ModTime().Format("2006-01-02 03:04:05"), fs.Size())
}

まとめ

ファイルの更新日付とサイズをWindows XPでも取得できるようにしました。 FileInfo インターフェースをすべて実装すれば、よりシームレスに使えるのではないかと思いますが 今回はそこまでのものは必要なかったので、これで十分でした。