Skip to content

ファイルのみのアップロード・大容量ファイル ​

第7版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

/api/binaries/upload を使うと、Base64 に変換せず multipart/form-data でファイルだけをアップロードし、その GUID を受け取れます。受け取った GUID は、レコード作成時の AttachmentsHash や、「内容」「説明項目」への画像埋め込みに使えます。このエンドポイントは公式マニュアルには載っておらず、編集画面で添付ファイル項目にアップロードするときの実装を流用するものです(実装は BinariesController.cs)。

使いどころ ​

  • 添付ファイルを Base64 で送りたくない場合: Base64 だと転送量が約 1.33 倍になり、エンコードの CPU 負荷やメモリ消費もかかります。
  • 「内容」「説明項目」に画像を入れたい場合: API の ImageHash は 1 項目につき一度に 1 つの画像しか送れず扱いにくいため、先にファイルだけアップロードして GUID を埋め込みます。

エンドポイント ​

エンドポイントメソッド用途
/api/binaries/upload?id={SiteId}POST新規アップロード(レコード未作成時)
/api/binaries/{guid}/uploadPOST既存添付ファイルの更新

サブディレクトリ配置

上記はルート配置の場合のパスです。サブディレクトリ配置(例: Pleasanter.net)ではプレフィックスが付きます(例: /fs/api/binaries/upload)。URL を組み立てるときは baseUrl にサブディレクトリを含めてください。

id に何を渡すか

確認したソースでは、新規アップロードの id はファイル(Binaries テーブルの行)の ReferenceId としてそのまま保存されます(BinariesController.cs)。id が無いか 0 のときは 400 になりますが、アップロード時点ではその ID への権限チェックはありません(L199-L203)。

  • レコード作成前にアップロードする場合: id に SiteId を渡します。レコード作成時に AttachmentsHash で同じ Guid を Added: true で指定すると、ReferenceId は作成したレコードの ID に書き換わります(Attachment.cs)。このページはこの手順を基本にしています。
  • 既存レコードに画像を入れる場合: id にレコードの ID(ResultId・IssueId・WikiId)を渡してもかまいません。

ファイルを表示・ダウンロードするときは、ReferenceId の ID に対する読み取り権限で判定されます(FileContentResults.cs)。

認証 ​

Authorization ヘッダーに Bearer {ApiKey} の形式で API キーを指定します。

http
POST /api/binaries/upload?id=123 HTTP/1.1
Host: example.pleasanter.org
Authorization: Bearer your-api-key-here
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...

WARNING

一般的な OAuth2 のようなアクセストークンではなく、API キーをそのまま指定します。API キーは「ユーザー設定 > API 設定」から取得できます。

1 リクエスト = 1 ファイル ​

1 回のリクエストで受け付けるファイルは 1 つです(2 つ以上送っても先頭以外は無視されます)。複数ファイルはファイルごとにリクエストし、それぞれの GUID を取得します。

レスポンス ​

json
{
  "Id": 123,
  "StatusCode": 200,
  "Message": "550E8400E29B41D4A716446655440000"
}
キー内容
Id新規アップロードの完了時は登録したファイルの BinaryId、チャンク転送の途中は 0。/api/binaries/{guid}/upload の完了時は更新したレコードの ID
StatusCode処理結果のステータスコード
Message成功時はアップロードしたファイルの GUID(ハイフンなし大文字 32 文字)、エラー時はエラーメッセージ

INFO

確認したソースでは、新規アップロードの完了時は Binaries テーブルに INSERT した行の ID(BinaryId)と GUID を返します(Attachment.cs)。チャンク転送の途中は Id: 0 と GUID を返します(BinariesController.cs)。

StatusCode と Message ​

StatusCode意味Message(日本語設定時の例)
200成功アップロードされたファイルの GUID
400不正なリクエスト要求が不正です。 または JSONデータが不正です。
401認証エラー認証できませんでした。
403権限エラーこの操作を行うための権限がありません。
404対象が見つからない指定された情報は見つかりませんでした。
405ロック中レコード 123 は ○○ が 2025/01/15 10:30 にロックしました。
429API 制限超過ID: 456 のサイトで利用できるAPIの制限(1000 件/日)を超えました。
442ファイルサイズ超過制限容量10Mbyteを超えているファイルがあります。
443合計サイズ超過添付可能な容量は100Mbyteまでです。
500サーバーエラーエラー内容に応じたメッセージ

Message はサーバーの言語設定によって変わります。

HTTP ステータスとボディの StatusCode を両方見る

HTTP ヘッダーは 200 なのにボディの StatusCode がエラー、ということがあります。次の組み合わせが確認されています。

ヘッダーボディの StatusCode状況
200400id がセットされていないか 0
200401認証ヘッダーの API キーが不正
404—ファイルがコンテンツに含まれない
404—コンテンツタイプの MIME が不正
500—リクエスト内容を解釈できなかった

