もなかアイスの試食品

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

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

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

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

Kotlinで期待していない例外UndeclaredThrowableExceptionがやってくる

はじめに

SpringBoot + Kotlinでテストを書いていた

自分で作成した例外が投げられるのを期待したテストを書いていたけど、期待していた例外が発生しなかった・・・

発生していた例外は「UndeclaredThrowableException」

Undeclared・・・未宣言、、、一体なんのこと??

SpringBoot + Kotlinでのコーディングは初めてだったのもあり解決にちょっと時間が掛かった

環境

  • SpringBoot:2.2.2
  • Kotlin:1.3.61

結論

結局、例外が発生するメソッドに「@Throws」を付ければ解決した

@Service
class CompanyService(
        private val companyRepository: CompanyRepository
) {

    @Transactional
    @Throws(NotFoundException::class)  // <---コレを追加
    fun deleteCompany(id: Int) {
        val company = companyRepository.findById(id)
                .orElseThrow {
                    NotFoundException()
                }
        companyRepository.delete(company)
        companyRepository.flush()
    }

}

実際のユニットテスト

@Test(expected = NotFoundException::class)
fun deleteCompany_NotFound_001() {
    run {
        val company = companyRepository.findById(/*存在しないデータ*/999999)
        Assert.assertFalse(company.isPresent)
    }

    target.deleteCompany(999999)
}

例外の定義

@ResponseStatus(value = HttpStatus.NOT_FOUND)
class NotFoundException: Exception() {
}

@Throwsを付けていない場合「target.deleteCompany(999999)」を呼び出すと、期待しているNotFoundExceptionではなく、UndeclaredThrowableExceptionが発生しテストが失敗する

発生したUndeclaredThrowableExceptionインスタンスの内部に、実際に投げている例外インスタンス(ここで言うNotFoundException)があった

@ResponseStatusを付与しているし、UndeclaredThrowableExceptionをハンドルせずに投げっぱなしにしたいなぁと思っていた

@Throwsがあったのを思い出して、試しに付けてみたら期待した動作になった

おわりに

Kotlinのメソッド内でJavaで言うところの検査例外を投げ、その例外が@Throwsで宣言していない場合、UndeclaredThrowableExceptionにラッパーされるっぽい

@Throwsを書かなくてもKotlinはコンパイルエラーにならないので、Spring(SpringBoot)の場合、@ControllerAdviceとか@ExceptionHandlerでUndeclaredThrowableExceptionをハンドリングするのが楽なのかもしれない(でもこの方法使えるのControllerだけか・・・?)

参考サイト

stackoverflow.com

docs.oracle.com

gsh-kz.hatenadiary.org

Javascriptで可読性と保守性を意識したエラー処理を考えてみた

はじめに

Webアプリを作成で、Httpのステータスコードや、Javascriptの例外によってユーザにわかりやすいメッセージを表示したい

このとき特に何も考えずにコーディングすると、try-catchのcatch句の中で、結構なif文(またはswitch文)が出てきそうな気がする

しかも条件判断はほとんど同じなんだけれども、細かいところが違うだけみたいな(404エラーのときは、ある変数に文言を代入する。「ある変数」だけが色んなところで微妙に違う)

ということで勉強も理解もしていないけれども、確かこんなデザインパターンあったよね?ということで、Javascriptのエラー処理を読みやすくする方法について考えてみた

とりあえず良く書きそうなエラー処理

try {
  // axiosでAPI呼び出し
} catch (e) {
  if (!e.response) {
    this.errorMessage = 'サーバに接続できません'
  } else {
    switch (e.response.status) {
      case 401:
        this.errorMessage = 'ログインID、またはパスワードが異なります'
        break
      case 500:
        this.errorMessage = '処理中に問題が発生しました'
        break

      // 中略

    }
  }
}

switch文のbreakって邪魔だよなぁ・・・

switch文をif文にしても、個人的には「ゴチャゴチャしてるなぁ」と感じる

ネストしたif文が結構嫌いなのかもしれない

