Přejít na obsah
Sursa Developers

Nahrávání velkých souborů

Soubor lze odeslat v jediném požadavku POST /v1/cards až do limitu nahrávání dané instalace (výchozí je 200 MB). Nad tento limit, nebo v síti, kde se dlouhý požadavek pravděpodobně přeruší, nahrávejte soubor po částech:

  1. Vytvořte nahrávání: sdělte Surse název, typ a velikost souboru.
  2. Odešlete části v libovolném pořadí, klidně i paralelně. Neúspěšná část se odešle znovu samostatně.
  3. Dokončete nahrávání s kontrolním součtem SHA-256 celého souboru. Sursa soubor sestaví a zkontroluje.
  4. Místo souboru použijte ID nahrávání: pro kartu, archiv nebo dokument vlastního zdroje.
LimitHodnota
Velikost souboruAž 5 GB
Velikost částiVolí ji Sursa v part_size, od 5 MiB do 100 MiB; poslední část může být menší
Počet částíAž 1000
Čas na dokončení24 hodin od vytvoření nahrávání
Čas na použití dokončeného nahrávání24 hodin

1. Vytvořte nahrávání

POST /v1/uploads přijímá název, typ a velikost souboru a účel, ke kterému ho použijete:

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"
}
purposeNahrávání se použije pro
card_fileKartu: POST /v1/cards.
archiveArchiv dokumentů: POST /v1/cards/archive.
import_attachmentSoubor dokumentu vlastního zdroje v upload_id nebo attachments.

Klíč potřebuje rozsah místa, kam nahrávání míří: cards:write pro card_file a archive, import:write pro import_attachment.

Sursa zvolí part_size a sdělí vám, kolik částí máte odeslat. Pokud kontrolní součet SHA-256 souboru znáte předem, můžete ho předat jako sha256 už zde.

2. Odešlete části

Rozdělte soubor na part_count kusů po part_size bajtech (poslední je zbytek) a každý odešlete pomocí PUT /v1/uploads/{upload_id}/parts/{n}. Části se číslují od 1. Tělo požadavku tvoří nezpracované bajty části.

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

Opětovné odeslání části ji nahradí, takže část, která selhala nebo u které vypršel časový limit, se prostě odešle znovu. Rychlé připojení obvykle vytíží až 4 paralelní části.

Kontrola částí

Každá část prochází přes API: přímé odkazy do úložiště neexistují. Odpověď na část má hlavičku ETag s kontrolním součtem SHA-256 části v uvozovkách a stejnou hodnotu v etag těla JSON. GET /v1/uploads/{upload_id} uvádí v parts dosud přijaté části, takže po přerušení odešlete jen ty, které chybějí.

3. Dokončete nahrávání

POST /v1/uploads/{upload_id}/complete přijímá kontrolní součet SHA-256 celého souboru jako 64 šestnáctkových znaků a volitelně také etag každé části v parts, aby se zkontrolovaly i části:

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 části sestaví, porovná hash a zkontroluje soubor stejně jako jakékoli jiné nahrávání: jeho formát a tam, kde je zapnutá, i antivirovou kontrolou. Odpověď je 200, když je kontrola hotová, a 202, dokud ještě probíhá. Stav nahrávání přejde z processing na ready, nebo na rejected s důvodem v reject_reason. Zjistíte ho pomocí GET /v1/uploads/{upload_id}.

4. Použijte nahrávání

Předejte upload_id tam, kam se jinak posílá soubor. Každé nahrávání se použije jen jednou: poté má stav consumed.

U karty a archivu odešlete upload_id v těle JSON místo formuláře multipart/form-data. Toto tělo přijímá kolekci podle jejího názvu v 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\"}"

Úplný nahrávací program

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));
  }
}

Chyby

KódStavový kódPříčina
uploads.too_large413Soubor je větší než 5 GB nebo než celé úložiště v tarifu organizace.
limits.storage402Organizaci nezbývá pro soubor dost volného úložiště.
uploads.part_invalid400Číslo části mimo rozsah nebo část mimo povolenou velikost.
uploads.checksum_mismatch422SHA-256 sestaveného souboru neodpovídá odeslané hodnotě. Některá část byla poškozená nebo chybí: odešlete části znovu a nahrávání znovu dokončete.
uploads.not_ready409Nahrávání ještě není dokončené a zkontrolované. Počkejte na ready.
uploads.already_used409Nahrávání už bylo použito. Každé nahrávání se použije jen jednou.
uploads.expired410Uplynulo více než 24 hodin. Vytvořte nové nahrávání.
uploads.rejected422Soubor neprošel kontrolami, například jeho formát neodpovídá názvu. Důvod uvádí zpráva chyby.
uploads.not_found404Takové nahrávání v organizaci neexistuje.