PowershellとOutlookVBAを使ってRedmineAPIで既存のチケットにファイルを(ほぼ)自動添付する#005

概要

 定期的にOutlookメールで通知されてくる業務カレンダーを、Redmineのチケットにアップロードして開発ベンダーに連携する作業が手作業で煩わしく、自動化したい欲求にかられたため着手した。幸いにもメールタイトルが正規表現可能(正し性善説仕様のため、送信者が表現の範疇を超えて誤字るとコケるw)

Qiitaに掲載しようと思った経緯は、Powershellでチケットの起票方法はあったがファイル添付のやり方が意外とGoogle先生に聞いてもヒットしなかったので、備忘録も兼ねて記事を書く事にしました(^q^)

(5/5回)

※この業務は2年前の物と古くRedmineのVersionは2.5

第一回 課題概要とAPI使用のための事前準備
第二回 OutlookVBAでメールトリガーを作成する
第三回 Powershellでプロキシを通過する
第四回 PowershellでRedmineチケットにファイルを添付する(前半)
第五回 PowershellでRedmineチケットにファイルを添付する(後半) ←今ここ


着手

4. PowershellでRedmineチケットにファイルを添付する(後半)

8. アップロードしたファイルとチケットを紐づける

 前回セクション7で取得したトークン文字列やファイル名、チケット番号、プロジェクト番号等をここで指定する事で初めてチケットとアップロードしたファイルが紐づきます。ちなみにこの処理を忘れるといつまでたってもRedmineからはそのファイルにアクセス出来ません!**(゚д゚|||)メンドクセー

    ### 第二段階のパラメタをセット
    $token = $post1.upload.token          #セクション7でもらったトークン
    $fileName = $xlsFile.Name             #紐づけしたいファイル名。私の場合、どうしてもMB文字(日本語とか)の部分があるとそこだけ欠けてしまいます(´;ω;`)ウッ…
    $issueId = $get.issue.id              #紐づけしたいチケット番号。ここを割愛すると新規チケットを起票してアップロードするようになる。
    $projectId = $get.issue.project.id    #紐づけしたいプロジェクト番号
#    $subject = $get.issue.subject        #無くてもいい。ここで別の文字列を渡してあげるとチケットタイトル変更出来ます
    $description = $T.Year.ToString() + '-' + $T.Month.ToString() + '-' + $T.Day + ' Uploaded.'  #添付ファイルのコメント欄。例では識別のため日付文字列を入れているが、なくてもよい。

 ちなみに紐づけ成功した場合でも忘れた場合でも、ファイルたちはredmine/uploadsディレクトリ(だったかな?)配下に溜まっていきますので、紐づけしないゴミファイルはrmで削除しておきましょう…(´・ω・`)


9. 添付するファイルのContent-Typeの指定

 ここは説明のためセクションが分かれていますが、セクション8の続きで、パラメタを指定しています。
そしてセクション7で出てきたContent-Typeと混同しやすいのですが、、、(´・ω・`)
セクション7のやつはHTTPヘッダとして指定したContent-Typeです(´・ω・`)
↓のContent-Typeはjsonに記述してRedmine側で動いているJavaScriptがRailsに渡す際に処理するためのContent-Typeだと思います(´・ω・`)
ええ、結局はどちらもHTTPヘッダになるものだと思いますが、APIとして指定するのが前者、スクリプトに渡すのが後者だと私は解釈しますた(´・ω・`)

 気を取り直して、本記事の例ではExcelさんをアップロードしたいので、下記の通り、ExcelのバージョンによってContent-Typeを判別するためにファイル拡張子を見て判断する様にしています。xlsxであれば2007形式、それ以外(主にxlsを想定。はい、条件式足りてないのは承知しています)の場合は2003以前の形式としてContent-Typeを指定しています。xlsmだったらどないすんねんという声がここまで聞こえていそうですが、ワタクシの現場環境では当面その心配が無かったため簡素化しますた(^q^)

    $contentType = if ($testFile.Name -match "xlsx") {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"} else {"application/vnd.ms-excel"}

10. JSONの作成

 実はここもセクション8の続きなんですが、ここも特殊だったためセクション分けています。(´・ω・`)
