REST APIでRedmineアクセス(4)
2025/06/15
最後は添付ファイル
ファイルをWikiへ添付
  • 添付ファイルは 公式ページ で書かれているように、一度 uploads.json にデータをアップロードし、その token を取得してから、Wiki または Issue に登録する流れです。

  • 下記は、プロジェクト'Sandbox' の Wiki 'Sub_page_0' です。(今Sandboxのtypoに気付きましたが、もうこのままで...)


  • 上記Wikiにファイル 'file\attached_file_0.png' を添付するコードは下記になります。
    • Fileアップロード時の headers で 'Content-Type' に 'application/octet-stream' 指定します
      • token取得のアップロード時には、ファイルタイプに関係なく application/octet-stream でOKです
    • バイナリモードで read したファイルデータを post します
      • post時のパラメータ名で {'filename': 'ファイル名'} を与えると redmine\files 内で見やすくなります
        • 設定必須ではありません
    • レスポンスの jsonデータから uploadファイルの token を取得します
      • (注)取得した token は一時的な値です(ほぼ使い捨て)
    • 添付先Wikiデータの 'uploads' に token, filename, content_type 情報を設定します
      • この時 png, txt, xlsx等ファイルタイプに応じた content_type の指定が必要です
    • 変更したWikiデータを put します
    import requests
    import json
    import os
    
    # ==============================================================================
    # File Upload
    # ==============================================================================
    # UploadのURL
    strUrlUpl = 'http://localhost:3000/uploads.json'
    
    # Upload時のrequests headers
    dicHeadUpl = {
        'X-Redmine-API-Key': '8dc1f63aeうんぬん',
        'Content-Type': 'application/octet-stream'
    }
    # Upload時のrequests params
    strPath = r'file\attached_file_0.png' # ファイルフルパス
    strFile = os.path.basename(strPath)   # ファイルbasename
    dicParamUpl = {
        'filename': strFile
    }
    
    # FileをBinaryデータとして読む
    with open(strFile, 'rb') as fh:
        binData = fh.read()
    
    # requests post実行
    resUpload = requests.post(strUrlUpl, headers=dicHeadUpl, params=dicParamUpl, data=binData)
    print(f"request_upload_status=[{resUpload.status_code}]")
    
    # token情報json部をdicに変換
    dicUpload = resUpload.json()
    jsoUpload = json.dumps(dicUpload, indent=2, ensure_ascii=False)
    print(f"Upload情報:\n{jsoUpload}")
    
    # File token取得
    strToken = dicUpload['upload']['token']
    
    # ==============================================================================
    # Wiki操作
    # ==============================================================================
    # WikiページのURL
    strProjId = 'sandbox'
    strWikiId = 'Sub_page_0'
    strUrl = f"http://localhost:3000/projects/{strProjId}/wiki/{strWikiId}.json"
    
    # Wiki操作時のヘッダ
    dicHead = {
        'X-Redmine-API-Key': '8dc1f63aeうんぬん',
        'Content-Type': 'application/json'
    }
    
    # requests get実行
    resWiki = requests.get(strUrl, headers=dicHead)
    print(f"request_get_status=[{resWiki.status_code}]")
    
    # Wikiデータ取得
    dicWiki=resWiki.json()
    
    # wikiのdicデータにuploadsデータ追加(ファイル毎にtoken,filename,content_type設定)
    dicWiki['wiki_page']['uploads'] = [
        {
            'token': strToken,
            'filename': strFile,
            'content_type': 'image/png'
        }
    ]
    # 変更後表示
    jsoWikiDisp = json.dumps(dicWiki, indent=2, ensure_ascii=False)
    print(f"変更後Wikiデータ:\n{jsoWikiDisp}")
    
    # dicデータをjson(requests put用)に変換
    jsoWiki = json.dumps(dicWiki)
    
    # requests put実行
    resWikiUpload = requests.put(strUrl, headers=dicHead, data=jsoWiki)
    print(f"request_put_status=[{resWikiUpload.status_code}]")
    
    >test6.py
    request_upload_status=[201]
    Upload情報:
    {
      "upload": {
        "id": 15,
        "token": "15.eff57e08dcbe28bca461c78b08962de04a99fbcb9c4d181ca4d29e7da42dcdc6"
      }
    }
    request_get_status=[200]
    変更後Wikiデータ:
    {
      "wiki_page": {
        "title": "Sub_page_0",
        "parent": {
          "title": "Wiki"
        },
        "text": "# Project: Sandobox: Sub wiki page [0]\r\n* サブWikiページ[0]\r\n* REST APIで追加した記述",
        "version": 4,
        "author": {
          "id": 5,
          "name": "Altmo Test"
        },
        "comments": "",
        "created_on": "2025-06-13T04:36:33Z",
        "updated_on": "2025-06-14T15:25:25Z",
        "uploads": [
          {
            "token": "15.eff57e08dcbe28bca461c78b08962de04a99fbcb9c4d181ca4d29e7da42dcdc6",
            "filename": "attached_file_0.png",
            "content_type": "image/png"
          }
        ]
      }
    }
    request_put_status=[204]
  • 添付ファイルが追加されました。既存ではなく新規Wikiページ作成時は、'text'値がダミーでも必要な点は注意下さい。


  • 尚、Wiki(次のIssueも含めて) 'uploads'でファイル毎に指定する 'content_type' はタイプに合わせた指定が必要です。