Base64 方式との比較 ​

項目Base64 方式Multipart 方式
Content-Typeapplication/jsonmultipart/form-data
ファイルデータBase64 エンコード必須バイナリのまま送信
転送量100%(基準)約 75%
メモリ使用量ファイル全体 + エンコード後ストリーム可能
GUID 管理サーバー自動生成サーバー自動生成(レスポンスで取得)
チャンク転送未対応Content-Range で対応
整合性検証未対応FileHash で検証可能
デバッグJSON なので可読性ありバイナリなので確認しにくい

Base64 は 3 バイトを 4 文字に変換するため、データ量が約 1.33 倍になります。

登録フローの違い ​

Base64 方式は 1 回の API 呼び出しでレコード作成と添付ファイル登録を同時に行えます。Multipart 方式は、ファイルアップロードで GUID を取得した後、レコード作成時にその GUID を指定する 2 段階の処理になります。

Base64 方式(1 回の API 呼び出し):

図を読み込み中…

Multipart 方式(2 回の API 呼び出し):

図を読み込み中…

Multipart 方式は呼び出しが 2 回になりますが、大容量ファイルではエンコードのオーバーヘッドを避けられるため、全体の処理時間は短くなります。

基本的な使い方 ​

新規ファイルをアップロードする ​

C#:

csharp
using System.Net.Http.Json;
using System.Text.Json;

public async Task<string> UploadFileAsync(
    string baseUrl, string apiKey, long siteId, string filePath)
{
    using var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = 
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
    
    using var content = new MultipartFormDataContent();
    using var fileStream = File.OpenRead(filePath);
    content.Add(new StreamContent(fileStream), "file", Path.GetFileName(filePath));
    
    var response = await client.PostAsync(
        $"{baseUrl}/api/binaries/upload?id={siteId}", content);
    response.EnsureSuccessStatusCode();
    
    var result = await response.Content.ReadFromJsonAsync<JsonElement>();
    return result.GetProperty("Message").GetString()!; // アップロードされたファイルのGUID
}

Python:

python
import requests
import os

def upload_file(base_url: str, api_key: str, site_id: int, file_path: str) -> str:
    url = f"{base_url}/api/binaries/upload?id={site_id}"
    headers = {"Authorization": f"Bearer {api_key}"}
    
    with open(file_path, 'rb') as f:
        files = {'file': (os.path.basename(file_path), f)}
        response = requests.post(url, headers=headers, files=files)
    
    response.raise_for_status()
    return response.json()['Message']  # アップロードされたファイルのGUID

フォームのフィールド名

サーバーはフィールド名を見ずに、送られたファイルの先頭 1 つを使います(BinariesController.cs、L115)。このページの例は file にしていますが、upfile など別の名前でも動作します。

日本語ファイル名

C# の HttpClient、Python の requests、curl は日本語ファイル名を RFC 5987 形式で自動エンコードし、プリザンター側も ASP.NET Core の IFormFile.FileName で自動解析するため、特別な対応は不要です。

レコード作成時に添付ファイルとして関連付ける ​

アップロードで得た GUID を AttachmentsHash に Guid として指定し、Added: true を付けます。

C#:

csharp
public async Task<long> CreateRecordWithFileAsync(
    string baseUrl, string apiKey, long siteId, string title, string filePath)
{
    // Step 1: ファイルをアップロード(GUIDを取得)
    var guid = await UploadFileAsync(baseUrl, apiKey, siteId, filePath);
    
    // Step 2: レコード作成時にAttachmentsHashで関連付け
    using var client = new HttpClient();
    var payload = new
    {
        ApiKey = apiKey,
        Title = title,
        AttachmentsHash = new
        {
            AttachmentsA = new[]
            {
                new
                {
                    Guid = guid,
                    Name = Path.GetFileName(filePath),
                    Added = true
                }
            }
        }
    };
    
    var content = new StringContent(
        JsonSerializer.Serialize(payload),
        System.Text.Encoding.UTF8,
        "application/json");
    
    var response = await client.PostAsync(
        $"{baseUrl}/api/items/{siteId}/create", content);
    response.EnsureSuccessStatusCode();
    
    var result = await response.Content.ReadFromJsonAsync<JsonElement>();
    return result.GetProperty("Id").GetInt64();
}

Python:

python
def create_record_with_file(
    base_url: str, api_key: str, site_id: int, title: str, file_path: str) -> int:
    # Step 1: ファイルをアップロード(GUIDを取得)
    guid = upload_file(base_url, api_key, site_id, file_path)
    
    # Step 2: レコード作成時にAttachmentsHashで関連付け
    payload = {
        "ApiKey": api_key,
        "Title": title,
        "AttachmentsHash": {
            "AttachmentsA": [{
                "Guid": guid,
                "Name": os.path.basename(file_path),
                "Added": True
            }]
        }
    }
    
    response = requests.post(
        f"{base_url}/api/items/{site_id}/create",
        headers={"Content-Type": "application/json"},
        json=payload
    )
    response.raise_for_status()
    return response.json()['Id']

