大きなファイルをアップロードする
インストール環境のアップロードの上限(デフォルトは200MB)までのファイルは、POST /v1/cardsの1回のリクエストで送信できます。それを超える場合や、長いリクエストが途切れやすいネットワークでは、ファイルをパートに分けてアップロードします。
- アップロードを作成します。ファイルの名前、種類、サイズをSursaに伝えます。
- パートを送信します。順序は自由で、並行して送信してもかまいません。失敗したパートはそのパートだけを送り直します。
- ファイル全体のSHA-256を指定してアップロードを完了します。Sursaはファイルを組み立てて確認します。
- ファイルの代わりにアップロードのIDを使います。カード、アーカイブファイル、カスタムソースのドキュメントに使えます。
| 上限 | 値 |
|---|---|
| ファイルサイズ | 5GBまで |
| パートのサイズ | Sursaがpart_sizeで決めます。5MiB〜100MiB。最後のパートはそれより小さくてもかまいません |
| パートの数 | 1000まで |
| 完了までの期限 | アップロードの作成から24時間 |
| 完了したアップロードを使うまでの期限 | 24時間 |
1. アップロードを作成する
POST /v1/uploadsは、ファイルの名前、種類、サイズと、その用途を受け取ります。
FILE=training-video-transcripts.zip
SIZE=$(wc -c < "$FILE" | tr -d ' ')
curl -X POST https://api.sursa.com/v1/uploads \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d "{\"filename\": \"$FILE\", \"content_type\": \"application/zip\", \"size\": $SIZE, \"purpose\": \"archive\"}"{
"id": "upl_5d3c2b1a0f9e8d7c6b5a493827161504",
"status": "pending",
"part_size": 8388608,
"part_count": 23,
"expires_at": "2026-10-25T10:00:00Z"
}
purpose | アップロードの用途 |
|---|---|
card_file | カード:POST /v1/cards。 |
archive | ドキュメントのアーカイブファイル:POST /v1/cards/archive。 |
import_attachment | カスタムソースのドキュメントのファイル(upload_idまたはattachmentsの中)。 |
キーには、アップロードの使用先のスコープが必要です。card_fileとarchiveにはcards:write、import_attachmentにはimport:writeです。
part_sizeはSursaが決め、送信するパートの数を伝えます。ファイルのSHA-256が前もってわかっている場合は、ここでsha256として渡すこともできます。
2. パートを送信する
ファイルをpart_sizeバイトずつpart_count個に分け(最後のパートは残りの部分です)、それぞれをPUT /v1/uploads/{upload_id}/parts/{n}で送信します。パートの番号は1から始まります。ボディはパートの生のバイト列です。
UPLOAD=upl_5d3c2b1a0f9e8d7c6b5a493827161504
split -b 8388608 -a 4 "$FILE" part-
n=1
for part in part-*; do
curl -X PUT "https://api.sursa.com/v1/uploads/$UPLOAD/parts/$n" \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$part" --retry 5 --fail
n=$((n + 1))
doneパートをもう一度送信すると置き換えられるので、失敗したパートやタイムアウトしたパートは単に送り直します。高速な回線なら、通常は4つまでのパートを並行して送信すれば帯域を使い切れます。
パートを確認する
すべてのパートはAPIを通して送信します。ストレージへの直接のリンクはありません。パートへのレスポンスには、引用符で囲んだパートのSHA-256を値とするETagヘッダーがあり、同じ値がJSONボディのetagにも入ります。GET /v1/uploads/{upload_id}は、それまでに受け取ったパートをpartsに一覧表示するので、中断した後は足りないパートだけを送信します。
3. アップロードを完了する
POST /v1/uploads/{upload_id}/completeは、ファイル全体のSHA-256を64文字の16進数で受け取ります。パートも確認したい場合は、partsに各パートのetagを指定することもできます。
SHA=$(shasum -a 256 "$FILE" | cut -d' ' -f1)
curl -X POST "https://api.sursa.com/v1/uploads/$UPLOAD/complete" \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Idempotency-Key: complete-$UPLOAD" \
-H "Content-Type: application/json" \
-d "{\"sha256\": \"$SHA\"}"Sursaはパートを組み立て、ハッシュを比較し、他のアップロードと同じようにファイルを確認します。確認するのは形式と、有効になっている場合はウイルススキャンです。確認が終わっていればレスポンスは200、まだ実行中なら202です。アップロードのステータスはprocessingからreadyに変わるか、rejectedになり、理由がreject_reasonに入ります。GET /v1/uploads/{upload_id}で読み込んでください。
4. アップロードを使う
ファイルを送信する場所で、代わりにupload_idを渡します。各アップロードは一度だけ使えます。使った後、ステータスはconsumedになります。
カードとアーカイブファイルでは、multipart/form-dataのフォームの代わりに、JSONボディでupload_idを送信します。このボディでは、コレクションをcollectionにその名前で指定します。
# A card from the uploaded file
curl -X POST https://api.sursa.com/v1/cards \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Idempotency-Key: card-from-$UPLOAD" \
-H "Content-Type: application/json" \
-d "{\"upload_id\": \"$UPLOAD\", \"collection\": \"support\", \"async\": true}"
# An archive of documents from the uploaded ZIP
curl -X POST https://api.sursa.com/v1/cards/archive \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Idempotency-Key: archive-from-$UPLOAD" \
-H "Content-Type: application/json" \
-d "{\"upload_id\": \"$UPLOAD\", \"collection\": \"support\"}"アップローダーの完成例
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { open, stat } from "node:fs/promises";
import { basename } from "node:path";
const BASE = "https://api.sursa.com";
const auth = { Authorization: `Bearer ${process.env.SURSA_API_KEY}` };
async function call(method, path, body, headers = {}) {
const isJson = body !== undefined && !(body instanceof Uint8Array);
const response = await fetch(BASE + path, {
method,
headers: { ...auth, ...(isJson ? { "Content-Type": "application/json" } : {}), ...headers },
body: isJson ? JSON.stringify(body) : body,
});
if (!response.ok) throw new Error(`${method} ${path}: HTTP ${response.status} ${await response.text()}`);
return response.json();
}
export async function uploadLargeFile(file, contentType, purpose, parallel = 4) {
const { size } = await stat(file);
const upload = await call("POST", "/v1/uploads",
{ filename: basename(file), content_type: contentType, size, purpose },
{ "Idempotency-Key": crypto.randomUUID() });
const hash = createHash("sha256");
for await (const block of createReadStream(file)) hash.update(block);
const handle = await open(file);
let next = 1;
async function worker() {
for (let n = next++; n <= upload.part_count; n = next++) {
const buffer = new Uint8Array(Math.min(upload.part_size, size - (n - 1) * upload.part_size));
await handle.read(buffer, 0, buffer.length, (n - 1) * upload.part_size);
for (let attempt = 1; ; attempt++) {
try {
await call("PUT", `/v1/uploads/${upload.id}/parts/${n}`, buffer, { "Content-Type": "application/octet-stream" });
break;
} catch (error) {
if (attempt === 5) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt));
}
}
}
}
await Promise.all(Array.from({ length: parallel }, worker));
await handle.close();
await call("POST", `/v1/uploads/${upload.id}/complete`, { sha256: hash.digest("hex") },
{ "Idempotency-Key": `complete-${upload.id}` });
for (;;) {
const state = await call("GET", `/v1/uploads/${upload.id}`);
if (state.status === "ready") return upload.id;
if (state.status === "rejected") throw new Error(`upload rejected: ${state.reject_reason}`);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}エラー
| コード | ステータス | 原因 |
|---|---|---|
uploads.too_large | 413 | ファイルが5GBより大きいか、組織のプランのストレージ全体より大きい。 |
limits.storage | 402 | 組織のストレージの空きが、ファイルに必要な分だけ残っていない。 |
uploads.part_invalid | 400 | パートの番号が範囲外か、パートが許可されたサイズの範囲外。 |
uploads.checksum_mismatch | 422 | 組み立てたファイルのSHA-256が、送信した値と一致しない。パートが壊れているか欠けています。パートを送り直し、もう一度完了してください。 |
uploads.not_ready | 409 | アップロードの完了と確認がまだ済んでいない。readyになるまで待ってください。 |
uploads.already_used | 409 | アップロードはすでに使われている。各アップロードは一度だけ使えます。 |
uploads.expired | 410 | 24時間以上が経過した。新しいアップロードを作成してください。 |
uploads.rejected | 422 | ファイルが確認に通らなかった。たとえば形式が名前と一致しない場合です。メッセージに理由が示されます。 |
uploads.not_found | 404 | 組織にそのアップロードがない。 |