BacklogのAPIを使って課題添付ファイルをまるごとダウンロードしてみた

Backlogの課題やコメントの内容をローカルにエクスポートするには標準の機能で簡単に実現できました!今回はそれに加えて、課題に添付されたファイルの一覧をBacklog APIを使って取得してみました。
2023.01.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。

Backlogってとても便利で使いやすいですよね。

たまにBacklogのスペースは自社が保有したまま、それまでの課題でのやりとりやWikiの内容をローカルに保管したいと考えることはありませんか?

例えば、プロジェクトから離脱する関係会社のメンバーにこれまでのやり取りの履歴を渡す・・・なんて場面も考えられますね。

今回はBacklog側のドキュメントを参照して良い方法を模索しつつ、Backlog APIを使って一部処理の自動化をしてみました!

検討したこと・やってみたことは以下のとおりです。

  1. Backlogの課題やWikiの内容をバックアップする方法を検討
  2. 【ノーコード】Backlogの標準機能を利用して課題の一覧・コメントをcsvに出力
  3. 【コーディング】Backlog APIを使って課題に添付されたファイルをすべてダウンロードしてみる

※ 以下やってみた内容は公式ドキュメントを拝見しながら自分なりにベストな方法を模索しましたが、あくまで私見です。もっと楽にできる方法があるかもしれませんので、ご留意ください。

先に結論

単に課題・やりとりしたコメントをテキストとして出力するのであれば、課題検索からの出力機能で秒速で可能です!

とはいえ、実際は課題の中で各種ファイル(Officeファイルなど)を添付したり、画像を添付することがあると思います。

今回は上で出力したCSVファイルを使用しつつBackLog APIを利用して、課題に添付されたファイルをダウンロードするCLIツールをサンプルで作ってみました!

以下のように、課題に添付されたファイルを順次ダウンロードしていきます。

課題:EXPORTTEST-215の添付ファイルを確認開始
    添付ファイル1m7wlVHg_400x400.jpegのダウンロード開始
    添付ファイル1m7wlVHg_400x400.jpegのダウンロード終了
課題:EXPORTTEST-213の添付ファイルを確認開始
    添付ファイルcomment.csvのダウンロード開始
    添付ファイルcomment.csvのダウンロード終了
課題:EXPORTTEST-214の添付ファイルを確認開始
課題:EXPORTTEST-211の添付ファイルを確認開始
課題:EXPORTTEST-212の添付ファイルを確認開始
課題:EXPORTTEST-210の添付ファイルを確認開始
終了しました

Step1.料金コストをかけないで要件を満たす方法を検討

先に検討した内容を図示すると以下のようになります。

20220113_backlog_issue_export_flow

今回は「特定スペースにおける特定プロジェクト配下の課題」をローカルに出力する方法を考えてみました。

なお、スペースとプロジェクトという言葉はサル先生のWiki入門【プロジェクト管理ツールBacklog】に記載する定義の意味合いで使っています。

「スペース」とは Backlog の用語で、一契約で利用できるデータ領域を指します。通常は、一組織につき一契約(1スペース)です。

Backlog では「プロジェクト」という単位ごとに Wiki が作られます。ここで言う「プロジェクト」は Backlog の用語です。一般的に言うプロジェクトごとに Backlog の「プロジェクト」を作ることもできますし、組織ごとに「プロジェクト」を作ることもできます。

ちょっと検索するだけで、課題とコメントのバックアップ方法についてすぐユーザーガイドで便利機能が見つかりました。

こういう機能が当たり前のように備わっているのがすばらしいですね。

試しに215ほど課題を作って、コメントを入れたりしてみたのですが以下のようにCSVとして出力できました。

20220113_backlog_issue_export_basic_export1

課題に投稿されたコメントも200件までなら同CSVに出力されています。

20220113_backlog_issue_export_basic_export2

「もうこれで良いんじゃないか」という気持ちにもなりましたが、あくまでテキストデータとして出力しただけなので課題に添付されたファイルなどは取得できていません。

調べてみたところ、Backlogは有料オプションでスペースの課題やWikiのデータ・共有ファイル・添付ファイルなどをバックアップできるようです。

今回求めることは以下のとおりです。

  • スペース全体ではなく特定のプロジェクトの課題とコメントの内容を出力したい(上のCSVでクリア済)
  • 上記出力した課題の添付ファイルを取得したい

そもそもスペース全体のバックアップが目的ではないので、上記有料オプションまでは必要なさそうです。

ということでコストをかけずに、課題の添付ファイルを取得する方法がないか探していると、すぐに以下APIがみつかりました。

ということで、今回は上記Backlogから出力した課題とコメントのCSVを使って添付ファイルを取得するサンプルコードを書いてみました!

上記フローでいえば、赤枠で囲った範囲をやります。

20220113_backlog_issue_export_flow_marked

