ファイルのみのアップロード・大容量ファイル
/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}/upload | POST | 既存添付ファイルの更新 |
サブディレクトリ配置
上記はルート配置の場合のパスです。サブディレクトリ配置(例: 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 キーを指定します。
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 を取得します。
レスポンス
{
"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 にロックしました。 |
| 429 | API 制限超過 | ID: 456 のサイトで利用できるAPIの制限(1000 件/日)を超えました。 |
| 442 | ファイルサイズ超過 | 制限容量10Mbyteを超えているファイルがあります。 |
| 443 | 合計サイズ超過 | 添付可能な容量は100Mbyteまでです。 |
| 500 | サーバーエラー | エラー内容に応じたメッセージ |
Message はサーバーの言語設定によって変わります。
HTTP ステータスとボディの StatusCode を両方見る
HTTP ヘッダーは 200 なのにボディの StatusCode がエラー、ということがあります。次の組み合わせが確認されています。
| ヘッダー | ボディの StatusCode | 状況 |
|---|---|---|
| 200 | 400 | id がセットされていないか 0 |
| 200 | 401 | 認証ヘッダーの API キーが不正 |
| 404 | — | ファイルがコンテンツに含まれない |
| 404 | — | コンテンツタイプの MIME が不正 |
| 500 | — | リクエスト内容を解釈できなかった |
Base64 方式との比較
| 項目 | Base64 方式 | Multipart 方式 |
|---|---|---|
Content-Type | application/json | multipart/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#:
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:
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#:
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:
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* に  という文字列をセットすると画像が埋め込まれます。サブディレクトリで運用している場合はパスを合わせて書き換えてください。文章と画像を混ぜることが多い Wiki などで扱いが楽になります。
既存の添付ファイルを上書きする
既存ファイルの GUID を URL に入れ、?overwrite=true を付けてアップロードします。サーバーは GUID からそのファイルが添付されているレコードを探し、そのレコードを更新する形でファイルを差し替えます。レスポンスはレコード更新 API と同じ形になり、Id はレコードの ID、Message は更新完了のメッセージです(GUID ではありません)。
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 チャンクごとにかかります。
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 guidC# 版
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 を使います。
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 ファイルなので、複数ファイルは並列実行で効率化します。
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 resultsC# では SemaphoreSlim で同時実行数を制限します。
C# 版
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}");
}