Cybozu Garoon APIのファイル管理の部分だけのgoライブラリを書いた

はじめに

Cybozu Garoon APIのファイル管理のうち、フォルダ一覧取得、フォルダ内のファイル一覧取得、ファイルダウンロードのAPIを呼び出すライブラリをGoで書いてみました。

ただし、汎用的なライブラリではなく、自分が必要な機能のみを実装しています。レスポンスの中の項目も自分が必要な部分だけ取り出して残りは破棄しています。 sigbus.info: コードを書くことは無限の可能性を捨てて一つのやり方を選ぶということを読んでから、汎用性をあまり気にせず自分の用途に合わせて書くようになって楽で良いです。

実装方法についてのメモ

まず、Garoon APIの手動での呼び出し方はgaroon - Cybozu ガルーン API を使ってみる - Qiitaを参考にして試してみました。

リクエストのXML組み立て

Garoon APIはSOAPなので、リクエストやレスポンスはXMLになります。

リクエストを送るところはCybozu ガルーン API を golang で叩いてみる - Qiitaを見たのですが、Goのencoding/xmlを使いこなす - Qiitaを参考にMarshalXMLを実装する方式にしてみました。

xml.MarshalerMarshalXML(e *Encoder, start StartElement) errorstart をエンコードするのが本来の使い方だとは思うのですが、下記の例のように CabinetGetFolderInfo といったリクエスト本体を渡すと soap:Envelope でラップしてエンコードしてくれる方が使うときに楽なので、 MarshalXML 内でデータ構造を組み立ててエンコードするようにしてみました。

<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Header>
    <Action>CabinetGetFolderInfo</Action>
    <Security>
      <UsernameToken>
        <Username>foo</Username>
        <Password>password</Password>
      </UsernameToken>
    </Security>
    <Timestamp>
      <Created>2010-08-12T14:45:00Z</Created>
      <Expires>2037-08-12T14:45:00Z</Expires>
    </Timestamp>
    <Locale>jp</Locale>
  </soap:Header>
  <soap:Body>
    <CabinetGetFolderInfo>
      <parameters></parameters>
    </CabinetGetFolderInfo>
  </soap:Body>
</soap:Envelope>

soap:Envelope でラップした構造を作るところは、

https://github.com/hnakamur/garoonclient/blob/d8aceb8ae09c6094dd65a1623fc99ca89a1ccebd/request.go#L44-L62

func buildRequestStruct(h RequestHeader, apiName string, parameters interface{}) envelope {
    return envelope{
        Xmlns: "http://www.w3.org/2003/05/soap-envelope",
        Header: header{
            Action:   apiName,
            Username: h.Username,
            Password: h.Password,
            Created:  h.Created,
            Expires:  h.Expires,
            Locale:   h.Locale,
        },
        Body: body{
            Content: bodyContent{
                XMLName:    xml.Name{Local: apiName},
                Parameters: parameters,
            },
        },
    }
}

で共通処理として実装し、各API用のリクエストの構造体では

https://github.com/hnakamur/garoonclient/blob/d8aceb8ae09c6094dd65a1623fc99ca89a1ccebd/cabinet.go#L16-L26

type CabinetGetFolderInfoRequest struct {
    Header RequestHeader
}

func (r CabinetGetFolderInfoRequest) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
    return e.Encode(buildRequestStruct(
        r.Header,
        "CabinetGetFolderInfo",
        struct{}{},
    ))
}

のようにして呼び出しています。

また、日時の項目は構造体側では time.Time にしたいところですが、Issue 2771 - go - encoding/xml: MarshalXML interface is not good enough - The Go Programming Language - Google Project Hostingコメント#2を読んで string にしました。

レスポンスのパース処理

レスポンスのパースはCybozu ガルーン API のレスポンスのXMLを golang でパースする - Qiitaを見たのですが、Parsing huge XML files with Go - david singletonの方法のほうが楽なのでこちらを参考にしました。

共通のユーテリティ関数としては https://github.com/hnakamur/garoonclient/blob/d8aceb8ae09c6094dd65a1623fc99ca89a1ccebd/response.go

var ResponseTagNotFoundError = errors.New("response tag not found")

func parseResponse(r io.Reader, localName string, v interface{}) error {
    decoder := xml.NewDecoder(r)
    for {
        t, _ := decoder.Token()
        if t == nil {
            break
        }
        switch se := t.(type) {
        case xml.StartElement:
            if se.Name.Local == localName {
                return decoder.DecodeElement(v, &se)
            }
        }
    }
    return ResponseTagNotFoundError
}

のように定義して、

https://github.com/hnakamur/garoonclient/blob/d8aceb8ae09c6094dd65a1623fc99ca89a1ccebd/cabinet.go#L115-L127

func parseCabinetGetFolderInfoResponse(r io.Reader) (*CabinetGetFolderInfoResponse, error) {
    exclude := NewExclude(func(b byte) bool {
        return b == 0x08 || b == 0x0B
    })
    r2 := transform.NewReader(r, exclude)
    var resp CabinetGetFolderInfoResponse
    err := parseResponse(r2, "CabinetGetFolderInfoResponse", &resp)
    if err != nil {
        return nil, err
    }
    resp.fillPath()
    return &resp, err
}

という感じで呼び出しています。

レスポンスからU+0008などの制御文字を除去

あと、レスポンスのXMLをそのままxml.Decoderに渡すとUTF-8の不正なバイト列といったエラーが出ました。U+0008やU+000Bというデータが入っていたので、これを除去するようにしました。

日本語の文字コード変換用のライブラリgolang.org/x/text/encoding/japaneseで使っているインターフェースgolang.org/x/text/transform/Transformerに合わせて実装しました。

https://github.com/hnakamur/garoonclient/blob/d8aceb8ae09c6094dd65a1623fc99ca89a1ccebd/cabinet.go#L28-L50

type exclude struct {
    transform.NopResetter
    excluder func(byte) bool
}

func NewExclude(excluder func(byte) bool) transform.Transformer {
    return exclude{excluder: excluder}
}

func (e exclude) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
    for nSrc = 0; nSrc < len(src); nSrc++ {
        b := src[nSrc]
        if !e.excluder(b) {
            if nDst >= len(dst) {
                err = transform.ErrShortDst
                return
            }
            dst[nDst] = b
            nDst++
        }
    }
    return
}

利用するときはgolang.org/x/text/transform/NewReaderを使います。

まとめ

Cybozu Garoon APIの一部のクライアントライブラリをGoで実装しました。

  • MarshalXMLを実装することで、構造体とXMLの構造がかなり違う場合でも、XMLに合わせて一々構造体を定義することなく楽に対応出来ました。
  • xml.DecoderのTokenを使うことでXMLの一部だけをパースしました。
  • 制御文字除去の処理をgolang.org/x/text/transform/Transformerインタフェースに合わせて実装しました。