Langsung ke konten
Sursa Developers

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:

  1. Buat unggahan: beri tahu Sursa nama, jenis, dan ukuran file.
  2. Kirim bagian-bagiannya, dalam urutan apa pun dan secara paralel jika Anda mau. Bagian yang gagal dikirim lagi secara tersendiri.
  3. Selesaikan unggahan dengan SHA-256 seluruh file. Sursa menyusun file dan memeriksanya.
  4. Pakai ID unggahan sebagai pengganti file: untuk kartu, arsip, atau dokumen sumber kustom.
BatasNilai
Ukuran fileHingga 5 GB
Ukuran bagianDipilih oleh Sursa di part_size, dari 5 MiB hingga 100 MiB; bagian terakhir boleh lebih kecil
Jumlah bagianHingga 1.000
Waktu untuk menyelesaikan24 jam setelah unggahan dibuat
Waktu untuk memakai unggahan yang sudah selesai24 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"
}
purposeUnggahan akan dipakai untuk
card_fileKartu: POST /v1/cards.
archiveArsip dokumen: POST /v1/cards/archive.
import_attachmentFile 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))
done

Mengirim 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

KodeStatusPenyebab
uploads.too_large413File lebih besar dari 5 GB, atau lebih besar dari seluruh penyimpanan dalam paket organisasi.
limits.storage402Sisa penyimpanan kosong organisasi tidak cukup untuk file ini.
uploads.part_invalid400Nomor bagian di luar rentang, atau bagian di luar ukuran yang diizinkan.
uploads.checksum_mismatch422SHA-256 file yang tersusun bukan yang Anda kirim. Ada bagian yang rusak atau hilang: kirim bagian-bagiannya lagi dan selesaikan lagi.
uploads.not_ready409Unggahan belum selesai dan belum diperiksa. Tunggu hingga ready.
uploads.already_used409Unggahan sudah dipakai. Setiap unggahan dipakai sekali.
uploads.expired410Sudah lewat lebih dari 24 jam. Buat unggahan baru.
uploads.rejected422File tidak lolos pemeriksaan, misalnya formatnya tidak sesuai dengan namanya. Pesannya menyebutkan alasannya.
uploads.not_found404Tidak ada unggahan seperti itu di organisasi.