Questetra BPM Suite の Workflow API を利用して、「条件に一致するプロセスでアップロードされたファイルをすべてダウンロードする」スクリプトを紹介します。例えば、次のようなダウンロード条件を実装できます。

  • あるワークフローアプリで、今期開始されたプロセスにてアップロードされたファイル
  • 特定のユーザによって開始されたプロセスでアップロードされたファイル

大まかな手順は以下の通りです。

  • 検索条件を表す XML データ (criteria) を用意
  • プロセス検索を行う API エンドポイントを用いて、条件に一致するプロセスのデータを取得
  • 取得したプロセスデータからファイルに関するデータ(ex: ファイル ID)を抽出
  • ファイルダウンロードを行う API エンドポイントを用いて、目的のファイルをダウンロード

必須環境

シェルスクリプトを用いる場合

  • jq コマンド
  • Windows の場合は Windows Subsystem for Linux 上で curl コマンドが動作すること

Python を用いる場合

条件に一致するプロセスを検索する

Workflow API を用いてプロセス検索をする方法の詳細は「Workflow API を用いてプロセスやタスク一覧をダウンロードする」で紹介しています。検索条件を示す criteria という XML データの用意や、curl コマンドによる Basic 認証、API アクセスの方法についてはこちらをご覧ください。

今回使用する API エンドポイントは /API/OR/ProcessInstance/list(JSON, UTF-8)です。

ファイルをダウンロードする

ファイルダウンロードのための API エンドポイントは /API/OR/ProcessInstance/File/download ですが、このエンドポイントは 2 つのパラメータを取ります。

  • id: ファイルに付与された ID
  • processDataInstanceId: 各プロセスの各データ項目に付与された ID

この 2 つのパラメータはプロセス検索結果の JSON から抽出することができます。結果の例を以下に挙げますので、確認してみましょう。見やすさのためにプロセス 1 件分のみ抜き書いています。

プロセス検索API 応答例

[
 {
   "activeTokenNodeName": null,
   "data": {
     "1": { ← データ項目番号(モデラーに表示されているもの)
       "dataType": "STRING", ← 文字型データ項目
       ...(中略)...
     },
     "2": { ← データ項目番号(モデラーに表示されているもの)
       "dataType": "FILE2", ← ファイル型データ項目
       "id": 9143, ← processDataInstanceId と同じ
       "processDataDefinitionNumber": 2,
       "subType": null,
       "value": [
         {
           "contentType": "image/png",
           "id": 9149, ← ファイルに付与された ID
           "image": true,
           "length": 71550,
           "lengthText": "69.9 KB",
           "name": "ques-kun-01.png", ← ファイル名
           "processDataInstanceId": 9143 ← 各プロセスの各データ項目に付与された ID
         },
         {
           "contentType": "text/plain",
           "id": 9150, ← ファイルに付与された ID
           "image": false,
           "length": 1056,
           "lengthText": "1.1 KB",
           "name": "README.txt", ← ファイル名
           "processDataInstanceId": 9143 ← 各プロセスの各データ項目に付与された ID
         }
       ],
       "viewOrder": 2
     }
   },
   ...,
   "processInstanceId": 183,
   ...,
   "processInstanceTitle": "テストです",
   ...,
   "processModelInfoName": "テスト用: ファイルダウンロード",
   ...
 },
 ...
]

データ項目を取得したい場合、criteriaで取得対象データのデータ項目番号を指定する必要があります。「Workflow API を用いてプロセスやタスク一覧をダウンロードする」の手順に沿ってcriteriaを取得する場合、Web UIの検索画面で表示項目エリアを開き、ファイル型データ項目にチェックを入れた状態で「更新」ボタンをクリックし、最新のcriteriaを取得してください(データ項目に一つもチェックが入っていない状態で取得したcriteriaを使用すると、”data”:nullの結果が返されます)。

データ項目のオブジェクトは、データ項目番号をキーとしたマップに格納されています。

dataType が FILE2 であるオブジェクトがファイル型データ項目を表しています。ques-kun-01.png と README.txt がアップロードされたプロセスだということがわかりますね。id と processDataInstanceId もこのオブジェクトに格納されています。JSON をパースしてこの 2 項目を抽出すれば、API アクセスのためのパラメータが揃います。

ファイルダウンロード API にアクセスする

API エンドポイントに id, processDataInstanceId の組を送信することで、該当するファイルをダウンロードできます。必要なパラメータが違うだけで、プロセス検索 API と同じようにアクセスできます。API レスポンスはファイルのバイナリデータなので、適宜保存してください。

以下にプロセス検索からファイルダウンロードまでを行うサンプルコードを掲載します。シェルスクリプトと Python の 2 例です。criteria(検索条件)に一致するプロセスでアップロードされたファイルをすべてダウンロードするスクリプトです。ファイルの保存ディレクトリはプロセス毎に分けています。

ディレクトリ名は {プロセス ID}_{アプリ名}_({件名})

プロセスへの添付ファイルの一括ダウンロードが必要になった際、以下のコードを参考にしてみてください!

シェルスクリプト コード例

#!/bin/sh
# プロセス検索のためのスクリプト
# -u: Basic認証オプション
# パラメータ...criteria: 検索条件XML, start: 検索結果の取得開始位置, limit: 最大取得件数
process_search=$(curl -u {メールアドレス}:{Basicパスワード}\
                'https://example.questetra.net/API/OR/ProcessInstance/list'\
                --data-urlencode criteria@process-criteria.txt\
                --data-urlencode 'start=0'\
                --data-urlencode 'limit=10')
 