ファイルをIssueへ添付
  • チケット(Issue)への添付ファイル追加は、編集中のコメント(journal)でのみ可能です(手編集の場合もそうですね)。

  • 後はWikiの場合と同じです。下記は30番のIssueにファイルを添付する例です。
  • import requests
    import json
    import os
    
    # ==============================================================================
    # File Upload
    # ==============================================================================
    # UploadのURL
    strUrlUpload = 'http://localhost:3000/uploads.json'
    
    # Upload時のrequests header
    dicHeadUpload = {
        'X-Redmine-API-Key': '8dc1f63aeうんぬん',
        'Content-Type': 'application/octet-stream'
    }
    # Upload時のrequests params
    strPath = r'file\attached_file_1.png'
    strFile = os.basename(strPath)
    dicParamUpl = {
        'filename': strFile
    }
    
    # FileをBinaryデータとして読む
    with = open(strPath, 'rb') as fh:
        binData = fh.read()
    
    # requests post実行
    resUpload = requests.post(strUrlUpload, headers=dicHeadUpload, data=binData)
    print(f"request_upload_status=[{resUpload.status_code}]")
    
    # token情報json部をdicに変換
    dicUpload = resUpload.json()
    jsoUpload = json.dumps(dicUpload, indent=2, ensure_ascii=False)
    print(f"Upload情報:\n{jsoUpload}")
    
    # File token取得
    strToken = dicUpload['upload']['token']
    
    # ==============================================================================
    # Issue操作
    # ==============================================================================
    # IssueのURL
    strIssueId = '30'
    strUrl = f"http://localhost:3000/issues/{strIssueId}.json"
    
    # Issue操作時のヘッダ
    dicHead = {
        'X-Redmine-API-Key': '8dc1f63aeうんぬん',
        'Content-Type': 'application/json'
    }
    
    # 追加コメント(journal)データ
    dicJournal = {
        'issue': {
            'notes': '追加コメント',
            'uploads': [
                {
                    'token': strToken,
                    'filename': strFile,
                    'content_type': 'image/png'
                }
            ]
        }
    }
    
    # dicデータをjson(requests put用)に変換
    jsoJournal = json.dumps(dicJournal)
    
    # requests put実行
    resJournal = requests.put(strUrl, headers=dicHead, data=jsoJournal)
    print(f"request_put_status=[{resJournal.status_code}]")
    
    >test7.py
    request_upload_status=[201]
    Upload情報:
    {
      "upload": {
        "id": 14,
        "token": "14.025a008fa4218066d2440663f364fdcac70e5a04c58a7d39cee4431d669ea25d"
      }
    }
    request_put_status=[204]
    
  • 添付ファイルが追加されました。

