BacklogのAPIを使ってWikiの添付ファイルごとまるっとダウンロードしてみた
こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。
Backlogってとても便利で使いやすいですよね。(数日ぶり)
私の前回の記事にて、Backlog APIを使って課題と課題添付ファイルの取得をやってみました!
前回はあくまで課題の添付ファイルダウンロードということで、Wikiに関してはノータッチでした。
今回は続きとしてプロジェクトのWikiのコンテンツを添付ファイルを含めてダウンロードしてみました!
やってみたことは以下のとおりです。
- BacklogのWikiの情報をローカルに保存する方法を検討
- 【コーディング】Backlog APIを使ってWikiのページと添付ファイルをダウンロード
※ 以下やってみた内容は公式ドキュメントを拝見しながら自分なりにベストな方法を模索しましたが、あくまで私見です。もっと楽にできる方法があるかもしれませんので、ご留意ください。
先に結論(出力結果イメージ)
Wikiページの内容を出力するだけならWikiページをPDFとして出力する標準機能を利用すれば良いと思います!
ただし、Wikiページが大量にあったり、添付ファイルも含めてダウンロードしておきたい場合は以下APIを利用すれば良いと思います。
今回もGo言語でサクッとCLIツールを作って、Wikiページ本文・添付ファイルのダウンロードができました!
# CLIツールの様子 Wiki一覧情報の取得開始 ページ情報の取得開始: Home ページ情報の取得開始: キックオフテンプレート ページ情報の取得開始: 振り返りテンプレート ページ情報の取得開始: 進捗報告テンプレート # 子ページにも対応 ページ情報の取得開始: ParentPage ページ情報の取得開始: ParentPage/ChildPage1 添付ファイルのダウンロード開始:1m7wlVHg_400x400.jpeg ページ情報の取得開始: ParentPage/ChildPage2 添付ファイルのダウンロード開始:20220113_backlog_issue_export_flow.jpg ページ情報の取得開始: ParameterSheet ページ情報の取得開始: ParameterSheet/EC2
Step1.料金コストをかけないで要件を満たす方法を検討
まずはWikiの情報をサクッと出力する方法を検討してみました。
いろいろと調べたところ、ノーコードで一番早そうなのが標準のWikiのPDF出力機能の利用でした。
その他にも前回の記事でも確認した有料オプションも利用できそうですが、今回も特定プロジェクト配下のWikiの出力で十分なのでスコープ外としました。
PDFとして出力した場合、WikiページにcsvやPDFファイルなどが添付されていると、当然ながらPDF化しても添付されていたファイルはついてきません。
また、Wikiのページ数が多い場合は手動でPDF化するのも大変だと思うので、今回はWikiページの内容と添付されたファイルの両方を取得できるようにBacklog APIを利用してみました。
Step2.下準備・留意事項の整理
事前にBacklog APIを利用するためのAPIキーなどの発行が必要です。
APIを利用するにあたりリクエストレートの制限などもありますので、前回の記事のStep2.下準備・留意事項の整理をご確認ください。
Step3.Goで実装してみた
今回もGoでサクッと実装してみました。
この記事ではポイントを掻い摘んで説明するため、コードの全文は以下リポジトリをご参照ください。
なお、以下で記載するコードはテストコードを書けていません。
また、私の環境(OS:macOS Monterey,チップ:Apple M1)でのみ動作確認をしています。
あくまでサンプルとしてご認識いただけますと幸いです。
再送処理などの実装
前回の記事と同様の内容となってしまいますが、再掲します。
以下Backlogのブログに記載されていますが、APIのリクエスト制限に抵触した場合HTTPステータスコード429が返却されます。
そのため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 }
また、コマンドラインツールを楽に作るためにspf13/cobraや、JSONを取り扱うためのjson-iteratorなどのライブラリも引き続き利用しています。
プロジェクト配下のWikiページ一覧の取得
次のAPIを使用してプロジェクト配下のWikiページの一覧を取得する処理を準備します。
type wikiInfo struct { id string name string } // Wikiページの一覧を取得 func getWikiList(client *resty.Client) []wikiInfo { // https://developer.nulab.com/ja/docs/backlog/api/2/get-wiki-page-list/# client.SetQueryParam("projectIdOrKey", ProjectID) resp, err := client.R(). Get(client.BaseURL + "/api/v2/wikis") 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 wikiList []wikiInfo for i := 0; i < fileList.Size(); i++ { elm := fileList.Get(i) wikiID := elm.Get("id").ToString() wikiName := elm.Get("name").ToString() wi := wikiInfo{id: wikiID, name: wikiName} wikiList = append(wikiList, wi) } return wikiList }
APIのレスポンスを参考にしながら、必要なWikiページのIDとWikiページ名のみを保持します。
[ { "id": 112, //これと "projectId": 103, "name": "Home", //この情報だけで良い "tags": [ { "id": 12, "name": "議事録" } ], "createdUser": { "id": 1, "userId": "admin", "name": "admin", "roleType": 1, "lang": "ja", "mailAddress": "eguchi@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "created": "2013-05-30T09:11:36Z", "updatedUser": { "id": 1, "userId": "admin", "name": "admin", "roleType": 1, "lang": "ja", "mailAddress": "eguchi@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "updated": "2013-05-30T09:11:36Z" }, ... ]
Wikiページの詳細・添付ファイル一覧の取得
次に以下のAPIを使ってWikiページの詳細情報(添付ファイルの一覧を含む)を取得する処理を準備します。
type wikiAttachment struct { id string name string } type wikiDetail struct { id string content string attachments []wikiAttachment } // Wikiページの詳細情報を取得 func getWikiDetail(client *resty.Client, wikiID string) wikiDetail { resp, err := client.R(). Get(client.BaseURL + fmt.Sprintf("/api/v2/wikis/%s", wikiID)) if err != nil { log.Fatal(err) } if resp.StatusCode() != http.StatusOK { log.Fatalln("Get WikiDetail Request Fail:" + resp.String()) } fileListResponse := jsoniter.Get(resp.Body()) elm := fileListResponse.Get() wikiContent := elm.Get("content").ToString() fileList := elm.Get("attachments") // Wikiページの添付ファイル情報を取得 var attachmentList []wikiAttachment for i := 0; i < fileList.Size(); i++ { elm := fileList.Get(i) attachmenID := elm.Get("id").ToString() attachmentName := elm.Get("name").ToString() wa := wikiAttachment{id: attachmenID, name: attachmentName} attachmentList = append(attachmentList, wa) } wd := wikiDetail{id: wikiID, content: wikiContent, attachments: attachmentList} return wd }
以下はレスポンスのサンプルですが、Wikiページの内容の他、attachments
として添付ファイルの情報が含まれているため、取得しています。
{ "id": 1, "projectId": 1, "name": "Home", // Wikiページに書かれている内容(本文) "content": "test", "tags": [ { "id": 12, "name": "議事録" } ], // 添付ファイルの情報 "attachments": [ { "id": 1, "name": "test.json", "size": 8857, "createdUser": { "id": 1, "userId": "admin", "name": "admin", "roleType": 1, "lang": "ja", "mailAddress": "eguchi@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "created": "2014-01-06T11:10:45Z" }, ... ], "sharedFiles": [ { "id": 454403, "type": "file", "dir": "/ユーザアイコン/", "name": "01_サラリーマン.png", "size": 2735, "createdUser": { "id": 5686, "userId": "takada", "name": "takada", "roleType":2, "lang":"ja", "mailAddress":"takada@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "created": "2009-02-27T03:26:15Z", "updatedUser": { "id": 5686, "userId": "takada", "name": "takada", "roleType": 2, "lang": "ja", "mailAddress": "takada@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "updated":"2009-03-03T16:57:47Z" }, ... ], "stars": [], "createdUser": { "id": 1, "userId": "admin", "name": "admin", "roleType": 1, "lang": "ja", "mailAddress": "eguchi@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "created": "2012-07-23T06:09:48Z", "updatedUser": { "id": 1, "userId": "admin", "name": "admin", "roleType": 1, "lang": "ja", "mailAddress": "eguchi@nulab.example", "lastLoginTime": "2022-09-01T06:35:39Z" }, "updated": "2012-07-23T06:09:48Z" }
添付ファイルのダウンロード
次に以下のAPIを使ってWikiページの添付ファイルをダウンロードします。
// Wiki添付ファイルのダウンロード func downLoadWikiFile( client *resty.Client, detail wikiDetail, attachment wikiAttachment, dir string, ) { // https://developer.nulab.com/ja/docs/backlog/api/2/get-wiki-page-attachment/# url := fmt.Sprintf( client.BaseURL+ "/api/v2/wikis/%s/attachments/%s", detail.id, attachment.id, ) _, err := client.R().SetOutput(path.Join(dir, attachment.name)).Get(url) if err != nil { log.Fatalln("DownLoad WikiAttachedFile Fail ", err) } }
作ったコードをビルドして実行してみる
ここまで紹介したコードを呼び出すメイン部分はこのような形となっています。
// getWikiCmd represents the getWiki command var getWikiCmd = &cobra.Command{ Use: "wiki", Short: "get wiki contents.", Long: `get wiki contents.`, Run: func(cmd *cobra.Command, args []string) { client := NewRequestClient() fmt.Println("Wiki一覧情報の取得開始") wikiList := getWikiList(client) for _, w := range wikiList { // Wikiページの詳細取得 client = NewRequestClient() fmt.Println("ページ情報の取得開始:", w.name) detail := getWikiDetail(client, w.id) dir := getContentDir(w) // Wikiのコンテンツをローカルに保存する os.MkdirAll(dir, os.ModePerm) cf, err := os.Create(path.Join(dir, "content.md")) if err != nil { log.Fatalln("Wiki content file create error: ", err) } defer cf.Close() cf.WriteString(detail.content) // 添付ファイルのダウンロード for _, attachment := range detail.attachments { fmt.Println(fmt.Sprintf(" 添付ファイルのダウンロード開始:%s", attachment.name)) client = NewRequestClient() downLoadWikiFile(client, detail, attachment, dir) } } }, } // Wikiコンテンツを保存するディレクトリパスの作成 func getContentDir(wi wikiInfo) string { cwd, _ := os.Getwd() baseDir := filepath.Join(cwd, "wiki") wikiPathList := strings.Split(wi.name, "/") wikiPath := filepath.Join(wikiPathList...) dir := filepath.Join(baseDir, wikiPath) return dir }
実際にビルドして実行してみます。
go build -ldflags="-s -w" -trimpath # ビルド完了後 ./backlog-backup-sample wiki --apikey ${APIキー} --space ${スペース名} --project ${プロジェクト名}
以下のように処理が始まります。
Wiki一覧情報の取得開始 ページ情報の取得開始: Home ページ情報の取得開始: キックオフテンプレート ページ情報の取得開始: 振り返りテンプレート ページ情報の取得開始: 進捗報告テンプレート ページ情報の取得開始: ParentPage ページ情報の取得開始: ParentPage/ChildPage1 添付ファイルのダウンロード開始:1m7wlVHg_400x400.jpeg ページ情報の取得開始: ParentPage/ChildPage2 添付ファイルのダウンロード開始:20220113_backlog_issue_export_flow.jpg ページ情報の取得開始: ParameterSheet ページ情報の取得開始: ParameterSheet/EC2 ページ情報の取得開始: ParentPage/ChildPage3 添付ファイルのダウンロード開始:comment.csv
冒頭でお見せしたように添付ファイルも含めたコンテンツがダウンロードできました。
最後に
今回もBacklog APIを使ってWikiコンテンツを取得してみました。
Backlog APIの一覧を眺めているだけでいろいろなことができそうでとても楽しいですね。
ぜひみなさんも「あれ?これって自動化できないかな?」と思った時はこのページを参照してみてください!
以上、この記事がどなたかの参考になればとてもうれしいです。