はじめに
プリザンターのAPIで添付ファイルをアップロードする場合、従来は Base64エンコード してJSON形式で送信する方法が一般的でした。しかしこの方法には以下の課題があります:
- GUIDへの変換が面倒(Attachments項目はGUID形式で管理される)
- 転送量が約33%増加(Base64エンコードによるオーバーヘッド)
- 送信側の処理負荷(大容量ファイルのBase64変換)
- メモリ消費(ファイル全体をメモリに展開)
これらの課題を解決するため、プリザンターには Multipart/form-data 形式でファイルをアップロードする専用APIが用意されています。
Binaries API(Upload)とは
/api/binaries/upload エンドポイントを使用すると、Base64変換なしでファイルを直接アップロードできます。
| エンドポイント | メソッド | 用途 |
|---|---|---|
/api/binaries/upload?id={SiteId} |
POST | 新規アップロード(レコード未作成時) |
/api/binaries/{guid}/upload |
POST | 既存添付ファイルの更新 |
上記のパスはルート配置の場合です。サブディレクトリ配置(例: Pleasanter.net)の場合はパスにプレフィックスが付きます(例: /fs/api/binaries/upload)。コード中で URL を組み立てる際は baseUrl にサブディレクトリを含めてください。
認証
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...
認証方式が独特:一般的なOAuth2ではアクセストークンを指定しますが、プリザンターではAPIキーをそのまま指定します。APIキーはプリザンターの「ユーザー設定 > API設定」から取得できます。
レスポンス形式
{
"Id": 123,
"StatusCode": 200,
"Message": "550E8400E29B41D4A716446655440000"
}
- Id: リクエストID(サーバー内部での識別子)
- StatusCode: 処理結果を示すステータスコード
- Message: 成功時はアップロードされたファイルのGUID(ハイフンなし大文字32文字)、エラー時はエラーメッセージ
1リクエスト=1ファイル:このAPIは1回のリクエストで1ファイルのみ受け付けます。複数ファイルをアップロードする場合は、ファイルごとにリクエストを送信し、それぞれのGUIDを取得してください。
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 はサーバーの言語設定に依存します。上記は日本語設定時の表示例です。
従来方式との比較
| 項目 | 従来方式(Base64) | Multipart方式 |
|---|---|---|
Content-Type |
application/json |
multipart/form-data |
| ファイルデータ | Base64エンコード必須 | バイナリのまま送信 |
| 転送量 | 100%(基準) | 約75%(25%削減) |
| GUID管理 | サーバー側で自動生成 | サーバー側で自動生成 |
| 大容量対応 | メモリ制約あり | チャンク転送対応 |
| 送信側処理 | エンコード処理必要 | ほぼ不要 |
登録フローの違い
Base64方式 は1回のAPI呼び出しでレコード作成と添付ファイル登録を同時に行えます。
一方、Multipart方式 はファイルアップロードでGUIDを取得した後、レコード作成時にそのGUIDを指定する2段階の処理が必要です。
Base64方式(1回のAPI呼び出し)
Multipart方式(2回のAPI呼び出し)
Multipart方式は2回のAPI呼び出しが必要ですが、大容量ファイルの場合はBase64エンコードのオーバーヘッドを回避できるため、総合的な処理時間は短縮されます。
Base64方式との詳細比較
効率性の比較
| 観点 | Base64方式 | Multipart方式 | 削減率 |
|---|---|---|---|
| 転送データ量 | 100%(基準) | 約75% | 25%削減 |
| エンコード時間 | 必要(CPU負荷) | 不要 | 処理時間短縮 |
| メモリ使用量 | ファイル全体+エンコード後 | ストリーム可能 | 大幅削減 |
使い勝手の比較
| 観点 | Base64方式 | Multipart方式 |
|---|---|---|
| GUID管理 | サーバー自動生成(レスポンスで取得) | サーバー自動生成(レスポンスで取得) |
| 複数ファイル | JSON配列で構造化 | フォームで複数ファイル指定可能 |
| 実装の複雑さ | エンコード処理の実装必要 | HTTP標準のfile送信 |
| デバッグ | JSONなので可読性あり | バイナリなので確認しにくい |
| 既存ツール連携 | JSON対応ツール | curl等で簡単に実行可能 |
| チャンク転送 | 未対応 |
Content-Rangeで対応 |
| 整合性検証 | 未対応 |
FileHashで検証可能 |
コード量の比較(C#)
Base64方式:
using System.Text;
using System.Text.Json;
public async Task<string> UploadBase64Async(
string baseUrl, string apiKey, long recordId, string filePath)
{
// ファイル読み込み + Base64エンコード(メモリ負荷大)
var fileBytes = await File.ReadAllBytesAsync(filePath);
var base64Data = Convert.ToBase64String(fileBytes);
// リクエストボディ構築(GUIDは指定不要、サーバーが自動生成)
var payload = new
{
ApiKey = apiKey,
AttachmentsHash = new
{
AttachmentsA = new[]
{
new
{
Name = Path.GetFileName(filePath),
Base64 = base64Data,
Added = true
}
}
}
};
using var client = new HttpClient();
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json");
await client.PostAsync($"{baseUrl}/api/items/{recordId}/update", content);
return fileGuid;
}
Multipart方式:
public async Task<string> UploadMultipartAsync(
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);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
return result.GetProperty("Message").GetString()!; // GUID自動取得
}
Multipart方式はBase64方式と比較して、コード量が約1/2、Convert.ToBase64String によるメモリ負荷も回避できます。
基本的な使い方
1. 新規ファイルのアップロード
サイトIDを指定してファイルをアップロードします。
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
成功時はレスポンスの Message にGUIDが返されます。エラー時は対応するStatusCodeとエラーメッセージが返されます(詳細は上記「StatusCode と Message 一覧」参照)。
日本語ファイル名について:Multipart形式では Content-Disposition ヘッダーでファイル名を送信しますが、C# の HttpClient、Python の requests、curl はいずれも日本語ファイル名を RFC 5987 形式で自動エンコードします。プリザンター側も ASP.NET Core の IFormFile.FileName でこれを自動解析するため、特別な対応は不要です。
2. レコード作成時に添付ファイルを関連付け
アップロードしたファイルをレコード作成時に関連付けます。
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']
3. 既存添付ファイルの更新
既存の添付ファイルを上書き更新する場合は、GUIDを指定してアップロードします。
C#:
public async Task<string> UpdateFileAsync(
string baseUrl, string apiKey, string existingGuid, 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/{existingGuid}/upload?overwrite=true", content);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
return result.GetProperty("Message").GetString()!;
}
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 response.json()['Message']
既存ファイルを上書きする場合は ?overwrite=true クエリパラメータが必須です。指定しない場合、新しいGUIDで別ファイルとして登録されます。
大容量ファイルのチャンク転送
Content-Rangeヘッダーを使用して、大容量ファイルを分割送信できます。
アップロード可能な最大サイズはサーバー設定(Parameters/BinaryStorage.json)に依存します。チャンク転送を使用しても、この制限を超えることはできません。
C#:
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();
using var chunkStream = new MemoryStream(buffer, 0, bytesRead);
content.Add(new StreamContent(chunkStream), "file", fileName);
// Content-Rangeヘッダーを設定
using var request = new HttpRequestMessage(HttpMethod.Post,
$"{baseUrl}/api/binaries/upload?id={siteId}");
request.Content = content;
request.Headers.Add("Content-Range", $"bytes {offset}-{end}/{fileSize}");
var response = await client.SendAsync(request);
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!;
}
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ヘッダーを設定
chunk_headers = headers.copy()
chunk_headers['Content-Range'] = f'bytes {offset}-{end}/{file_size}'
files = {'file': (file_name, chunk)}
response = requests.post(url, headers=chunk_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
チャンク転送では、途中のリクエストには一時GUIDが返され、最終チャンク送信後に正式なGUIDが返されます。
ファイル整合性検証(FileHash)
アップロード時にファイルのハッシュ値を送信することで、転送中のデータ破損を検出できます。
C#:
using System.Security.Cryptography;
public async Task<string> UploadWithHashAsync(
string baseUrl, string apiKey, long siteId, string filePath)
{
// SHA-256ハッシュを計算
var fileBytes = await File.ReadAllBytesAsync(filePath);
var hashBytes = SHA256.HashData(fileBytes);
var fileHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
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));
content.Add(new StringContent(fileHash), "FileHash");
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()!;
}
Python:
import hashlib
def upload_with_hash(
base_url: str, api_key: str, site_id: int, file_path: str) -> str:
# SHA-256ハッシュを計算
with open(file_path, 'rb') as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
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)}
data = {'FileHash': file_hash}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()['Message']
ハッシュ値が一致しない場合、エラーが返されます。
複数ファイルの一括アップロード
Binaries APIは1リクエストで1ファイルのみ受け付けるため、複数ファイルをアップロードする場合は並列実行で効率化できます。
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}");
}
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
# 使用例
files = ["doc1.pdf", "doc2.pdf", "doc3.pdf", "image.png"]
results = batch_upload("https://your-pleasanter", "your-api-key", 123, files)
print(f"\nアップロード結果: {len(results)}件")
for name, guid in results.items():
print(f" {name}: {guid}")
活用シナリオ
1. バッチ処理での大量ファイルアップロード
C#:
// ディレクトリ内の全PDFをアップロード
var pdfFiles = Directory.GetFiles(@".\documents", "*.pdf");
var results = await BatchUploadAsync(baseUrl, apiKey, siteId, pdfFiles);
foreach (var (fileName, guid) in results)
{
Console.WriteLine($"Uploaded: {fileName} -> {guid}");
}
Python:
import glob
pdf_files = glob.glob("./documents/*.pdf")
results = batch_upload("https://your-pleasanter", "your-api-key", 123, pdf_files)
for file_name, guid in results.items():
print(f"Uploaded: {file_name} -> {guid}")
2. 外部システムからの自動連携
従来のBase64方式では外部システム側でエンコード処理が必要でしたが、Multipart方式ならファイルをそのまま送信できます。
3. モバイルアプリからのアップロード
Base64変換が不要なため、モバイル端末のメモリ・CPU負荷を軽減できます。
(Xamarin/MAUI でも同様のコードが使用可能)
まとめ
Multipart/form-data形式でのファイルアップロードには以下のメリットがあります:
- GUID変換不要 - サーバー側で自動生成されるため管理が簡単
- 転送量削減 - Base64のオーバーヘッド(約33%)がない
- 処理負荷軽減 - エンコード/デコード処理が不要
- 大容量対応 - チャンク転送で巨大ファイルも安定送信
- 整合性検証 - FileHashによるデータ破損検出
外部システム連携やバッチ処理で大量・大容量のファイルを扱う場合は、ぜひこのMultipart方式を活用してみてください。
-
RFC 4648 - The Base16, Base32, and Base64 Data Encodings Section 4: "The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters." ↩