Mengunggah file besar
File dapat dikirim dalam satu permintaan POST /v1/cards hingga batas unggahan
instalasi (secara default 200 MB). Di atas batas itu, atau di jaringan yang kemungkinan memutus permintaan yang lama,
unggah file per bagian:
- Buat unggahan: beri tahu Sursa nama, jenis, dan ukuran file.
- Kirim bagian-bagiannya, dalam urutan apa pun dan secara paralel jika Anda mau. Bagian yang gagal dikirim lagi secara tersendiri.
- Selesaikan unggahan dengan SHA-256 seluruh file. Sursa menyusun file dan memeriksanya.
- Pakai ID unggahan sebagai pengganti file: untuk kartu, arsip, atau dokumen sumber kustom.
| Batas | Nilai |
|---|---|
| Ukuran file | Hingga 5 GB |
| Ukuran bagian | Dipilih oleh Sursa di part_size, dari 5 MiB hingga 100 MiB; bagian terakhir boleh lebih kecil |
| Jumlah bagian | Hingga 1.000 |
| Waktu untuk menyelesaikan | 24 jam setelah unggahan dibuat |
| Waktu untuk memakai unggahan yang sudah selesai | 24 jam |
1. Buat unggahan
POST /v1/uploads menerima nama, jenis, dan ukuran file, serta tujuan
pemakaiannya:
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 | Unggahan akan dipakai untuk |
|---|---|
card_file | Kartu: POST /v1/cards. |
archive | Arsip dokumen: POST /v1/cards/archive. |
import_attachment | File dari dokumen sumber kustom, di upload_id atau attachments. |
Kunci memerlukan cakupan dari tempat tujuan unggahan: cards:write untuk card_file dan archive, import:write
untuk import_attachment.
Sursa memilih part_size dan memberi tahu Anda berapa banyak bagian yang harus dikirim. Jika Anda sudah mengetahui
SHA-256 file sebelumnya, Anda juga dapat memberikannya di sini sebagai sha256.
2. Kirim bagian-bagiannya
Bagi file menjadi part_count bagian berukuran part_size byte (bagian terakhir berisi sisanya) dan kirim
masing-masing dengan PUT /v1/uploads/{upload_id}/parts/{n}. Bagian diberi nomor
mulai dari 1. Isi permintaannya adalah byte mentah bagian tersebut.
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))
doneMengirim bagian lagi akan menggantikannya, jadi bagian yang gagal atau terkena timeout cukup dikirim lagi. Hingga 4 bagian secara paralel biasanya sudah memenuhi kapasitas koneksi yang cepat.
Periksa bagian-bagiannya
Setiap bagian melewati API: tidak ada tautan langsung ke penyimpanan. Respons untuk sebuah bagian memiliki header
ETag, yaitu SHA-256 bagian itu dalam tanda kutip, dan nilai yang sama di etag pada isi JSON.
GET /v1/uploads/{upload_id} mencantumkan bagian yang sudah diterima di parts,
jadi setelah koneksi terputus Anda hanya mengirim bagian yang belum ada.
3. Selesaikan unggahan
POST /v1/uploads/{upload_id}/complete menerima SHA-256 seluruh file sebagai
64 karakter heksadesimal dan, secara opsional, etag setiap bagian di parts, untuk memeriksa bagian-bagiannya
juga:
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 menyusun bagian-bagiannya, membandingkan hash, dan memeriksa file seperti memeriksa unggahan apa pun:
formatnya dan, jika diaktifkan, antivirus. Jawabannya 200 jika pemeriksaan sudah selesai dan 202 selama
pemeriksaan masih berjalan. Status unggahan berubah dari processing menjadi ready, atau menjadi rejected dengan
alasan di reject_reason. Baca status itu dengan GET /v1/uploads/{upload_id}.
4. Pakai unggahan
Berikan upload_id di tempat Anda biasanya mengirim file. Setiap unggahan dipakai sekali: sesudahnya, statusnya
consumed.
Untuk kartu dan arsip, kirim upload_id dalam isi JSON sebagai pengganti formulir multipart/form-data. Isi ini
menerima koleksi menurut namanya di 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\"}"Pengunggah lengkap
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));
}
}Error
| Kode | Status | Penyebab |
|---|---|---|
uploads.too_large | 413 | File lebih besar dari 5 GB, atau lebih besar dari seluruh penyimpanan dalam paket organisasi. |
limits.storage | 402 | Sisa penyimpanan kosong organisasi tidak cukup untuk file ini. |
uploads.part_invalid | 400 | Nomor bagian di luar rentang, atau bagian di luar ukuran yang diizinkan. |
uploads.checksum_mismatch | 422 | SHA-256 file yang tersusun bukan yang Anda kirim. Ada bagian yang rusak atau hilang: kirim bagian-bagiannya lagi dan selesaikan lagi. |
uploads.not_ready | 409 | Unggahan belum selesai dan belum diperiksa. Tunggu hingga ready. |
uploads.already_used | 409 | Unggahan sudah dipakai. Setiap unggahan dipakai sekali. |
uploads.expired | 410 | Sudah lewat lebih dari 24 jam. Buat unggahan baru. |
uploads.rejected | 422 | File tidak lolos pemeriksaan, misalnya formatnya tidak sesuai dengan namanya. Pesannya menyebutkan alasannya. |
uploads.not_found | 404 | Tidak ada unggahan seperti itu di organisasi. |