Anar al contingut
Sursa Developers

Pujar fitxers grans

Un fitxer es pot enviar en una sola sol·licitud de POST /v1/cards fins al límit de pujada de la instal·lació (200 MB per defecte). Per sobre d’aquesta mida, o en una xarxa en què és probable que una sol·licitud llarga es talli, pugeu el fitxer per parts:

  1. Creeu una pujada: indiqueu a Sursa el nom, el tipus i la mida del fitxer.
  2. Envieu les parts, en qualsevol ordre i, si voleu, en paral·lel. Una part que falla es torna a enviar tota sola.
  3. Completeu la pujada amb el SHA-256 de tot el fitxer. Sursa munta el fitxer i el comprova.
  4. Feu servir l’ID de la pujada en lloc del fitxer: per a una entrada, un arxiu comprimit o un document d’una font personalitzada.
LímitValor
Mida del fitxerFins a 5 GB
Mida de la partLa tria Sursa a part_size, de 5 MiB a 100 MiB; l’última part pot ser més petita
Nombre de partsFins a 1000
Temps per completar la pujada24 hores després de crear-la
Temps per fer servir una pujada completada24 hores

1. Crear la pujada

POST /v1/uploads rep el nom, el tipus i la mida del fitxer, i la finalitat per a la qual el fareu servir:

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"
}
purposeLa pujada es farà servir per a
card_fileUna entrada: POST /v1/cards.
archiveUn arxiu comprimit de documents: POST /v1/cards/archive.
import_attachmentUn fitxer d’un document d’una font personalitzada, a upload_id o attachments.

La clau necessita l’àmbit del lloc on va la pujada: cards:write per a card_file i archive, import:write per a import_attachment.

Sursa tria part_size i us indica quantes parts heu d’enviar. Si coneixeu per endavant el SHA-256 del fitxer, també el podeu passar aquí com a sha256.

2. Enviar les parts

Talleu el fitxer en part_count trossos de part_size bytes (l’últim és la resta) i envieu-los un per un amb PUT /v1/uploads/{upload_id}/parts/{n}. Les parts es numeren a partir d’1. El cos són els bytes en brut de la part.

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

Si torneu a enviar una part, la substituïu, de manera que una part que ha fallat o que ha esgotat el temps d’espera simplement es torna a enviar. Amb fins a 4 parts en paral·lel, normalment se satura una connexió ràpida.

Comprovar les parts

Totes les parts passen per l’API: no hi ha enllaços directes a l’emmagatzematge. La resposta a una part té la capçalera ETag, el SHA-256 de la part entre cometes, i el mateix valor a l’etag del cos JSON. GET /v1/uploads/{upload_id} llista a parts les parts rebudes fins ara, de manera que després d’una interrupció només envieu les que falten.

3. Completar la pujada

POST /v1/uploads/{upload_id}/complete rep el SHA-256 de tot el fitxer en 64 caràcters hexadecimals i, opcionalment, l’etag de cada part a parts, per comprovar també les parts:

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 munta les parts, compara el hash i comprova el fitxer com comprova qualsevol pujada: el format i, on està activada, l’anàlisi antivirus. La resposta és 200 quan la comprovació ha acabat i 202 mentre encara s’executa. L’estat de la pujada passa de processing a ready, o a rejected amb el motiu a reject_reason. Consulteu-lo amb GET /v1/uploads/{upload_id}.

4. Fer servir la pujada

Passeu upload_id on enviaríeu el fitxer. Cada pujada es fa servir una sola vegada: després, el seu estat és consumed.

Per a una entrada i un arxiu comprimit, envieu upload_id en un cos JSON en lloc del formulari multipart/form-data. Aquest cos rep la col·lecció pel seu nom a 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\"}"

Un programa de pujada complet

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

Errors

CodiEstatCausa
uploads.too_large413El fitxer supera els 5 GB o tot l’emmagatzematge del pla de l’organització.
limits.storage402A l’organització no li queda prou emmagatzematge lliure per al fitxer.
uploads.part_invalid400Un número de part fora de l’interval o una part fora de la mida permesa.
uploads.checksum_mismatch422El SHA-256 del fitxer muntat no és el que heu enviat. Una part s’ha malmès o falta: torneu a enviar les parts i torneu a completar la pujada.
uploads.not_ready409La pujada encara no s’ha completat i comprovat. Espereu l’estat ready.
uploads.already_used409La pujada ja s’ha fet servir. Cada pujada es fa servir una sola vegada.
uploads.expired410Han passat més de 24 hores. Creeu una pujada nova.
uploads.rejected422El fitxer no ha superat les comprovacions, per exemple perquè el format no correspon al nom. El missatge n’indica el motiu.
uploads.not_found404No hi ha cap pujada amb aquest ID a l’organització.