Gå til indholdet
Sursa Developers

Upload store filer

En fil kan sendes i én anmodning til POST /v1/cards op til installationens grænse for uploads (som standard 200 MB). Er filen større, eller er du på et netværk, hvor en lang anmodning sandsynligvis bliver afbrudt, så upload filen i dele:

  1. Opret en upload: Fortæl Sursa filens navn, type og størrelse.
  2. Send delene i vilkårlig rækkefølge, gerne parallelt. En del, der mislykkes, sendes igen for sig.
  3. Afslut uploaden med SHA-256 for hele filen. Sursa samler filen og kontrollerer den.
  4. Brug uploadens id i stedet for filen: til et kort, et arkiv eller et dokument i en brugerdefineret kilde.
GrænseVærdi
FilstørrelseOp til 5 GB
DelstørrelseVælges af Sursa i part_size, fra 5 MiB til 100 MiB; den sidste del kan være mindre
Antal deleOp til 1000
Frist for afslutning24 timer efter, at uploaden er oprettet
Frist for brug af en afsluttet upload24 timer

1. Opret uploaden

POST /v1/uploads tager filens navn, type og størrelse og det, du vil bruge den til:

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"
}
purposeUploaden bruges til
card_fileEt kort: POST /v1/cards.
archiveEt arkiv med dokumenter: POST /v1/cards/archive.
import_attachmentEn fil til et dokument i en brugerdefineret kilde, i upload_id eller attachments.

Nøglen skal have scopet for det sted, uploaden skal bruges: cards:write til card_file og archive, import:write til import_attachment.

Sursa vælger part_size og fortæller dig, hvor mange dele du skal sende. Kender du filens SHA-256 på forhånd, kan du også angive den her som sha256.

2. Send delene

Del filen i part_count stykker på part_size byte (det sidste stykke er resten), og send hvert stykke med PUT /v1/uploads/{upload_id}/parts/{n}. Delene nummereres fra 1. Brødteksten er delens rå bytes.

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

Sender du en del igen, erstattes den, så en del, der mislykkedes eller fik timeout, blot sendes igen. Op til 4 dele parallelt udnytter som regel en hurtig forbindelse fuldt ud.

Kontrollér delene

Alle dele går gennem API’et: Der er ingen direkte links til lageret. Svaret på en del har headeren ETag, delens SHA-256 i anførselstegn, og den samme værdi i etag i JSON-brødteksten. GET /v1/uploads/{upload_id} viser de dele, der er modtaget indtil nu, i parts, så du efter en afbrydelse kun sender dem, der mangler.

3. Afslut uploaden

POST /v1/uploads/{upload_id}/complete tager SHA-256 for hele filen som 64 hexadecimale tegn og eventuelt etag for hver del i parts for også at kontrollere delene:

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 samler delene, sammenligner hashen og kontrollerer filen, som den kontrollerer enhver upload: dens format og, hvor det er slået til, antivirus. Svaret er 200, når kontrollen er færdig, og 202, mens den stadig kører. Uploadens status går fra processing til ready eller til rejected med årsagen i reject_reason. Læs den med GET /v1/uploads/{upload_id}.

4. Brug uploaden

Angiv upload_id der, hvor du ellers ville sende filen. Hver upload bruges én gang: Derefter er dens status consumed.

Til et kort og et arkiv sender du upload_id i en JSON-brødtekst i stedet for formularen multipart/form-data. Denne brødtekst angiver samlingen med dens navn 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\"}"

Et komplet uploadprogram

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

Fejl

KodeStatusÅrsag
uploads.too_large413Filen er større end 5 GB eller større end hele lagerpladsen i organisationens prisplan.
limits.storage402Organisationen har ikke nok ledig lagerplads tilbage til filen.
uploads.part_invalid400Et delnummer uden for intervallet eller en del uden for den tilladte størrelse.
uploads.checksum_mismatch422SHA-256 for den samlede fil er ikke den, du sendte. En del er beskadiget eller mangler: Send delene igen, og afslut igen.
uploads.not_ready409Uploaden er endnu ikke afsluttet og kontrolleret. Vent på ready.
uploads.already_used409Uploaden er allerede brugt. Hver upload bruges én gang.
uploads.expired410Der er gået mere end 24 timer. Opret en ny upload.
uploads.rejected422Filen bestod ikke kontrollen, fx passer dens format ikke til navnet. Meddelelsen fortæller hvorfor.
uploads.not_found404Ingen sådan upload i organisationen.