Zum Inhalt springen
Sursa Developers

Große Dateien hochladen

Eine Datei kann bis zum Upload-Limit der Installation (standardmäßig 200 MB) in einer einzigen Anfrage an POST /v1/cards gesendet werden. Darüber oder in einem Netzwerk, in dem eine lange Anfrage wahrscheinlich abbricht, laden Sie die Datei in Teilen hoch:

  1. Erstellen Sie einen Upload: Teilen Sie Sursa Name, Typ und Größe der Datei mit.
  2. Senden Sie die Teile in beliebiger Reihenfolge und auf Wunsch parallel. Ein fehlgeschlagener Teil wird einzeln erneut gesendet.
  3. Schließen Sie den Upload mit dem SHA-256 der ganzen Datei ab. Sursa setzt die Datei zusammen und prüft sie.
  4. Verwenden Sie die Upload-ID anstelle der Datei: für eine Karte, ein Archiv oder ein Dokument einer benutzerdefinierten Quelle.
LimitWert
DateigrößeBis zu 5 GB
Größe eines TeilsVon Sursa in part_size gewählt, 5 MiB bis 100 MiB; der letzte Teil kann kleiner sein
Anzahl der TeileBis zu 1000
Zeit bis zum Abschluss24 Stunden nach dem Erstellen des Uploads
Zeit zum Verwenden eines abgeschlossenen Uploads24 Stunden

1. Upload erstellen

POST /v1/uploads erwartet Name, Typ und Größe der Datei sowie den Zweck, für den Sie sie verwenden werden:

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"
}
purposeDer Upload wird verwendet für
card_fileEine Karte: POST /v1/cards.
archiveEin Archiv mit Dokumenten: POST /v1/cards/archive.
import_attachmentEine Datei eines Dokuments einer benutzerdefinierten Quelle, in upload_id oder attachments.

Der Schlüssel braucht den Scope des Ziels, für das der Upload bestimmt ist: cards:write für card_file und archive, import:write für import_attachment.

Sursa wählt part_size und teilt Ihnen mit, wie viele Teile Sie senden müssen. Wenn Sie den SHA-256 der Datei vorab kennen, können Sie ihn hier bereits als sha256 übergeben.

2. Teile senden

Zerlegen Sie die Datei in part_count Stücke zu je part_size Bytes (das letzte ist der Rest) und senden Sie jedes mit PUT /v1/uploads/{upload_id}/parts/{n}. Die Teile werden ab 1 nummeriert. Der Body sind die rohen Bytes des Teils.

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

Ein erneut gesendeter Teil ersetzt den vorherigen. Ein Teil, der fehlgeschlagen ist oder in ein Timeout gelaufen ist, wird daher einfach noch einmal gesendet. Bis zu 4 parallele Teile lasten eine schnelle Verbindung meist voll aus.

Teile prüfen

Jeder Teil läuft über die API: Es gibt keine direkten Links zum Speicher. Die Antwort auf einen Teil hat den Header ETag mit dem SHA-256 des Teils in Anführungszeichen und denselben Wert in etag des JSON-Bodys. GET /v1/uploads/{upload_id} listet die bisher empfangenen Teile in parts auf, sodass Sie nach einer Unterbrechung nur die fehlenden senden.

3. Upload abschließen

POST /v1/uploads/{upload_id}/complete erwartet den SHA-256 der ganzen Datei als 64 Hexadezimalzeichen und optional das etag jedes Teils in parts, um auch die Teile zu prüfen:

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 setzt die Teile zusammen, vergleicht den Hash und prüft die Datei wie jeden Upload: ihr Format und, wo aktiviert, auf Viren. Die Antwort ist 200, wenn die Prüfung abgeschlossen ist, und 202, solange sie noch läuft. Der Status des Uploads wechselt von processing zu ready oder zu rejected mit dem Grund in reject_reason. Lesen Sie ihn mit GET /v1/uploads/{upload_id}.

4. Upload verwenden

Übergeben Sie upload_id dort, wo Sie sonst die Datei senden würden. Jeder Upload wird genau einmal verwendet: Danach ist sein Status consumed.

Für eine Karte und ein Archiv senden Sie upload_id in einem JSON-Body statt im Formular multipart/form-data. Dieser Body erwartet die Sammlung über ihren Namen in 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\"}"

Ein vollständiges Upload-Programm

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

Fehler

CodeStatusUrsache
uploads.too_large413Die Datei ist größer als 5 GB oder größer als der gesamte Speicherplatz des Tarifs der Organisation.
limits.storage402Die Organisation hat nicht mehr genug freien Speicherplatz für die Datei.
uploads.part_invalid400Eine Teilnummer außerhalb des Bereichs oder ein Teil außerhalb der erlaubten Größe.
uploads.checksum_mismatch422Der SHA-256 der zusammengesetzten Datei stimmt nicht mit dem gesendeten überein. Ein Teil ist beschädigt oder fehlt: Senden Sie die Teile erneut und schließen Sie den Upload erneut ab.
uploads.not_ready409Der Upload ist noch nicht abgeschlossen und geprüft. Warten Sie auf ready.
uploads.already_used409Der Upload wurde bereits verwendet. Jeder Upload wird genau einmal verwendet.
uploads.expired410Es sind mehr als 24 Stunden vergangen. Erstellen Sie einen neuen Upload.
uploads.rejected422Die Datei hat ihre Prüfungen nicht bestanden, zum Beispiel passt ihr Format nicht zu ihrem Namen. Die Meldung nennt den Grund.
uploads.not_found404Kein solcher Upload in der Organisation.