Preskoči na sadržaj
Sursa Developers

Učitavanje velikih datoteka

Datoteka može ići u jednom zahtjevu POST /v1/cards do ograničenja učitavanja instalacije (zadano 200 MB). Iznad toga, ili na mreži na kojoj će se dugi zahtjev vjerojatno prekinuti, učitajte datoteku u dijelovima:

  1. Stvorite učitavanje: javite Sursi ime, vrstu i veličinu datoteke.
  2. Pošaljite dijelove, bilo kojim redoslijedom i, ako želite, paralelno. Neuspjeli dio šalje se ponovno zasebno.
  3. Dovršite učitavanje sažetkom SHA-256 cijele datoteke. Sursa sastavlja datoteku i provjerava je.
  4. ID učitavanja upotrijebite umjesto datoteke: za karticu, arhivu ili dokument vlastitog izvora.
OgraničenjeVrijednost
Veličina datotekeDo 5 GB
Veličina dijelaOdabire je Sursa u part_size, od 5 MiB do 100 MiB; posljednji dio može biti manji
Broj dijelovaDo 1000
Rok za dovršetak24 sata nakon stvaranja učitavanja
Rok za korištenje dovršenog učitavanja24 sata

1. Stvorite učitavanje

POST /v1/uploads prima ime, vrstu i veličinu datoteke te namjenu za koju ćete je koristiti:

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"
}
purposeUčitavanje će se koristiti za
card_fileKarticu: POST /v1/cards.
archiveArhivu dokumenata: POST /v1/cards/archive.
import_attachmentDatoteku dokumenta vlastitog izvora, u upload_id ili attachments.

Ključu treba opseg mjesta kamo učitavanje ide: cards:write za card_file i archive, import:write za import_attachment.

Sursa odabire part_size i javlja vam koliko dijelova trebate poslati. Ako SHA-256 datoteke znate unaprijed, možete ga i ovdje proslijediti kao sha256.

2. Pošaljite dijelove

Podijelite datoteku na part_count dijelova veličine part_size bajtova (posljednji je ostatak) i svaki pošaljite pomoću PUT /v1/uploads/{upload_id}/parts/{n}. Dijelovi se numeriraju od 1. Tijelo su sirovi bajtovi dijela.

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

Ponovno slanje dijela zamjenjuje ga, pa se dio koji nije uspio ili je istekao jednostavno pošalje ponovno. Do 4 dijela paralelno obično potpuno iskorištavaju brzu vezu.

Provjera dijelova

Svaki dio prolazi kroz API: izravnih poveznica na pohranu nema. Odgovor na dio ima zaglavlje ETag, SHA-256 dijela u navodnicima, i istu vrijednost u etag JSON tijela. GET /v1/uploads/{upload_id} u parts navodi dosad primljene dijelove, pa nakon prekida šaljete samo one koji nedostaju.

3. Dovršite učitavanje

POST /v1/uploads/{upload_id}/complete prima SHA-256 cijele datoteke kao 64 heksadekadska znaka i, po želji, etag svakog dijela u parts, kako bi provjerio i dijelove:

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 sastavlja dijelove, uspoređuje sažetak i provjerava datoteku kao i svako drugo učitavanje: provjerava njezin format i, ondje gdje je uključena, provodi antivirusnu provjeru. Odgovor je 200 kad je provjera gotova i 202 dok još traje. Status učitavanja prelazi iz processing u ready ili u rejected s razlogom u reject_reason. Pročitajte ga pomoću GET /v1/uploads/{upload_id}.

4. Iskoristite učitavanje

Proslijedite upload_id ondje gdje biste poslali datoteku. Svako se učitavanje koristi jednom: nakon toga je njegov status consumed.

Za karticu i arhivu pošaljite upload_id u JSON tijelu umjesto obrasca multipart/form-data. Takvo tijelo prima zbirku prema njezinu nazivu u 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\"}"

Potpuni program za učitavanje

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

Pogreške

KodStatusUzrok
uploads.too_large413Datoteka je veća od 5 GB ili od cijelog prostora za pohranu prema planu organizacije.
limits.storage402Organizaciji nije preostalo dovoljno slobodnog prostora za pohranu za datoteku.
uploads.part_invalid400Broj dijela izvan raspona ili dio izvan dopuštene veličine.
uploads.checksum_mismatch422SHA-256 sastavljene datoteke nije onaj koji ste poslali. Neki je dio oštećen ili nedostaje: ponovno pošaljite dijelove i ponovno dovršite učitavanje.
uploads.not_ready409Učitavanje još nije dovršeno i provjereno. Pričekajte ready.
uploads.already_used409Učitavanje je već iskorišteno. Svako se učitavanje koristi jednom.
uploads.expired410Prošlo je više od 24 sata. Stvorite novo učitavanje.
uploads.rejected422Datoteka nije prošla provjere, na primjer njezin format ne odgovara njezinu imenu. Poruka navodi razlog.
uploads.not_found404Takvo učitavanje ne postoji u organizaciji.