3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターAPIで大容量ファイルをアップロードする方法(Multipart対応)

3
Posted at

はじめに

プリザンターの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エンコードは3バイト(24ビット)を4文字に変換するため12、データ量が約1.33倍(133%)になります。Multipart方式はこのオーバーヘッドがないため、Base64を100%とすると約75%のデータ量で済みます。

使い勝手の比較

観点 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 の requestscurl はいずれも日本語ファイル名を 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方式を活用してみてください。

  1. 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."

  2. Microsoft Learn - Convert.ToBase64String

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?