# プロセス検索APIから返ってきたJSONから、ファイルが添付されているプロセスデータだけ切り出す
# tr -d '[:cntrl:]'は制御文字の除去
processes=$(echo $process_search | tr -d '[:cntrl:]' | jq '.processInstances[]' | jq -s)
# forループのために要素数を取得
processes_len=$(echo $processes | jq length)
 
# 各プロセスについて
for i in $(seq 0 $(($processes_len-1)))
do
   # ファイルが添付されているかどうか確認
   # 各dataについて、dataTypeがFILE2かつvalueがnullでないもの(ファイルがアップロードされているファイル型項目)を抽出する
   data=$(echo $processes | jq .[$i].data[] | jq 'select(.dataType == "FILE2" and .value != null)' | jq -s)
   # forループのために要素数を取得
   data_len=$(echo $data | jq length)
   # 要素数が0
   if [ $data_len -eq 0 ] ; then
       continue
   fi
 
   # プロセス情報からディレクトリ名を作成
   process_id=$(echo $processes | jq .[$i].processInstanceId)
   # 文字列の前後に付いたダブルクォートをsedで除去
   app_name=$(echo $processes | jq .[$i].processModelInfoName | sed -e "s/\"\$//" | sed -e "s/^\"//")
   process_title=$(echo $processes | jq .[$i].processInstanceTitle | sed -e "s/\"\$//" | sed -e "s/^\"//")
   directory_name=$process_id'_'$process_title'_('$app_name')'
   # ディレクトリが存在しなければ作成する
   mkdir -p "./file/$directory_name"
 
   # 各ファイル型項目について
   for j in $(seq 0 $(($data_len-1)))
   do
       # アップロードされたファイルの情報を抽出
       files=$(echo $data | jq .[$j].value[] | jq -s)
       # forループのために要素数を取得
       files_len=$(echo $files | jq length)
       # 各ファイルについて
       for k in $(seq 0 $(($files_len-1)))
       do
           # ファイルID
           file_id=$(echo $files | jq .[$k].id)
           # プロセスデータID
           process_data_id=$(echo $files | jq .[$k].processDataInstanceId)
           # ファイル名
           file_name=$(echo $files | jq .[$k].name | sed -e "s/\"\$//" | sed -e "s/^\"//")
           # ファイルの保存先
           file_path='./file/'$directory_name'/'$file_name
           # ファイルダウンロードのためのスクリプト
           # > でつないで標準出力をファイル保存(上書き)
           file_download=$(curl -u {メールアドレス}:{Basicパスワード}\
                           'https://example.questetra.net/API/OR/ProcessInstance/File/download'\
                           --data-urlencode 'id='$file_id\
                           --data-urlencode 'processDataInstanceId='$process_data_id > $file_path)
           echo $file_download
       done
   done
done

Python コード例

import os
import requests
import json
 
if __name__ == "__main__":
   # APIエンドポイント
   process_search_url = 'https://example.questetra.net/API/OR/ProcessInstance/list'
   file_download_url = 'https://example.questetra.net/API/OR/ProcessInstance/File/download'
 
   # Basic認証情報
   auth = ("{メールアドレス}", "{Basicパスワード}")
 
   # criteriaを読み込み
   with open('./process-criteria.txt', 'r') as f:
       criteria = f.read()
 
   # プロセス検索のためのパラメータ
   search_params = {
       'criteria': criteria,
       'start': 0,
       'limit': 10,
   }
 
   try:
       # プロセス検索実行
       r_search = requests.post(process_search_url, data=search_params, auth=auth) # POST送信
       r_search.raise_for_status()
   except requests.exceptions.HTTPError as e:  # HTTP エラーをキャッチ
       print('Error')
       print('Status Code: {0}'.format(r_search.status_code))
       print(r_search.text)
   except requests.exceptions.Timeout as e:  # タイムアウトをキャッチ
       print('Timeout')
 
   # プロセス検索APIが返したJSONをロード
   processes = json.loads(r_search.text)['processInstances']
   # 各プロセスについて
   for process in processes:
       # 各データ項目について
       for data in process['data'].values():
           # ファイル型でないか、アップロードされたファイルがなければcontinue
           if data['dataType'] != 'FILE2' or data['value'] is None:
               continue
           # ディレクトリ名を作成
           directory = './file_py/{0}_{1}_({2})'.format(process['processInstanceId'],
                                                        process['processModelInfoName'],
                                                        process['processInstanceTitle'])
           # ディレクトリが存在しなければ作成
           os.makedirs(directory, exist_ok=True)
           # 各ファイルについて
           for file_info in data['value']:
               # ファイルダウンロードのためのパラメータ
               download_params = {
                   'id': file_info['id'],
                   'processDataInstanceId': file_info['processDataInstanceId']
               }
 
               try:
                   # ファイルダウンロード実行
                   r_download = requests.post(file_download_url, data=download_params, auth=auth)
                   r_download.raise_for_status()
               except requests.exceptions.HTTPError as e:  # HTTP エラーをキャッチ
                   print('Error')
                   print('Status Code: {0}'.format(r_search.status_code))
                   print(r_search.text)
               except requests.exceptions.Timeout as e:  # タイムアウトをキャッチ
                   print('Timeout')
 
               # ファイル書込み(バイナリモードでopenすること)
               with open(directory + '/' + file_info['name'], 'wb') as f:
                   f.write(r_download.content)
%d