Go言語でZipの圧縮・解凍を実装してみた
はじめに
少しでも安全にファイル・ディレクトリのやり取りを行うためのツールを作ることにした。
ファイル・ディレクトリを圧縮→暗号化したバイナリファイルを何処かに送りつけ、受け取りは復号→圧縮データを解凍みたいな。
Window・Macの両方でツールを使用したいので、Go言語で作ることにした。
先立って圧縮処理を作ることにした。
軽く調べたところ、Zip方式だとそれなりにサンプルがあったので、Go言語でZipの圧縮・解凍処理を書いてみた。
環境
- 開発OS:Windows 10
- Go:1.12.7
参考サイト
圧縮のソースコードは以下のサイトを参考にした。
解凍は以下のサイト(多分)
その他参考サイト
ハマったところ
参考サイトをそのままコピペで動かしたところ色々問題があった・・・(特に圧縮側)
- ファイル単体の圧縮に対応出来なかったり、ディレクトリの文字列の最後に「\」が無いと正しく動かない。
- 空のディレクトリを含んでいるディレクトリを圧縮したところ、空のディレクトリがZipの中に無い。
- Go言語の実行ファイルで圧縮は出来るが、解凍が出来ない。(フォルダを作ろうとしてくれない。ただし7zipでは解凍できる。)
- メタ情報(ファイル・ディレクトリの更新日時など)がZipに含まれていない。
とりあえずチマチマ修正して、自分が使う上での問題点は解決出来た。
ソースコード
Go言語でZipの圧縮・解凍は面倒臭いです。(白目)
圧縮
package zip import ( "../logger" "archive/zip" "fmt" "io" "io/ioutil" "os" "path/filepath" ) func Compress(writer io.Writer, target string) error { zipWriter := zip.NewWriter(writer) defer func() { if err := zipWriter.Close(); err != nil { logger.Errorln(err.Error()) } }() isDir, err := isDirectory(target) if err != nil { return err } if isDir { if err := addZipFiles(zipWriter, target, ""); err != nil { return err } } else { fileName := filepath.Base(target) if err := addZipFile(zipWriter, target, fileName); err != nil { return err } } return nil } func isDirectory(path string) (bool, error) { fileInfo, err := os.Stat(path) if err != nil { return false, &Error{ internalError: err, Message: fmt.Sprintf("Can not access directory: '%s'", path), } } return fileInfo.IsDir(), nil } func addZipFiles(writer *zip.Writer, basePath, pathInZip string) error { fileInfoArray, err := ioutil.ReadDir(basePath) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not read directory: '%s'", basePath), } } basePath = complementPath(basePath) pathInZip = complementPath(pathInZip) for _, fileInfo := range fileInfoArray { newBasePath := basePath + fileInfo.Name() newPathInZip := pathInZip + fileInfo.Name() if fileInfo.IsDir() { if err = addDirectory(writer, newBasePath); err != nil { return err } newBasePath = newBasePath + string(os.PathSeparator) newPathInZip = newPathInZip + string(os.PathSeparator) if err = addZipFiles(writer, newBasePath, newPathInZip); err != nil { return err } } else { if err = addZipFile(writer, newBasePath, newPathInZip); err != nil { return err } } } return nil } func addZipFile(writer *zip.Writer, targetFilePath, pathInZip string) error { logger.Infoln("Target file:" + targetFilePath) logger.Infoln("Path in zip:" + pathInZip) data, err := ioutil.ReadFile(targetFilePath) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not read file: '%s'", targetFilePath), } } fileInfo, err := os.Lstat(targetFilePath) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Fail read header: '%s'", targetFilePath), } } header, err := zip.FileInfoHeader(fileInfo) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Fail create file info header: '%s'", targetFilePath), } } header.Name = pathInZip header.Method = zip.Deflate w, err := writer.CreateHeader(header) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not create zip: '%s'", targetFilePath), } } _, err = w.Write(data) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not write zip: '%s'", targetFilePath), } } return nil } func addDirectory(writer *zip.Writer, basePath string) error { fileInfo, err := os.Lstat(basePath) if err != nil { return err } header, err := zip.FileInfoHeader(fileInfo) if err != nil { return err } if _, err = writer.CreateHeader(header); err != nil { return err } return nil } func complementPath(path string) string { l := len(path) if l == 0 { return path } lastChar := path[l-1 : l] if lastChar == "/" || lastChar == "\\" { return path } else { return path + string(os.PathSeparator) } }
解凍
※圧縮と解凍処理は同じファイルに書いているため、import文は省略
func Decompress(dest, target string) error { reader, err := zip.OpenReader(target) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not read file: '%s'", target), } } defer func() { _ = reader.Close() }() for _, zippedFile := range reader.File { path := filepath.Join(dest, zippedFile.Name) if zippedFile.FileInfo().IsDir() { err = os.MkdirAll(path, zippedFile.Mode()) logger.Infoln("created directory: " + path) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not create directory: '%s'", path), } } } else { logger.Infoln("zipped file name: " + zippedFile.Name) if err = createFileFromZipped(path, zippedFile); err != nil { return err } } } return nil } func createFileFromZipped(path string, zippedFile *zip.File) error { reader, err := zippedFile.Open() if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not open zipped file: '%s'", zippedFile.Name), } } defer func() { _ = reader.Close() }() destFile, err := os.Create(path) if err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Can not open file: '%s'", path), } } defer func() { _ = destFile.Close() }() if _, err := io.Copy(destFile, reader); err != nil { return &Error{ internalError: err, Message: fmt.Sprintf("Failed copy data: '%s'", path), } } return nil }
例外
例外はコンナンでええやろ(適当)
package zip type Error struct { internalError error Message string } func (z *Error) Error() string { var output = z.Message if z.internalError != nil { output = output + "\n\t" + z.internalError.Error() } return output }
実際に使ってみる
とりあえずZipの圧縮・解凍をする実行ファイルを作ってみた。
CLIコマンドとして使えるように、「urfave/cli」を利用してみた。
「urfave/cli」について、参考にしたサイト↓↓↓
package main import ( "../../internal/logger" "../../internal/zip" "bytes" "github.com/urfave/cli" "os" ) func main() { app := cli.NewApp() app.Name = "zip" app.Version = "0.0.1" app.Action = defaultAction app.Commands = []*cli.Command{ { Name: "c", Usage: "create zip archive", Action: zipProcess, }, { Name: "d", Usage: "unzip archive", Action: unzipProcess, }, } err := app.Run(os.Args) if err != nil { logger.Error(err) } } func defaultAction(context *cli.Context) error { return cli.ShowAppHelp(context) } func zipProcess(context *cli.Context) error { var buffer bytes.Buffer filePath := context.Args().Get(0) if _, err := os.Stat(filePath); err != nil { return err } logger.Infoln("Target file path: " + filePath) logger.Infoln("Start zip") if err := zip.Compress(&buffer, filePath); err != nil { return err } logger.Infoln("Complete zip") file, err := os.Create("./test.zip") if err != nil { return err } defer func() { _ = file.Close() }() _, err = file.Write(buffer.Bytes()) if err != nil { return err } return nil } func unzipProcess(context *cli.Context) error { targetFilePath := context.Args().Get(0) if _, err := os.Stat(targetFilePath); err != nil { return err } logger.Infoln("Target file path: " + targetFilePath) logger.Infoln("Unzipping...") err := zip.Decompress("./unzipped", targetFilePath) if err != nil { return err } logger.Infoln("Unzipped") return nil }
以下のようなコマンドを実際にZipの解凍・圧縮が出来るようになった。
圧縮(実行ファイルがzip.exeの場合)
zip.exe c <ファイル・ディレクトリパス>
解凍
zip.exe d <Zipファイルパス>
おわりに
圧縮時にはメタ情報(ファイルの更新日時のような)をZipに含めるようにした。
けれども解凍時には、そのメタ情報を無視している。(ディレクトリの生成に使っているぐらい)
完全に別のファイルになるし、更新日時とか引き継がなくても良いかなと・・・