こんにちは。初投稿です。どうぞお手柔らかにm(__)m。
Go言語に触れ始めて数か月、それほどガッツリ書いてるわけではないですが、少しわかってきた気がします(危険な時期)
さて、タイトルの通りZipファイルを解凍するのですが、よくあるZip解凍ツールのように「ファイル名を展開先のディレクトリ名に使う」のってよくありませんか?ないですかそうですか。
例えば「test20210318.zip」であれば「test20210318」というディレクトリを作って、その下層に展開する、というものです。
実際に業務でニーズがありまして、同じファイル構成でzip圧縮されたファイル群を、繰り返し解凍するような仕組みです。
作ったサンプルがこちら。
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爆弾を回避するためにも利用は必須かと思います。