GolangでS3からZipダウンロードする時の中身のタイムスタンプの設定


## 概要

S3にあるファイルを一つにまとめてZipダウンロードする際の中身のファイルのタイムスタンプを引き継ぐ方法について解説する。

## zip化の流れ

1. tmpフォルダにzipファイルを配備する
1. S3から該当ファイルを複数取得する
1. 取得したファイルをZipファイルに書き込む **<- この中身のタイムスタンプの設定**
1. zipファイルをS3にアップロードする
1. tmpフォルダのzipファイルを削除し、S３へのzipパスを返却する

## 対象モジュール

archive/zip
※標準パッケージ

## Goのバージョン
1.17.2


## zip内のファイルの更新日時が1979/11/30になってしまう


zip書き込みを以下のようにそのまま実施してしまうとS3に登録した更新日時が適用されずに1979/11/30になってしまう問題がある。[参考記事](https://stackoverflow.com/questions/37517620/golang-zip-package-modified-time-defaults-to-30-november-1979-0000)

```go:write_zip.go
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`に置き換えた場合も同様


###　zipの中身
![19791130の例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222326/97bfe71f-8f46-a7d1-3bd2-eebb844077d8.png "Zipの中身")



### 解決方法

writerを作成する前ファイルヘッダを書き換え、それを読み込ませることで変更日時を設定することができる。
ただ、`zipWriter.CreateHeader`にセットするための`os.FileInfo`が必要になり、そのためには`os.File`が必要になるため、S3で取得したbyteデータを一度ファイル生成したのちに、os.FileInfoを取得する方法をとった。

####
```go:現在日時としてzip化する方法
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ダウンロードしたタイミング）で取得することなる。

###　zipの中身
![現在日時の例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222326/3e416a73-4f5c-9a53-78dd-bfccabbb01cd.png)


要件上、現在日時で問題ない場合は上記で解決だが、ファイルごとのS3への変更日時で取得したい場合はもう一手間必要になる。
その場合、S3パッケージのGetObject時にS3の変更日時（`s3.GetObjectOutput.LastModified`）を受け取り、それを`os.Chtimes`にセットすることで変更日時を変更することができる。こちらはファイルのatimeとmtimeを更新することができる。
※atime:アクセス日時、mtime:変更日時

**byteデータを一度Writeしてから`os.Chtimes`をコールしないと反映されないので注意する**

```go:S3の変更日時としてzip化する方法
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
}

```
###　zipの中身
![設定日の例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222326/3a475558-9e45-02cf-8107-a0607b09fe78.png)


## まとめ

S3ではなくzip化する環境で読み取れるファイルの場合はそのままファイルを読み込み、ヘッダーにセットするだけで期待通りの変更日時が設定されていたと思いますが、S3のように一度ダウンロードしてファイル生成する場合には、`os.Chtimes`のような一手間が必要になるかと思います。
ご参考になれば幸いですm(_ _)m



#### 参考
https://stackoverflow.com/questions/37517620/golang-zip-package-modified-time-defaults-to-30-november-1979-0000

https://qiita.com/yuki2006/items/0211421792bd788f8153