今の今までPowershellのヒアドキュメントの書き方知らなかったー(´ω`;A)
@""@で囲むなんてなんか少し特殊ですよね(´・ω・`)
さすがMicrosoft(`・ω・´*)

それにしてもなぜヒアドキュメント?
「ハッシュをConvertTo-JSONすればいいじゃんね?」というごもっともな声がグサグサ刺さるのですが、
私もはじめはそうしていましたが今回はダメなんです(´・ω・`)
なぜかって?次のセクションで落ちがきますので…(´・ω・`)

    $issue_json = @"
{"issue": 
    {
        "project_id": $projectId, 
        "uploads": [
            {
                "token": "$token",
                "filename": "$fileName",
                "description": "$description",
                "content_type": "$contentType"
            }
         ]
    }
}
"@

これが成功すればようやくチケットに添付することができます。
ただし、ここもハマりポイントでした( ノД`)シクシク…

 セクション冒頭に書いた通り、最初はこんなカンジでハッシュ(他の言語でいう辞書)をConvertTo-JSONメソッドに渡してJSON作っていたんですが、、、(´・ω・`)

$json = @{
    "issue":{
        "project_id": $projectId,
        "uploads": {
            "token": "$token",
            "filename": "$fileName",
            "description": "$description",
            "content_type": "$contentType"
        }
    }
}

$json = ConvertTo-JSON($json)

これで次のセクションでPUTするとHTTPコード200で返ってくるのにファイルが添付されていない…(´・ω・`)

何故だ…(´・ω・`)

と、いうことでおよそ1日ハマった私は、httpdのerrorログを見てみることに(´・ω・`)

App 11788 stderr: MultiJson::ParseError (795: unexpected token at 'issue=System.Collections.Hashtable'):
App 11788 stderr:   json (1.8.1) lib/json/common.rb:155:in `parse'
App 11788 stderr:   json (1.8.1) lib/json/common.rb:155:in `parse'
App 11788 stderr:   multi_json (1.10.1) lib/multi_json/adapters/json_common.rb:16:in `load'
App 11788 stderr:   multi_json (1.10.1) lib/multi_json/adapter.rb:20:in `load'
App 11788 stderr:   multi_json (1.10.1) lib/multi_json.rb:119:in `load'
App 11788 stderr:   activesupport (3.2.19) lib/active_support/json/decoding.rb:15:in `decode'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/params_parser.rb:47:in `parse_formatted_parameters'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/params_parser.rb:17:in `call'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/flash.rb:242:in `call'
App 11788 stderr:   rack (1.4.5) lib/rack/session/abstract/id.rb:210:in `context'
App 11788 stderr:   rack (1.4.5) lib/rack/session/abstract/id.rb:205:in `call'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/cookies.rb:341:in `call'
App 11788 stderr:   activerecord (3.2.19) lib/active_record/query_cache.rb:64:in `call'
App 11788 stderr:   activerecord (3.2.19) lib/active_record/connection_adapters/abstract/connection_pool.rb:479:in `call'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/callbacks.rb:28:in `block in call'
App 11788 stderr:   activesupport (3.2.19) lib/active_support/callbacks.rb:405:in `_run__976055210039787607__call__2928873253004765293__callbacks'
App 11788 stderr:   activesupport (3.2.19) lib/active_support/callbacks.rb:405:in `__run_callback'
App 11788 stderr:   activesupport (3.2.19) lib/active_support/callbacks.rb:385:in `_run_call_callbacks'
App 11788 stderr:   activesupport (3.2.19) lib/active_support/callbacks.rb:81:in `run_callbacks'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/remote_ip.rb:31:in `call'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/debug_exceptions.rb:16:in `call'
App 11788 stderr:   actionpack (3.2.19) lib/action_dispatch/middleware/show_exceptions.rb:56:in `call'
App 11788 stderr:   railties (3.2.19) lib/rails/rack/logger.rb:32:in `call_app'

JSONのパースエラーとか出とる…(´・ω・`) 