「内容」「説明項目」に画像を埋め込む ​

アップロードのレスポンスで得た GUID を使い、Body や Description* に ![image](/binaries/{Guid}/show) という文字列をセットすると画像が埋め込まれます。サブディレクトリで運用している場合はパスを合わせて書き換えてください。文章と画像を混ぜることが多い Wiki などで扱いが楽になります。

既存の添付ファイルを上書きする ​

既存ファイルの GUID を URL に入れ、?overwrite=true を付けてアップロードします。サーバーは GUID からそのファイルが添付されているレコードを探し、そのレコードを更新する形でファイルを差し替えます。レスポンスはレコード更新 API と同じ形になり、Id はレコードの ID、Message は更新完了のメッセージです(GUID ではありません)。

python
def update_file(
    base_url: str, api_key: str, existing_guid: str, file_path: str) -> str:
    url = f"{base_url}/api/binaries/{existing_guid}/upload?overwrite=true"
    headers = {"Authorization": f"Bearer {api_key}"}
    
    with open(file_path, 'rb') as f:
        files = {'file': (os.path.basename(file_path), f)}
        response = requests.post(url, headers=headers, files=files)
    
    response.raise_for_status()
    return existing_guid  # 上書きでは GUID は変わらない

WARNING

上書きには ?overwrite=true が必須です。確認したソースでは、付けないと新しい GUID のファイルとして同じレコードの同じ添付ファイル項目に追加されます(項目の「同じファイル名で上書き」が有効なときは元のファイルが削除扱いになります)。overwrite=true のときは元の GUID のまま内容が置き換わります(BinariesController.cs、ResultModel.cs)。

大容量ファイルのチャンク転送 ​

Content-Range を付けると、大きなファイルを分割して送れます。Content-Range は HTTP リクエストのヘッダーではなく、multipart のファイルパートのヘッダーに付けます(サーバーは Request.Form.Files[0].Headers.ContentRange を読みます。BinariesController.cs)。

  • 1 回目のチャンク(開始位置 0)で GUID が発行され、以降のチャンクでも同じ GUID が返ります。最後のチャンクを受け取った時点でファイルが登録されます(L119-L122)。
  • チャンクは先頭から順番に送ります。開始位置がサーバー側で受信済みのサイズと一致しないと 400(InvalidRequest)になるため、並列には送れません(L330-L338)。

WARNING

確認したソースでは、新規アップロードのサイズ上限は Parameters/BinaryStorage.json の MaxSize(MB 単位、既定 50)で判定され、チャンク転送でもこの制限は超えられません(BinaryValidators.cs)。Web サーバー側のリクエストサイズ上限は 1 チャンクごとにかかります。

python
def upload_large_file(
    base_url: str, api_key: str, site_id: int, file_path: str,
    chunk_size: int = 10 * 1024 * 1024) -> str:  # デフォルト10MB
    url = f"{base_url}/api/binaries/upload?id={site_id}"
    headers = {"Authorization": f"Bearer {api_key}"}
    file_name = os.path.basename(file_path)
    file_size = os.path.getsize(file_path)
    guid = None
    
    with open(file_path, 'rb') as f:
        offset = 0
        while offset < file_size:
            chunk = f.read(chunk_size)
            end = offset + len(chunk) - 1
            
            # Content-Range はファイルパートのヘッダーに付ける
            part_headers = {'Content-Range': f'bytes {offset}-{end}/{file_size}'}
            files = {'file': (file_name, chunk, 'application/octet-stream', part_headers)}
            response = requests.post(url, headers=headers, files=files)
            response.raise_for_status()
            
            guid = response.json().get('Message')
            progress = (end + 1) * 100 // file_size
            print(f"進捗: {progress}% ({end + 1}/{file_size} bytes)")
            
            offset += len(chunk)
    
    return guid