添付ファイルを削除
  • WikiまたはIssueの情報を取得する際に、'include' パラメータで 'attachments' を指定すると、参照している添付ファイルの情報を得ることができます。

  • レスポンスの 'attachments' 内にある 'id' からURLを特定できます。例えば id=12 ならば 'attachments/12.json' です。
  • import requests
    import json
    
    # ==============================================================================
    # Wiki情報取得
    # ==============================================================================
    # WikiページのURL
    strProjId = 'sandbox'
    strWikiId = 'Sub_page_0'
    strUrl = f"http://localhost:3000/projects/{strProjId}/wiki/{strWikiId}.json"
    
    # Wiki操作時のヘッダ
    dicHead = {
        'X-Redmine-API-Key': '8dc1f63aeうんぬん',
        'Content-Type': 'application/json'
    }
    dicParam = {
        'include': 'attachments'
    }
    
    # requests get実行
    resWiki = requests.get(strUrl, headers=dicHead, params=dicParam)
    print(f"request_get_status=[{resWiki.status_code}]")
    
    # Wikiデータ取得
    dicWiki=resWiki.json()
    
    # 表示用整形
    jsoWiki = json.dumps(dicWiki, indent=2, ensure_ascii=False)
    print(jsoWiki)
    
    # 対象添付ファイルのURL取得(今回は一番最初の添付ファイル)
    strDelId   = dicWiki['wiki_page']['attachments'][0]['id']
    strUrlDel = f"http://localhost:3000/attachments/{strDelId}.json"
    print(f"file_url:[{strUrlDel}]")
    
    # delete実行
    resDel = requests.delete(strUrlDel, headers=dicHead)
    print(f"request_delete_status=[{resDel.status_code}]")
    
    >test8.py
    request_get_status=[200]
    {
      "wiki_page": {
        "title": "Sub_page_0",
        "parent": {
          "title": "Wiki"
        },
        "text": "# Project: Sandobox: Sub wiki page [0]\r\n* サブWikiページ[0]\r\n* REST APIで追加した記述",
        "version": 4,
        "author": {
          "id": 5,
          "name": "Altmo Test"
        },
        "comments": "",
        "created_on": "2025-06-13T04:36:33Z",
        "updated_on": "2025-06-14T15:25:25Z",
        "attachments": [
          {
            "id": 12,
            "filename": "attached_file_0.png",
            "filesize": 1300,
            "content_type": "image/png",
            "description": "",
            "content_url": "http://localhost:3000/attachments/download/12/attached_file_0.png",
            "author": {
              "id": 5,
              "name": "Altmo Test"
            },
            "created_on": "2025-06-14T16:55:11Z"
          }
        ]
      }
    }
    file_url:[http://localhost:3000/attachments/12.json]
    request_delete_status=[204]

添付ファイルのcontent_type
  • 添付ファイルのtokenをWiki/Issueへ結びつける際に指定が必要な content_type ですが、これはMIMEタイプのことなので、mimetypesライブラリを使用すれば簡単に取得できます
  • import mimetypes
    
    # MIMEタイプ判定するファイルのリスト
    lstFiles = [
        'test0.txt', 'test1.xlsx', 'test2.pdf',
        'test3.pptx', 'test4.png', 'test5.jpg', 'test6.zip'
    ]
    
    for strFile in lstFiles:
        strMime, strEncode = mimetypes.guess_type(strFile) # タプルの[0]がMIMEタイプ
        print(f"{strFile}:\t{strMime}")
    
    >test9.py
    test0.txt:      text/plain
    test1.xlsx:     application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
    test2.pdf:      application/pdf
    test3.pptx:     application/vnd.openxmlformats-officedocument.presentationml.presentation
    test4.png:      image/png
    test5.jpg:      image/jpeg
    test6.zip:      application/x-zip-compressed
    

最後に
  • RedmineのREST APIの基本操作を一通り眺めました。公式ドキュメントがフワフワしているせいか、おかしな記述がWeb上で散乱しており、Copilot等に聞いても正しい回答へなかなかたどり着けません。

  • そのため調べて/試して/まとめる作業が必要なのはRedmineあるあるですが、もう少し何とかして欲しいなと思っています。
Copyright(C) 2025 Altmo
本HPについて