なお、今回は課題に直接されたファイルをダウンロードしていますが、共有ファイルの保管はコーディング無しで可能なようですので、データをダウンロードして保管(バックアップ)できますか? – Backlog ヘルプセンターをご参照ください。

Step2.下準備・留意事項の整理

APIキーの発行

Backlog APIを利用するにあたり、今回はAPIキーを利用します。

APIの設定 – Backlog ヘルプセンターの手順に従ってAPIキーを発行してください。

留意事項

Backlog APIにはレート制限があります。

無料プラン・有料プランにより差はありますが、制限を超えるリクエスト送信すると429 Too Many Requestsが返されるようです。

以下アナウンスではレート制限に達した場合1分後にリクエストを再送する処理の実装を推奨しているため、考慮に入れる必要がありそうです。

Step3.Goで実装してみた

個人的な興味・さまざまな環境で使いやすそうであるという理由でGoで実装してみました。

Goの実務経験がほとんどないため、おかしな点も含んでいると思いますがご了承ください。

この記事ではポイントを掻い摘んで説明するため、コードの全文は以下リポジトリをご参照ください。

なお、以下で記載するコードはテストコードを書けていません。

また、私の環境(OS:macOS Monterey,チップ:Apple M1)でのみ動作確認をしています。

あくまでサンプルとしてご認識いただけますと幸いです。

再送処理などの実装

今回はAPIリクエストのクライアントとしてgo-resty/restyというライブラリを利用しています。

以下がリクエスト用のクライアントを生成している部分となります。

429ステータスコードが返却された場合に65秒後に再送をするように設定してみました。

func NewRequestClient() *resty.Client {
	client := resty.New()
	client.BaseURL = getBaseUrl()
	client.SetQueryParam("apiKey", ApiKey)
	// json-iteratorをデフォルトのJSOnクライアントに設定
	client.JSONMarshal = jsoniter.Marshal
	// 429リクエストの際にリトライを行う
	// 1分間とする理由(https://backlog.com/ja/blog/backlog-api-rate-limit-announcement/)
	client.AddRetryCondition(
		func(r *resty.Response, err error) bool {
			return r.StatusCode() == http.StatusTooManyRequests
		},
	)
	client.SetRetryCount(3).SetRetryWaitTime(65 * time.Second)
	return client
}

バックログのAPIへリクエストするさいはこちらのメソッドを利用します。

また、他にコマンドラインツールを楽に作るためにspf13/cobraや、JSONを取り扱うためのjson-iteratorなどのライブラリも使っています。

CSVの読み込み

今回は先にバックログで必要な課題をCSVで出力していることを前提にしているので、以下の部分でCSVを読み込みます。

