概要
S3にあるファイルを一つにまとめてZipダウンロードする際の中身のファイルのタイムスタンプを引き継ぐ方法について解説する。
zip化の流れ
- tmpフォルダにzipファイルを配備する
- S3から該当ファイルを複数取得する
- 取得したファイルをZipファイルに書き込む <- この中身のタイムスタンプの設定
- zipファイルをS3にアップロードする
- tmpフォルダのzipファイルを削除し、S3へのzipパスを返却する
対象モジュール
archive/zip
※標準パッケージ
Goのバージョン
1.17.2
zip内のファイルの更新日時が1979/11/30になってしまう
zip書き込みを以下のようにそのまま実施してしまうとS3に登録した更新日時が適用されずに1979/11/30になってしまう問題がある。参考記事
func writeZip(zipFilePath string, zipWriter *zip.Writer, srcBody []byte) error {
writer, err := zipWriter.Create(zipFilePath)
if err != nil {
return err
}
if _, err := writer.Write(srcBody); err != nil {
return err
}
return nil
}
※writer.Write
をio.Copy
に置き換えた場合も同様
解決方法
writerを作成する前ファイルヘッダを書き換え、それを読み込ませることで変更日時を設定することができる。
ただ、zipWriter.CreateHeader
にセットするためのos.FileInfo
が必要になり、そのためにはos.File
が必要になるため、S3で取得したbyteデータを一度ファイル生成したのちに、os.FileInfoを取得する方法をとった。
func writeZip(zipFilePath string, srcBody []byte, zipWriter *zip.Writer) error {
// ユニークなtmpファイル名にする
fw, err := os.CreateTemp("tmp/s3_zip", "zip_item_*")
if err != nil {
return err
}
defer os.Remove(fw.Name()) // tmp作成したファイルは忘れず消しておく
defer fw.Close()
_, err = fw.Write(srcBody)
if err != nil {
return err
}
fileInfo, err := fw.Stat()
if err != nil {
return err
}
fileHeader, _ := zip.FileInfoHeader(fileInfo)
// Headerの場合、zipに含めるファイル名の指定が必要(sampleフォルダにsample1.jpgを配備したい場合はsample/sample1.jpgを設定する)
fileHeader.Name = zipFilePath
writer, err := zipWriter.CreateHeader(fileHeader)
if err != nil {
return err
}
if _, err := writer.Write(srcBody); err != nil {
return err
}
return nil
}
この場合はtmpにS3ファイルを作成した時間がセットされるため現在日時(≒zipダウンロードしたタイミング)で取得することなる。
要件上、現在日時で問題ない場合は上記で解決だが、ファイルごとのS3への変更日時で取得したい場合はもう一手間必要になる。
その場合、S3パッケージのGetObject時にS3の変更日時(s3.GetObjectOutput.LastModified
)を受け取り、それをos.Chtimes
にセットすることで変更日時を変更することができる。こちらはファイルのatimeとmtimeを更新することができる。
※atime:アクセス日時、mtime:変更日時
byteデータを一度Writeしてからos.Chtimes
をコールしないと反映されないので注意する
func writeZip(zipFilePath string, srcBody []byte, zipWriter *zip.Writer, srcLastModifiedAt time.Time) error {
// ユニークなtmpファイル名にする
fw, err := os.CreateTemp("tmp/s3_zip", "zip_item_*")
if err != nil {
return err
}
defer os.Remove(fw.Name())
defer fw.Close()
_, err = fw.Write(srcBody)
if err != nil {
return err
}
// --ここだけ追加--
err = os.Chtimes(fw.Name(), srcLastModifiedAt, srcLastModifiedAt)
if err != nil {
return err
}
// ----
fileInfo, err := fw.Stat()
if err != nil {
return err
}
fileHeader, _ := zip.FileInfoHeader(fileInfo)
// Headerの場合、zipに含めるファイル名の指定が必要(sampleフォルダにsample1.jpgを配備したい場合はsample/sample1.jpgを設定する)
fileHeader.Name = zipFilePath
writer, err := zipWriter.CreateHeader(fileHeader)
if err != nil {
return err
}
if _, err := writer.Write(srcBody); err != nil {
return err
}
return nil
}
// S3から中身のファイルと最終更新日時を取得する
func (s *S3Handler) getObject(bucket string, path string) ([]byte, *time.Time, error) {
obj, err := s.Client.GetObject(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: &path,
})
if err != nil {
return []byte{}, nil, err
}
defer obj.Body.Close()
buf := new(bytes.Buffer) // buffer Response Body
buf.ReadFrom(obj.Body)
return buf.Bytes(), obj.LastModified, nil
}
まとめ
S3ではなくzip化する環境で読み取れるファイルの場合はそのままファイルを読み込み、ヘッダーにセットするだけで期待通りの変更日時が設定されていたと思いますが、S3のように一度ダウンロードしてファイル生成する場合には、os.Chtimes
のような一手間が必要になるかと思います。
ご参考になれば幸いですm(_ _)m
参考