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:
- Erstellen Sie einen Upload: Teilen Sie Sursa Name, Typ und Größe der Datei mit.
- Senden Sie die Teile in beliebiger Reihenfolge und auf Wunsch parallel. Ein fehlgeschlagener Teil wird einzeln erneut gesendet.
- Schließen Sie den Upload mit dem SHA-256 der ganzen Datei ab. Sursa setzt die Datei zusammen und prüft sie.
- Verwenden Sie die Upload-ID anstelle der Datei: für eine Karte, ein Archiv oder ein Dokument einer benutzerdefinierten Quelle.
| Limit | Wert |
|---|---|
| Dateigröße | Bis zu 5 GB |
| Größe eines Teils | Von Sursa in part_size gewählt, 5 MiB bis 100 MiB; der letzte Teil kann kleiner sein |
| Anzahl der Teile | Bis zu 1000 |
| Zeit bis zum Abschluss | 24 Stunden nach dem Erstellen des Uploads |
| Zeit zum Verwenden eines abgeschlossenen Uploads | 24 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"
}
purpose | Der Upload wird verwendet für |
|---|---|
card_file | Eine Karte: POST /v1/cards. |
archive | Ein Archiv mit Dokumenten: POST /v1/cards/archive. |
import_attachment | Eine 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))
doneEin 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
| Code | Status | Ursache |
|---|---|---|
uploads.too_large | 413 | Die Datei ist größer als 5 GB oder größer als der gesamte Speicherplatz des Tarifs der Organisation. |
limits.storage | 402 | Die Organisation hat nicht mehr genug freien Speicherplatz für die Datei. |
uploads.part_invalid | 400 | Eine Teilnummer außerhalb des Bereichs oder ein Teil außerhalb der erlaubten Größe. |
uploads.checksum_mismatch | 422 | Der 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_ready | 409 | Der Upload ist noch nicht abgeschlossen und geprüft. Warten Sie auf ready. |
uploads.already_used | 409 | Der Upload wurde bereits verwendet. Jeder Upload wird genau einmal verwendet. |
uploads.expired | 410 | Es sind mehr als 24 Stunden vergangen. Erstellen Sie einen neuen Upload. |
uploads.rejected | 422 | Die Datei hat ihre Prüfungen nicht bestanden, zum Beispiel passt ihr Format nicht zu ihrem Namen. Die Meldung nennt den Grund. |
uploads.not_found | 404 | Kein solcher Upload in der Organisation. |