きどたかのブログ

いつか誰かがこのブログからトラブルを解決しますように。

Go言語でzipファイルを作る、ただしWindowsに限る

Go言語でzipファイルを扱うとき、とりあえず"archive/zip"を使いますよね。

しかし、使ってみて、気付いたことがある。

思っていたzipファイルを作ることができなかった。

ちなみに今は古いgo 1.6.2を使ってます。最新は1.8です。

zipのエントリ(zip.FileHeader)を作る方法が複数あるみたいだ。

  • ファイルパスから作る writer.Create(relativepath)
  • FileInfoから作る zip.FileInfoHeader(fileInfo)
  • 自分で作る &zip.FileHeader{}

ファイルパスから作ると細かいことができない。
FileInfoから作ると、unix形式(rwxrwxrxw)が強制される <- SetModeを呼ばせたくない。
仕方ないから自分で作るよ。

属性

一般的なファイルは"A"が7zで表示されます。エクスプローラーの表示を変えれば属性も出せます。

setModeを呼び出すとホストOSがUnixになり、属性もrwx形式になる。
そのため、それは迂回して、自分でExternalAttrsを設定しないといけない。

とりあえず、何を設定すればどうなるのか手で打ち込んでチェックしてみた。

D  ディレクトリ               <- fh.ExternalAttrs = 16
R  読み取り専用               <- fh.ExternalAttrs =  1
H  隠しファイル               <- fh.ExternalAttrs =  2  
A  アーカイブ                 <- fh.ExternalAttrs = 32
S  システム ファイル          <- fh.ExternalAttrs =  4 
I  非インデックス対象ファイル <- 不明。。。力尽きた。
L  再解析ポイント             <- fh.ExternalAttrs = 1024
-  その属性以外

上記の調査は、だいたいあってると思う。
src/syscall/ztypes_windows.go - The Go Programming Language
https://msdn.microsoft.com/ja-jp/library/windows/desktop/gg258117(v=vs.85).aspx


あ、これでいいのかも。

  fh.ExternalAttrs = data.FileAttributes

すみません、dataっつーのは、更新日のところで説明しますが、
fileInfo.Sys().(*syscall.Win32FileAttributeData).FileAttributesということです。

ファイル所有者

java.nio.file.attributeならファイル所有者の情報を取得できるらしい。
goの場合、データをどこから抜いてくるのか発見できてない。
できるんだろうか??

そして、どうやって詰めるんだっけ・・・。
どうも、zipにするときには詰められないように思える。
7zには所有者情報は見えないし。
とりあえず、zipのためには取得する必要はなさそうだ。

おそらく、これを実現するには、fileの情報をもとに、セキュリティ情報を拾って、
その情報をもとにルックアップするイメージなんだろう。(C++でもJavaでもそんなイメージ)

更新日時、作成日時、アクセス日時

更新日時のみであれば、FileHeader#SetModTime(info.ModTime())で十分です。
作成日時とアクセス日時は、FileHeaderのExtraを設定する必要がある。
作成日時とアクセス日時は、FileInfoのSys()から取得する。
とりあえず以下の記事は参考になるでしょう。

 data := info.Sys().(*syscall.Win32FileAttributeData)

これを使ってExtraに突っ込む。
注意事項はリトルエンディアンであるということだ。
どういう[]byteになればいいのかは適宜バイナリエディタでzipファイルを比較確認するほうがいい。
理屈的な形式は、ここが参考になったので是非ご覧いただきたい。
https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld


トルエンディアンを簡単に扱うためには、
"encoding/binary"パッケージのLittleEndianを使うほうが間違いがないが、
bufの先頭から詰めてくるので、Extraのbyte配列に直接詰め込めない。

  binary.LittleEndian.PutUint32(buf, data)

とりあえず自分はこんな感じで書いた。間違ってたらスマン。
序盤は自前でエンディアンを意識して書いたが、後半はbinaryパッケージを利用する関数を用意した。

  data := info.Sys().(*syscall.Win32FileAttributeData)
  var extra []byte
  extra = append(extra, 0x0a, 0x00)             // NTFS @Short
  extra = append(extra, 0x20, 0x00)             // TSize @Short
  extra = append(extra, 0x00, 0x00, 0x00, 0x00) // Reserved @Long
  extra = append(extra, 0x01, 0x00)             // NTFS attribute Tag @Short
  extra = append(extra, 0x18, 0x00)             // Size of attribute#1 @Short
  extra = appendUint32(extra, data.LastWriteTime.LowDateTime)
  extra = appendUint32(extra, data.LastWriteTime.HighDateTime)
  extra = appendUint32(extra, data.LastAccessTime.LowDateTime)
  extra = appendUint32(extra, data.LastAccessTime.HighDateTime)
  extra = appendUint32(extra, data.CreationTime.LowDateTime)
  extra = appendUint32(extra, data.CreationTime.HighDateTime)

あ、すみませんが、infoってのはFileInfoですよ。
filepath.Walkを呼ぶので、WalkFuncを実装していて、引数でFileInfoが渡ってきます。

ホストOS

7zで言うところのホストOSは、FileHeaderのCreatorVersionに相当する。

  fh.CreatorVersion = 11 << 8

意味不明な数字がでてくるでしょう。
goのstructを見ると数字が解るので、それを8ビットシフトして使えばなんとかなる。
この11はNTFS