LoginSignup
1
0

More than 1 year has passed since last update.

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

Posted at

概要

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

zip化の流れ

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

対象モジュール

archive/zip
※標準パッケージ

Goのバージョン

1.17.2

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

zip書き込みを以下のようにそのまま実施してしまうとS3に登録した更新日時が適用されずに1979/11/30になってしまう問題がある。参考記事

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.Writeio.Copyに置き換えた場合も同様

 zipの中身

19791130の例.png

解決方法

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

現在日時として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

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

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

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

まとめ

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

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0