// getIssueFilesCmd represents the getIssueFiles command
var getIssueFilesCmd = &cobra.Command{
	Use:     "getIssueFiles",
	Aliases: []string{"gif"},
	Short:   "get files attached space's issues",
	Long:    `get files attached space's issues.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("CSVの読み込み開始")
		f := openExportedCsv()
		defer f.Close()
		r := csv.NewReader(transform.NewReader(f, japanese.ShiftJIS.NewDecoder()))
		// 最初の行は無視して読み込み
		fmt.Println("CSVの読み込み終了")
		r.Read()
		for {
			records, err := r.Read()
			if err == io.EOF {
				fmt.Println("終了しました")
				break
			} else if err != nil {
				log.Fatal(err)
			}
      // ここからAPIリクエストの処理などを記載している
		}
	},
}

func openExportedCsv() *os.File {
	cf, err := os.OpenFile(CsvFile, os.O_RDONLY, os.ModePerm)
	if err != nil {
		panic(err)
	}
	return cf
}

課題に添付されたファイル一覧の取得

次のAPIを使用して課題に添付されたファイルを取得します。

type attachmentInfo struct {
	id   string
	name string
}

func getAttachedFileList(client *resty.Client, issue string) []attachmentInfo {
	// https://developer.nulab.com/ja/docs/backlog/api/2/get-list-of-issue-attachments/
	resp, err := client.R().
		Get(client.BaseURL + fmt.Sprintf("/api/v2/issues/%s/attachments", issue))
	if err != nil {
		log.Fatal(err)
	}
	if resp.StatusCode() != http.StatusOK {
		log.Fatalln("Request Fail:" + resp.String())
	}
	fileListResponse := jsoniter.Get(resp.Body())
	fileList := fileListResponse.Get()
	var attachmentList []attachmentInfo
	for i := 0; i < fileList.Size(); i++ {
		// ダウンロード
		elm := fileList.Get(i)
		attachmenID := elm.Get("id").ToString()
		attachmentName := elm.Get("name").ToString()
		ai := attachmentInfo{id: attachmenID, name: attachmentName}
		attachmentList = append(attachmentList, ai)
	}
	return attachmentList
}

APIのレスポンスを参考にしながら、必要なIDと添付ファイル名のみを保持します。

[
    {
        "id":8, //これと
        "name":"IMG0088.png", //これだけでよい
        "size":5563,
        "createdUser":{
            "id":1,
            "userId":"admin",
            "name":"admin",
            "roleType":1,
            "lang":"ja",
            "mailAddress":"eguchi@nulab.example",
            "lastLoginTime": "2022-09-01T06:35:39Z"
        },
        "created":"2014-10-28T09:24:43Z"
    },
    ...
]

添付ファイルのダウンロード

最後に以下のAPIを使って添付ファイルをダウンロードする処理を書きます。

func downLoadFile(client *resty.Client, issue string, attachement attachmentInfo) {
	// NOTE: 添付ファイルの保存先はユーザーに選んでもらえればなお良い
	baseDir, _ := os.Getwd()
	attachmentFileDir := "attachedFiles"
	outpuDir := path.Join(baseDir, attachmentFileDir, issue)
	os.MkdirAll(outpuDir, os.ModePerm)
	// https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-attachment/#
	url := client.BaseURL + fmt.Sprintf("/api/v2/issues/%s/attachments/%s", issue, attachement.id)
	_, err := client.R().SetOutput(path.Join(outpuDir, attachement.name)).Get(url)
	if err != nil {
		log.Fatalln("DownLoad AttachedFile Fail")
	}
}

作ったコードをビルドして実行してみる

ここまで紹介したコードを組み合わせて以下のようなコードとなります。

// getIssueFilesCmd represents the getIssueFiles command
var getIssueFilesCmd = &cobra.Command{
  // 色々とCLIツールの設定が書いてある
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("CSVの読み込み開始")
		f := openExportedCsv()
		defer f.Close()
		r := csv.NewReader(transform.NewReader(f, japanese.ShiftJIS.NewDecoder()))
		// 最初の行は無視して読み込み
		fmt.Println("CSVの読み込み終了")
		r.Read()
		for {
			records, err := r.Read()
			if err == io.EOF {
				fmt.Println("終了しました")
				break
			} else if err != nil {
				log.Fatal(err)
			}
			issue := records[4]
			fmt.Printf("課題:%sの添付ファイルを確認開始\n", issue)
			client := NewRequestClient()
			fileList := getAttachedFileList(client, issue)
			for _, attachment := range fileList {
				fmt.Printf("    添付ファイル%sのダウンロード開始\n", attachment.name)
				downLoadFile(client, issue, attachment)
				fmt.Printf("    添付ファイル%sのダウンロード終了\n", attachment.name)
			}
		}
	},
}

実際にビルドして実行してみます。

go build -ldflags="-s -w" -trimpath
# ビルド完了後
./backlog-backup-sample gif -a ${APIKey} -s ${SpaceID} -c ${BackLogから出力したCSVファイルのパス}

以下のように処理が始まります。

CSVの読み込み開始
CSVの読み込み終了
課題:EXPORTTEST-215の添付ファイルを確認開始
    添付ファイル1m7wlVHg_400x400.jpegのダウンロード開始
    添付ファイル1m7wlVHg_400x400.jpegのダウンロード終了
課題:EXPORTTEST-213の添付ファイルを確認開始
    添付ファイルcomment.csvのダウンロード開始
    添付ファイルcomment.csvのダウンロード終了
課題:EXPORTTEST-214の添付ファイルを確認開始
課題:EXPORTTEST-211の添付ファイルを確認開始
課題:EXPORTTEST-212の添付ファイルを確認開始
課題:EXPORTTEST-210の添付ファイルを確認開始
課題:EXPORTTEST-208の添付ファイルを確認開始
課題:EXPORTTEST-209の添付ファイルを確認開始
課題:EXPORTTEST-206の添付ファイルを確認開始
課題:EXPORTTEST-207の添付ファイルを確認開始
課題:EXPORTTEST-204の添付ファイルを確認開始
課題:EXPORTTEST-205の添付ファイルを確認開始

途中APIのレート制限に引っかかった場合、途中で処理が停止しますが最後まで待つと無事終了しました!

課題:EXPORTTEST-2の添付ファイルを確認開始
課題:EXPORTTEST-1の添付ファイルを確認開始
終了しました

添付ファイルを確認すると、添付ファイルがある課題ごとにディレクトリができ、その中に添付ファイルが保存されていました。

20220113_backlog_issue_export_result

20220113_backlog_issue_export_result2

最後に

以上、Backlog APIを使って課題に添付されたファイルを取得するツールをGoで書いてみました!

Backlog自体が非常に便利なサービスですが、APIを使うことでさらに可能性を感じました。

課題のCSV出力を始めとして、便利なサービスを用意してくれていますので、「もうちょっとこうしたい」という時にはAPIの利用を検討されてはいかがでしょうか?

この記事がどなたかの参考になればとてもうれしいです。