try {
  // axiosでAPI呼び出し
} catch (e) {
  if (!e.response) {
    this.errorMessage = 'サーバに接続できません'
  } else {
    const status = e.response.status
    if (status === 401) {
      this.errorMessage = 'ログインID、またはパスワードが異なります'
    } else if (status === 500) {
      this.errorMessage = '処理中に問題が発生しました'
    }

    // 中略

    }
  }
}

switch文にしても、if文にしても「気を付けながら」コードを書いている気がする(しかも読むときも気を付けている)

あとAxiosでネットワークエラーが起きたかどうかを「!e.response」で判断しているけれども、バージョンアップで挙動が変わらないかちょっと心配

なんと言うか、ボケーッとしてても書けて、ボケーッとしても読めるように直感的な書き方にしたいんじゃ

メソッドチェーンや「チェイン・なんとかかんとか」というデザインパターンっぽいものを使えば読みやすくなると思ったので考えてみた

コードを考えてみた結果

とりあえず結果として、エラーハンドリングは以下のコードになった

import { ErrorHandler } from '../components/error-handler'

try {
  // axiosでAPI呼び出し
} catch (e) {
  ErrorHandler.builder(this, e)
    .unauthorized(function () {
      this.errorMessage = 'ログインID、またはパスワードが異なります'
    })
    .internalServerError(function () {
      this.errorMessage = '処理中に問題が発生しました'
    })
    .networkError(function () {
      this.errorMessage = 'サーバと通信が出来ません'
    })
    .handle()
}

ちなみにコレはVueコンポーネント内の処理の一部

エラーのコールバック関数の中で、「this」を使ってVueコンポーネントのdataのプロパティにアクセス出来るようになっている

以下がerror-handler.jsのソースコード

class AbstractHandler {
  constructor (thisArgs, chainContext, action) {
    this.thisArgs = thisArgs
    this.chainContext = chainContext
    if (typeof action !== 'function') {
      throw new TypeError('action is require function.')
    }
    this.action = action
  }

  invokeAction () {
    this.action.apply(this.thisArgs, this.chainContext.error)
    ++this.chainContext.handledCount
  }
}

class StatusCodeHandler extends AbstractHandler {
  constructor (thisArgs, chainContext, action, code) {
    super(thisArgs, chainContext, action)
    this.expectedCode = code
  }

  handle () {
    const e = this.chainContext.error
    if (!e.response) {
      return
    }
    if (e.response.status === this.expectedCode) {
      this.invokeAction()
    }
  }
}

class NetworkErrorHandler extends AbstractHandler {
  handle () {
    if (!this.chainContext.error.response) {
      this.invokeAction()
    }
  }
}

class ErrorHandlerBuilder {
  constructor (thisArgs, error) {
    this.thisArgs = thisArgs
    this.chainContext = {
      error: error,
      handledCount: 0
    }
    this.handlers = []
  }

  unauthorized (action) {
    this.handlers.push(new StatusCodeHandler(this.thisArgs, this.chainContext, action, 401))
    return this
  }

  internalServerError (action) {
    this.handlers.push(new StatusCodeHandler(this.thisArgs, this.chainContext, action, 500))
    return this
  }

  networkError (action) {
    this.handlers.push(new NetworkErrorHandler(this.thisArgs, this.chainContext, action))
    return this
  }

  handle () {
    this.handlers.forEach(handler => handler.handle())
    if (this.chainContext.handledCount === 0) {
      throw new Error('ErrorHandlerBuilder has unhandled error:\n' + this.chainContext.error)
    }
  }
}

export class ErrorHandler {
  static builder (thisArgs, error) {
    return new ErrorHandlerBuilder(thisArgs, error)
  }
}

おわりに

メソッドチェーンのおかげで、エラー処理の部分は自然な感じで読めるようになったと思う

あと統合開発環境ならピリオドを入力しただけでメソッドの候補が出てくるので、それだけでもエラー処理を書くのが楽になったのは地味に便利