はて(´・ω・`)

JSON間違っていない様に思えるが、、、(´・ω・`)

これを試しに出力してみると、、、(´・ω・`)

{
  "issue": {
    "project_id": $projectId, 
    "uploads": {
      "token": "$token",
      "filename": "$fileName",
      "description": "$description",
      "content_type": "$contentType"
    }
  }
}

リファレンスを読むと、、、(´・ω・`)

POST /issues.json
{
  "issue": {
    "project_id": "1",
    "subject": "Creating an issue with a uploaded file",
    "uploads": [
      {"token": "7167.ed1ccdb093229ca1bd0b043618d88743", "filename": "image.png", "content_type": "image/png"}
    ]
  }
}

ゴシゴシ(つд+)

(・q・)ジー

{
  "issue": {
    "project_id": $projectId, 
    "uploads": {
      "token": "$token",
      "filename": "$fileName",
      "description": "$description",
      "content_type": "$contentType"
    }
  }
}
POST /issues.json
{
  "issue": {
    "project_id": "1",
    "subject": "Creating an issue with a uploaded file",
    "uploads": [
      {"token": "7167.ed1ccdb093229ca1bd0b043618d88743", "filename": "image.png", "content_type": "image/png"}
    ]
  }
}

ゴシゴシ(つд⊂)

(・д・´)ジー

{
  "issue": {
    "project_id": $projectId, 
    "uploads": {
      "token": "$token",
      "filename": "$fileName",
      "description": "$description",
      "content_type": "$contentType"
    }
  }
}
POST /issues.json
{
  "issue": {
    "project_id": "1",
    "subject": "Creating an issue with a uploaded file",
    "uploads": [
      {"token": "7167.ed1ccdb093229ca1bd0b043618d88743", "filename": "image.png", "content_type": "image/png"}
    ]
  }
}

uploadsは配列かーい( ゚Д゚)

む、ハッシュのネストで配列ってどうやって書けばいいんだろう…(・q・;;)

と、いうことで仕方なくヒアドキュメントで書きました。(´・ω・`)


11. チケットにファイルを紐づける

 やっときました(´;ω;`)ウッ…

 ようやくチケットと添付ファイル紐づけのお時間です( *´艸`)
最初のPOSTと同じくInvoke-RestMethodコマンドを使います。 HTTPメソッドは今回はPUTを使ってBodyにはJSONを渡します。また、JSONを渡すため、Content-Typeはapplication/jsonを指定します。

    try{
        $post2 = Invoke-RestMethod -Uri ($baseuri + "issues/" + $issueId  + ".json") -Method put -Headers $Headers -body $issue_json -ContentType "application/json"

一応エラー制御しておいて、失敗したらプロンプトにエラーが表示される仕組みです。
(、、、が、このままだとエラー表示直後にプロンプト閉じますぜ(ФωФ)フフフ・・・)

    }catch [System.Net.WebException]{
        ### HTTPステータスコード取得
        $statusCode = $_.Exception.Response.StatusCode.value__

        ### レスポンス文字列取得
        $stream = $_.Exception.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader $stream
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd()
        Write-Output "results: " $responseBody      #確認用
    }

最後にVBAでローカルに保存しておいた添付ファイルを削除して終了です|д゚)ワスレズニ…
最後のelseはセクション6で出てきたifの兄弟ですので割愛します。

    Remove-Item $xlsFile

}else{
    Write-Host "チケット番号が違います。getしたチケット【題名】:" + $get.issue.subject
}

最後に

 実稼働のコスト減を考えると大した効果では無いが、私のシングルスレッドCPU(脳)はこの業務の度に一時停止し、再開時にはローディング時間がございますので、導入前後での(他にもあったので全て含めた成果ですが)心の余裕間はかなり大きかった気がします(^q^) と、思いたいw

 しかし、稼働が割けない現場だとそもそも無理な場合もありますよね...( ´-` )

プログラム作成に要した時間は記憶を辿ると凡そ2週間のうちの片手間だったのでトータルで6日間位かな。そのうち3日間は例の諸所でハマっていたな…( ´-` )

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.