もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

Go言語でZipの圧縮・解凍を実装してみた

はじめに

少しでも安全にファイル・ディレクトリのやり取りを行うためのツールを作ることにした。

ファイル・ディレクトリを圧縮→暗号化したバイナリファイルを何処かに送りつけ、受け取りは復号→圧縮データを解凍みたいな。

Window・Macの両方でツールを使用したいので、Go言語で作ることにした。

先立って圧縮処理を作ることにした。

軽く調べたところ、Zip方式だとそれなりにサンプルがあったので、Go言語でZipの圧縮・解凍処理を書いてみた。

環境

  • 開発OS:Windows 10
  • Go:1.12.7

参考サイト

圧縮のソースコードは以下のサイトを参考にした。

stackoverflow.com

解凍は以下のサイト(多分)

meideru.com

その他参考サイト

golang.org

ハマったところ

参考サイトをそのままコピペで動かしたところ色々問題があった・・・(特に圧縮側)

  • ファイル単体の圧縮に対応出来なかったり、ディレクトリの文字列の最後に「\」が無いと正しく動かない。
  • 空のディレクトリを含んでいるディレクトリを圧縮したところ、空のディレクトリが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」について、参考にしたサイト↓↓↓

qiita.com

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に含めるようにした。

けれども解凍時には、そのメタ情報を無視している。(ディレクトリの生成に使っているぐらい)

完全に別のファイルになるし、更新日時とか引き継がなくても良いかなと・・・