Ladda upp stora filer
En fil kan skickas i ett enda anrop till POST /v1/cards upp till installationens
gräns för uppladdningar (200 MB som standard). Över den gränsen, eller i ett nätverk där ett långt anrop lätt bryts,
laddar du upp filen i delar:
- Skapa en uppladdning: tala om filens namn, typ och storlek för Sursa.
- Skicka delarna, i valfri ordning och gärna parallellt. En del som misslyckas skickas om för sig.
- Slutför uppladdningen med SHA-256 för hela filen. Sursa sätter ihop filen och kontrollerar den.
- Använd uppladdningens id i stället för filen: för ett kort, ett arkiv eller ett dokument i en egen källa.
| Gräns | Värde |
|---|---|
| Filstorlek | Upp till 5 GB |
| Delstorlek | Väljs av Sursa i part_size, från 5 MiB till 100 MiB; den sista delen kan vara mindre |
| Antal delar | Upp till 1000 |
| Tid att slutföra | 24 timmar efter att uppladdningen har skapats |
| Tid att använda en slutförd uppladdning | 24 timmar |
1. Skapa uppladdningen
POST /v1/uploads tar filens namn, typ och storlek samt vad du ska använda den
till:
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 | Uppladdningen används för |
|---|---|
card_file | Ett kort: POST /v1/cards. |
archive | Ett arkiv med dokument: POST /v1/cards/archive. |
import_attachment | En fil till ett dokument i en egen källa, i upload_id eller attachments. |
Nyckeln behöver omfånget för det ställe som uppladdningen ska till: cards:write för card_file och archive,
import:write för import_attachment.
Sursa väljer part_size och talar om hur många delar du ska skicka. Om du känner till filens SHA-256 i förväg kan du
skicka den som sha256 redan här.
2. Skicka delarna
Dela filen i part_count bitar om part_size byte (den sista är resten) och skicka var och en med
PUT /v1/uploads/{upload_id}/parts/{n}. Delarna numreras från 1. Brödtexten är
delens råa byte.
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))
doneSkickar du en del igen ersätts den, så en del som misslyckades eller överskred tidsgränsen skickas helt enkelt igen. Upp till 4 delar parallellt brukar räcka för att fylla en snabb anslutning.
Kontrollera delarna
Varje del går via API:et: det finns inga direktlänkar till lagringen. Svaret på en del har huvudet ETag, delens
SHA-256 inom citattecken, och samma värde i etag i JSON-brödtexten.
GET /v1/uploads/{upload_id} listar de delar som har tagits emot hittills i parts,
så efter ett avbrott skickar du bara de som saknas.
3. Slutför uppladdningen
POST /v1/uploads/{upload_id}/complete tar SHA-256 för hela filen som 64
hexadecimala tecken och, om du vill, etag för varje del i parts, så att även delarna kontrolleras:
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 sätter ihop delarna, jämför hashen och kontrollerar filen som vid varje uppladdning: dess format och, där det är
aktiverat, virussökning. Svaret är 200 när kontrollen är klar och 202 medan den fortfarande pågår. Uppladdningens
status går från processing till ready, eller till rejected med orsaken i reject_reason. Läs den med
GET /v1/uploads/{upload_id}.
4. Använd uppladdningen
Skicka upload_id där du annars skulle skicka filen. Varje uppladdning används en gång: därefter är dess status
consumed.
För ett kort och ett arkiv skickar du upload_id i en JSON-brödtext i stället för i formuläret multipart/form-data.
Den här brödtexten anger samlingen med dess namn i 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\"}"Ett komplett uppladdningsprogram
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));
}
}Fel
| Kod | Status | Orsak |
|---|---|---|
uploads.too_large | 413 | Filen är större än 5 GB eller större än hela lagringsutrymmet i organisationens prisplan. |
limits.storage | 402 | Organisationen har inte tillräckligt med ledigt lagringsutrymme kvar för filen. |
uploads.part_invalid | 400 | Ett delnummer utanför intervallet, eller en del utanför den tillåtna storleken. |
uploads.checksum_mismatch | 422 | SHA-256 för den sammansatta filen är inte den som du skickade. En del var skadad eller saknas: skicka delarna igen och slutför igen. |
uploads.not_ready | 409 | Uppladdningen är inte slutförd och kontrollerad än. Vänta på ready. |
uploads.already_used | 409 | Uppladdningen har redan använts. Varje uppladdning används en gång. |
uploads.expired | 410 | Mer än 24 timmar har gått. Skapa en ny uppladdning. |
uploads.rejected | 422 | Filen klarade inte kontrollerna, till exempel stämmer dess format inte med namnet. Meddelandet anger varför. |
uploads.not_found | 404 | Ingen sådan uppladdning finns i organisationen. |