Przejdź do treści
Sursa Developers

Przesyłaj duże pliki

Plik może trafić do pojedynczego żądania POST /v1/cards, jeśli nie przekracza limitu przesyłania instalacji (domyślnie 200 MB). Powyżej tego limitu lub w sieci, w której długie żądanie najpewniej zostanie przerwane, przesyłaj plik w częściach:

  1. Utwórz przesyłanie: podaj Sursie nazwę, typ i rozmiar pliku.
  2. Wyślij części w dowolnej kolejności, jeśli chcesz – równolegle. Nieudaną część wysyła się ponownie osobno.
  3. Zakończ przesyłanie, podając sumę SHA-256 całego pliku. Sursa składa plik i go sprawdza.
  4. Użyj ID przesyłania zamiast pliku: dla karty, archiwum lub dokumentu własnego źródła.
LimitWartość
Rozmiar plikuDo 5 GB
Rozmiar częściWybierany przez Sursę w part_size, od 5 MiB do 100 MiB; ostatnia część może być mniejsza
Liczba częściDo 1000
Czas na zakończenie24 godziny od utworzenia przesyłania
Czas na użycie zakończonego przesyłania24 godziny

1. Utwórz przesyłanie

POST /v1/uploads przyjmuje nazwę, typ i rozmiar pliku oraz informację, do czego zamierzasz go użyć:

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"
}
purposePrzesyłanie zostanie użyte dla
card_fileKarty: POST /v1/cards.
archiveArchiwum dokumentów: POST /v1/cards/archive.
import_attachmentPliku dokumentu własnego źródła, w upload_id lub attachments.

Klucz potrzebuje zakresu miejsca, do którego trafia przesyłanie: cards:write dla card_file i archive, import:write dla import_attachment.

Sursa wybiera part_size i podaje, ile części należy wysłać. Jeśli znasz z góry sumę SHA-256 pliku, możesz ją przekazać już tutaj jako sha256.

2. Wyślij części

Podziel plik na part_count kawałków po part_size bajtów (ostatni zawiera resztę) i wyślij każdy za pomocą PUT /v1/uploads/{upload_id}/parts/{n}. Części są numerowane od 1. Treścią żądania są surowe bajty części.

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

Ponowne wysłanie części zastępuje ją, więc część, której wysłanie się nie powiodło lub przekroczyło limit czasu, po prostu wysyła się jeszcze raz. Do 4 części równolegle zwykle w pełni wykorzystuje szybkie łącze.

Sprawdź części

Każda część przechodzi przez API: nie ma bezpośrednich odnośników do magazynu. Odpowiedź na wysłanie części zawiera nagłówek ETag z sumą SHA-256 części w cudzysłowie i tę samą wartość w polu etag treści JSON. GET /v1/uploads/{upload_id} wymienia w parts dotychczas otrzymane części, więc po przerwie wysyłasz tylko brakujące.

3. Zakończ przesyłanie

POST /v1/uploads/{upload_id}/complete przyjmuje sumę SHA-256 całego pliku jako 64 znaki szesnastkowe oraz opcjonalnie etag każdej części w parts, aby sprawdzić także części:

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 składa części, porównuje sumę kontrolną i sprawdza plik tak samo jak każde przesyłanie: jego format oraz, jeśli jest włączone, skanowanie antywirusowe. Odpowiedź to 200, gdy sprawdzanie jest zakończone, i 202, dopóki jeszcze trwa. Status przesyłania zmienia się z processing na ready albo na rejected z przyczyną w reject_reason. Odczytaj go za pomocą GET /v1/uploads/{upload_id}.

4. Użyj przesyłania

Przekaż upload_id tam, gdzie normalnie wysyła się plik. Każde przesyłanie jest używane tylko raz: potem jego status to consumed.

W przypadku karty i archiwum wyślij upload_id w treści JSON zamiast formularza multipart/form-data. Ta treść wskazuje kolekcję jej nazwą w polu 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\"}"

Kompletny program do przesyłania

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

Błędy

KodStatusPrzyczyna
uploads.too_large413Plik jest większy niż 5 GB lub większy niż całe miejsce na dane w planie organizacji.
limits.storage402Organizacji nie zostało wystarczająco dużo wolnego miejsca na dane dla tego pliku.
uploads.part_invalid400Numer części poza zakresem lub część o niedozwolonym rozmiarze.
uploads.checksum_mismatch422Suma SHA-256 złożonego pliku różni się od wysłanej. Któraś część została uszkodzona lub jej brakuje: wyślij części ponownie i ponownie zakończ przesyłanie.
uploads.not_ready409Przesyłanie nie zostało jeszcze zakończone i sprawdzone. Poczekaj na ready.
uploads.already_used409Przesyłanie zostało już użyte. Każde przesyłanie jest używane tylko raz.
uploads.expired410Minęły ponad 24 godziny. Utwórz nowe przesyłanie.
uploads.rejected422Plik nie przeszedł kontroli, np. jego format nie pasuje do nazwy. Komunikat podaje przyczynę.
uploads.not_found404W organizacji nie ma takiego przesyłania.