Hoppa till innehållet
Sursa Developers

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:

  1. Skapa en uppladdning: tala om filens namn, typ och storlek för Sursa.
  2. Skicka delarna, i valfri ordning och gärna parallellt. En del som misslyckas skickas om för sig.
  3. Slutför uppladdningen med SHA-256 för hela filen. Sursa sätter ihop filen och kontrollerar den.
  4. 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änsVärde
FilstorlekUpp till 5 GB
DelstorlekVäljs av Sursa i part_size, från 5 MiB till 100 MiB; den sista delen kan vara mindre
Antal delarUpp till 1000
Tid att slutföra24 timmar efter att uppladdningen har skapats
Tid att använda en slutförd uppladdning24 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"
}
purposeUppladdningen används för
card_fileEtt kort: POST /v1/cards.
archiveEtt arkiv med dokument: POST /v1/cards/archive.
import_attachmentEn 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))
done

Skickar 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

KodStatusOrsak
uploads.too_large413Filen är större än 5 GB eller större än hela lagringsutrymmet i organisationens prisplan.
limits.storage402Organisationen har inte tillräckligt med ledigt lagringsutrymme kvar för filen.
uploads.part_invalid400Ett delnummer utanför intervallet, eller en del utanför den tillåtna storleken.
uploads.checksum_mismatch422SHA-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_ready409Uppladdningen är inte slutförd och kontrollerad än. Vänta på ready.
uploads.already_used409Uppladdningen har redan använts. Varje uppladdning används en gång.
uploads.expired410Mer än 24 timmar har gått. Skapa en ny uppladdning.
uploads.rejected422Filen klarade inte kontrollerna, till exempel stämmer dess format inte med namnet. Meddelandet anger varför.
uploads.not_found404Ingen sådan uppladdning finns i organisationen.