C# 版
csharp
public async Task<string> UploadLargeFileAsync(
    string baseUrl, string apiKey, long siteId, string filePath, 
    int chunkSize = 10 * 1024 * 1024) // デフォルト10MB
{
    using var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = 
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
    
    var fileInfo = new FileInfo(filePath);
    var fileSize = fileInfo.Length;
    var fileName = fileInfo.Name;
    string? guid = null;
    
    using var fileStream = File.OpenRead(filePath);
    var buffer = new byte[chunkSize];
    long offset = 0;
    
    while (offset < fileSize)
    {
        var bytesRead = await fileStream.ReadAsync(buffer);
        var end = offset + bytesRead - 1;
        
        using var content = new MultipartFormDataContent();
        var part = new ByteArrayContent(buffer, 0, bytesRead);
        // Content-Range はファイルパートのヘッダーに付ける
        part.Headers.ContentRange =
            new System.Net.Http.Headers.ContentRangeHeaderValue(offset, end, fileSize);
        content.Add(part, "file", fileName);

        var response = await client.PostAsync(
            $"{baseUrl}/api/binaries/upload?id={siteId}", content);
        response.EnsureSuccessStatusCode();
        
        var result = await response.Content.ReadFromJsonAsync<JsonElement>();
        guid = result.GetProperty("Message").GetString();
        
        var progress = (end + 1) * 100 / fileSize;
        Console.WriteLine($"進捗: {progress}% ({end + 1}/{fileSize} bytes)");
        
        offset += bytesRead;
    }
    
    return guid!;
}

ファイルの整合性検証(FileHash) ​

フォームに FileHash を付けて送ると、転送中のデータ破損を検出できます。確認したソースでは、ハッシュは MD5 を小文字 16 進で表した文字列で、最後のチャンクを受け取った時点でファイル全体と比較されます。一致しない場合は 400(InvalidRequest)が返ります(BinaryUtilities.cs、L1160-L1164)。

FileHash を使うときは、1 回で送る場合もファイルパートに Content-Range を付けてください。Content-Range が無いと判定処理が範囲情報を参照できず、サーバーエラーになります(L1129)。ハッシュには SHA-256 ではなく MD5 を使います。

python
import hashlib

def upload_with_hash(
    base_url: str, api_key: str, site_id: int, file_path: str) -> str:
    # MD5ハッシュ(小文字16進)を計算
    with open(file_path, 'rb') as f:
        file_hash = hashlib.md5(f.read()).hexdigest()

    url = f"{base_url}/api/binaries/upload?id={site_id}"
    headers = {"Authorization": f"Bearer {api_key}"}
    file_size = os.path.getsize(file_path)
    # FileHash の検証には Content-Range が必要
    part_headers = {'Content-Range': f'bytes 0-{file_size - 1}/{file_size}'}

    with open(file_path, 'rb') as f:
        files = {'file': (os.path.basename(file_path), f,
                          'application/octet-stream', part_headers)}
        data = {'FileHash': file_hash}
        response = requests.post(url, headers=headers, files=files, data=data)
    
    response.raise_for_status()
    return response.json()['Message']

複数ファイルのアップロード ​

1 リクエスト 1 ファイルなので、複数ファイルは並列実行で効率化します。

python
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict

def batch_upload(
    base_url: str, api_key: str, site_id: int, 
    file_paths: List[str], max_workers: int = 3) -> Dict[str, str]:
    results = {}
    
    def upload_single(file_path: str) -> tuple:
        guid = upload_file(base_url, api_key, site_id, file_path)
        return os.path.basename(file_path), guid
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {executor.submit(upload_single, fp): fp for fp in file_paths}
        
        for future in as_completed(futures):
            file_name, guid = future.result()
            results[file_name] = guid
            print(f"完了: {file_name} -> {guid}")
    
    return results

C# では SemaphoreSlim で同時実行数を制限します。

C# 版
csharp
public async Task<Dictionary<string, string>> BatchUploadAsync(
    string baseUrl, string apiKey, long siteId, IEnumerable<string> filePaths, 
    int maxConcurrency = 3)
{
    var results = new Dictionary<string, string>();
    var semaphore = new SemaphoreSlim(maxConcurrency);
    
    var tasks = filePaths.Select(async filePath =>
    {
        await semaphore.WaitAsync();
        try
        {
            var guid = await UploadFileAsync(baseUrl, apiKey, siteId, filePath);
            lock (results)
            {
                results[Path.GetFileName(filePath)] = guid;
            }
            Console.WriteLine($"完了: {Path.GetFileName(filePath)} -> {guid}");
        }
        finally
        {
            semaphore.Release();
        }
    });
    
    await Task.WhenAll(tasks);
    return results;
}

// 使用例
var files = new[] { "doc1.pdf", "doc2.pdf", "doc3.pdf", "image.png" };
var results = await BatchUploadAsync(baseUrl, apiKey, siteId, files);

Console.WriteLine($"\nアップロード結果: {results.Count}件");
foreach (var (name, guid) in results)
{
    Console.WriteLine($"  {name}: {guid}");
}

関連ページ ​

変更履歴

第7版記事の確認版を繰り返す表現を整理する
第6版本文から元記事や以前の版への言及を除き、正しい動作だけを書く形に整理
第5版「拡張機能」「API」を 1.5.8.1 のソースで検証して修正
第4版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第3版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第2版API にクエリパラメータで独自データを渡す方法を追加し、ファイルアップロードの流れを図で説明
第1版「拡張機能」「API」セクションの記事を追加