7
0

More than 3 years have passed since last update.

Golangでzipファイルを解凍する

Posted at

こんにちは。初投稿です。どうぞお手柔らかにm(__)m。

Go言語に触れ始めて数か月、それほどガッツリ書いてるわけではないですが、少しわかってきた気がします(危険な時期)

さて、タイトルの通りZipファイルを解凍するのですが、よくあるZip解凍ツールのように「ファイル名を展開先のディレクトリ名に使う」のってよくありませんか?ないですかそうですか。

例えば「test20210318.zip」であれば「test20210318」というディレクトリを作って、その下層に展開する、というものです。
実際に業務でニーズがありまして、同じファイル構成でzip圧縮されたファイル群を、繰り返し解凍するような仕組みです。

作ったサンプルがこちら。

sample/unzip/main.go
package main

import (
    "archive/zip"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "regexp"
)

// unZip zipファイルを展開する
func unZip(src, dest string) error {
    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer r.Close()

    ext := filepath.Ext(src)
    rep := regexp.MustCompile(ext + "$")
    dir := filepath.Base(rep.ReplaceAllString(src, ""))

    destDir := filepath.Join(dest, dir)
    // ファイル名のディレクトリを作成する
    if err := os.MkdirAll(destDir, os.ModeDir); err != nil {
        return err
    }

    for _, f := range r.File {
        if f.Mode().IsDir() {
            // ディレクトリは無視して構わない
            continue
        }
        if err := saveUnZipFile(destDir, *f); err != nil {
            return err
        }
    }

    return nil
}

// saveUnZipFile 展開したZipファイルをそのままローカルに保存する
func saveUnZipFile(destDir string, f zip.File) error {
    // 展開先のパスを設定する
    destPath := filepath.Join(destDir, f.Name)
    // 子孫ディレクトリがあれば作成する
    if err := os.MkdirAll(filepath.Dir(destPath), f.Mode()); err != nil {
        return err
    }
    // Zipファイルを開く
    rc, err := f.Open()
    if err != nil {
        return err
    }
    defer rc.Close()
    // 展開先ファイルを作成する
    destFile, err := os.Create(destPath)
    if err != nil {
        return err
    }
    defer destFile.Close()
    // 展開先ファイルに書き込む
    if _, err := io.Copy(destFile, rc); err != nil {
        return err
    }

    return nil
}

func main() {
    // パスを取得
    rootPath, _ := os.Getwd()
    rootDir := filepath.Dir(rootPath)
    fmt.Println(rootDir)

    zipPath := filepath.Join(rootDir, "downloads", "test20210318.zip")
    destDir := filepath.Join(rootDir, "reserves")

    if err := unZip(zipPath, destDir); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    os.Exit(0)
}

main()のzipPathとdestDirはお好みで。

$ go version
go version go1.14.6 linux/amd64
$ go run sample/unzip/main.go

Windowsユーザーなので、VSCodeで書いて、ターミナルをWSLにして実行しています。
Goの環境設定などは先人の記事を参考にしてください。

実はもっと無意味に複雑なことをしていましたが、あちこち記事を漁るうちに気が付いたことがあります。

zipファイル内のディレクトリを毎度作成する必要はない

zip.Fileを走査中、ディレクトリがあっても無視して構いません。

    for _, f := range r.File {
        if f.Mode().IsDir() {
            // ディレクトリは無視して構わない
            continue
        }

なぜかというと、zip.File.Nameにパスが含まれているからです。
後続処理のファイル作成時にディレクトリも作成すれば済みます。

    // 子孫ディレクトリがあれば作成する
    if err := os.MkdirAll(filepath.Dir(destPath), f.Mode()); err != nil {
        return err
    }

os.MkdirAllは既にディレクトリがあってもエラーを返しませんので、そのまま後続処理に流れます。
ついでに、zip.File.Mode()を使えば、zip内のパーミッションを引き継ぐことができます。

io.Copyを使う

以前はバッファにいったん全読み込みして、ファイルに書き出すような処理を書いていました。

以前のコード
        buf := make([]byte, f.UncompressedSize64)
        _, err = io.ReadFull(rc, buf)

これだとメモリを無駄に消費してしまうため、readerからwriterに直接よしなにしてくれるio.Copyが良いそうです。
また、io.CopyNというのもあります。これは書き込み時にバイト数を指定できるものです。
出自のわからないzipファイルを解凍するような場合は、zip爆弾を回避するためにも利用は必須かと思います。

7
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
7
0