Jira APIと戯れる 〜チケットの添付ファイルの一括ダウンロード編〜
eiryu
はじめに
エンジニアのeiryuと申します。
みなさんはJiraを使っていますか?
JiraはAtlassian社が提供しているプロジェクト・課題管理のWebサービスです。
JiraはAPIを提供しており、APIを扱えるようになると出来ることの幅が広がります。
私は今までAPIを利用して以下のようなことを行ってきました。
- チケット情報取得
- 特定条件のチケットを自動クローズ
- チケットの添付ファイルの一括ダウンロード
前回はチケット情報取得について書きました。
今回はチケットの添付ファイルの一括ダウンロードについて書いてみたいと思います。
環境情報
この記事の内容は以下の環境にて確認しています。
- Jira v8.5.1(オンプレミス)
- MySQL 5.6.51
$ groovy -v
Groovy Version: 2.5.13 JVM: 1.8.0_265 Vendor: Eclipse OpenJ9 OS: Mac OS X
$ wget -V
GNU Wget 1.21.1 built on darwin19.6.0.
今回のスクリプト作成の背景とJiraにおけるチケットの添付ファイル
Jiraのチケットにはファイルを添付することが出来ます。
そして、Jiraにはチケット情報をCSVでエクスポートする機能もあります。しかし、当然そのエクスポートされたCSVの中には添付ファイルの情報は含まれていません。
今回のスクリプトが必要になったケースは、事業譲渡により、企画等の社内機密の含まれない一部のJiraチケットだけを譲渡先に渡す時でした。
プロジェクト単位でのエクスポートであればアドオンがあるのですが、上記のようなケースでは個別に対応しなければなりません。
JiraのIssue APIの結果には以下のように添付ファイルの情報が含まれています。 /fields/attachment
の部分です。(表記はJSON Pointerによる)
この添付ファイルのリストの中のオブジェクトの content
がファイルの実体、 filename
がファイル名となります。
{
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
"id": "10002",
"self": "http://www.example.com/jira/rest/api/2/issue/10002",
"key": "EX-1",
"fields": {
"watcher": {
"self": "http://www.example.com/jira/rest/api/2/issue/EX-1/watchers",
"isWatching": false,
"watchCount": 1,
"watchers": [
{
"self": "http://www.example.com/jira/rest/api/2/user?username=fred",
"name": "fred",
"displayName": "Fred F. User",
"active": false
}
]
},
"attachment": [
{
"self": "http://www.example.com/jira/rest/api/2.0/attachments/10000",
"filename": "エラー状況.jpg",
"author": {
"self": "http://www.example.com/jira/rest/api/2/user?username=fred",
"name": "fred",
"avatarUrls": {
"48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred",
"24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred",
"16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred",
"32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred"
},
"displayName": "Fred F. User",
"active": false
},
"created": "2017-12-07T09:23:19.542+0000",
"size": 23123,
"mimeType": "image/jpeg",
"content": "http://www.example.com/jira/attachments/10000",
"thumbnail": "http://www.example.com/jira/secure/thumbnail/10000"
}
],
.
.
.
実際にやってみる
尚、今回のスクリプトは、ダウンロードしたいチケットのIssue APIの結果が以下のようなMySQLのテーブルに保存されている状態で動きます。
チケット情報の取得については、前回の記事もご参照ください。
create table tickets(
`issue_key` TEXT,
`summary` TEXT,
`ticket_created` TIMESTAMP,
`ticket_updated` TIMESTAMP,
`raw_json` MEDIUMTEXT, -- ISSUE APIで返ってきたJSON
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
primary key(`issue_key`(20))
);
やっていることとしては、前述したとおり、チケット情報のJSONの中に添付ファイルのURLが含まれているため、それを任意の場所にダウンロードするようにしています。
@GrabConfig(systemClassLoader = true)
@Grab('mysql:mysql-connector-java:5.1.31')
@Grab('com.google.code.gson:gson:2.8.5')
@Grab('com.squareup.okhttp3:okhttp:3.9.1')
@Grab('com.squareup.okhttp3:logging-interceptor:3.9.1')
import com.google.gson.Gson
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.sql.Sql
import groovyx.gpars.GParsPool
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
class Config {
static JIRA_USERNAME = System.getenv()['JIRA_USERNAME']
static JIRA_PASSWORD = System.getenv()['JIRA_PASSWORD']
static JIRA_HOST = System.getenv()['JIRA_HOST']
static DB_HOST = System.getenv()['DB_HOST']
static DB_PORT = System.getenv()['DB_PORT']
static DB_NAME = System.getenv()['DB_NAME']
static DB_USERNAME = System.getenv()['DB_USERNAME']
static DB_PASSWORD = System.getenv()['DB_PASSWORD']
// 末尾にスラッシュを入れないこと
// 事前に作成しておくこと
static DOWNLOAD_BASE_DIR = System.getenv()['DOWNLOAD_BASE_DIR']
}
def db = Sql.newInstance("jdbc:mysql://${Config.DB_HOST}:${Config.DB_PORT}/${Config.DB_NAME}?useLegacyDatetimeCode=false", Config.DB_USERNAME, Config.DB_PASSWORD, 'com.mysql.jdbc.Driver')
def httpLoggingInterceptor = new HttpLoggingInterceptor()
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder okHttpBuilder = new OkHttpClient().newBuilder()
// .addInterceptor(httpLoggingInterceptor) // 開発時のデバッグの際は設定する
OkHttpClient client = okHttpBuilder.build()
def credentialJson = new JsonBuilder([username: Config.JIRA_USERNAME, password: Config.JIRA_PASSWORD]).toString()
// ログイン
RequestBody requestBodyOfLogin = RequestBody.create(MediaType.parse("application/json"), credentialJson)
Request requestOfLogin = new Request.Builder()
.url("https://${Config.JIRA_HOST}/rest/auth/1/session")
.post(requestBodyOfLogin)
.build()
Response responseOfLogin = client.newCall(requestOfLogin).execute()
def sessionMap = new JsonSlurper().parseText(responseOfLogin.body().string())
def jsessionid = sessionMap['session']['value']
def rows = db.rows('select * from tickets order by issue_key')
GParsPool.withPool {
rows.eachParallel { row ->
def map = new Gson().fromJson(row['raw_json'], Map.class)
def issueKey = row['issue_key']
def attachments = map['fields']['attachment']
attachments.each { attachment ->
def downloadDir = "${Config.DOWNLOAD_BASE_DIR}/${issueKey}"
if (!new File(downloadDir).exists()) {
new File(downloadDir).mkdir()
}
def url = attachment['content']
def filename = attachment['filename']
def downloadPath = "${downloadDir}/${filename}"
// ダウンロード済みでないものをダウンロード
if (!new File(downloadPath).exists()) {
def p = ["wget", "--header", "Cookie: JSESSIONID=${jsessionid}", url, "-O", downloadPath].execute()
p.waitFor()
println "[parallel] Downloaded ${url} to ${downloadPath}"
}
}
}
}
参考文献
- JIRA Server platform REST API reference
- AMJ - Attachments Manager for Jira | Atlassian Marketplace
- 退屈なことはGroovyにやらせよう
- Jira APIと戯れる 〜チケット情報取得編〜
編集注記
本記事の著者は 2021 年 3 月末に退職されております。 この記事は本人承諾のもと投稿をさせて頂いております。