Ir al contenido
Sursa Developers

Subir archivos grandes

Un archivo puede ir en una sola solicitud de POST /v1/cards hasta el límite de subida de la instalación (200 MB de forma predeterminada). Por encima de ese tamaño, o en una red en la que es probable que una solicitud larga se corte, suba el archivo por partes:

  1. Cree una subida: indique a Sursa el nombre, el tipo y el tamaño del archivo.
  2. Envíe las partes, en cualquier orden y, si lo desea, en paralelo. Una parte fallida se vuelve a enviar por separado.
  3. Complete la subida con el SHA-256 del archivo completo. Sursa ensambla el archivo y lo comprueba.
  4. Use el ID de la subida en lugar del archivo: para una tarjeta, un archivo comprimido o un documento de una fuente personalizada.
LímiteValor
Tamaño del archivoHasta 5 GB
Tamaño de una parteLo elige Sursa en part_size, de 5 MiB a 100 MiB; la última parte puede ser más pequeña
Número de partesHasta 1000
Plazo para completarla24 horas desde que se crea la subida
Plazo para usar una subida completada24 horas

1. Crear la subida

POST /v1/uploads recibe el nombre, el tipo y el tamaño del archivo, y para qué lo va a usar:

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 subida se usará para
card_fileUna tarjeta: POST /v1/cards.
archiveUn archivo comprimido de documentos: POST /v1/cards/archive.
import_attachmentUn archivo de un documento de una fuente personalizada, en upload_id o attachments.

La clave necesita el ámbito del lugar al que va la subida: cards:write para card_file y archive, import:write para import_attachment.

Sursa elige part_size y le indica cuántas partes debe enviar. Si conoce de antemano el SHA-256 del archivo, también puede pasarlo aquí como sha256.

2. Enviar las partes

Divida el archivo en part_count trozos de part_size bytes (el último es el resto) y envíe cada uno con PUT /v1/uploads/{upload_id}/parts/{n}. Las partes se numeran desde 1. El cuerpo son los bytes sin procesar de la parte.

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

Enviar de nuevo una parte la sustituye, así que una parte que falló o agotó el tiempo de espera simplemente se vuelve a enviar. Con hasta 4 partes en paralelo suele saturarse una conexión rápida.

Comprobar las partes

Todas las partes pasan por la API: no hay enlaces directos al almacenamiento. La respuesta a una parte lleva su encabezado ETag, el SHA-256 de la parte entre comillas, y el mismo valor en etag del cuerpo JSON. GET /v1/uploads/{upload_id} enumera en parts las partes recibidas hasta el momento, así que después de una interrupción solo tiene que enviar las que faltan.

3. Completar la subida

POST /v1/uploads/{upload_id}/complete recibe el SHA-256 del archivo completo como 64 caracteres hexadecimales y, si lo desea, el etag de cada parte en parts, para comprobar también las partes:

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 ensambla las partes, compara el hash y comprueba el archivo como comprueba cualquier subida: su formato y, donde está activado, el análisis antivirus. La respuesta es 200 cuando la comprobación ha terminado y 202 mientras sigue en curso. El estado de la subida pasa de processing a ready, o a rejected con el motivo en reject_reason. Léalo con GET /v1/uploads/{upload_id}.

4. Usar la subida

Pase upload_id donde enviaría el archivo. Cada subida se usa una sola vez: después, su estado es consumed.

Para una tarjeta y un archivo comprimido, envíe upload_id en un cuerpo JSON en lugar del formulario multipart/form-data. Este cuerpo recibe la colección por su nombre en 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 subida completo

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

Errores

CódigoEstadoCausa
uploads.too_large413El archivo supera los 5 GB o todo el almacenamiento del plan de la organización.
limits.storage402A la organización no le queda suficiente almacenamiento libre para el archivo.
uploads.part_invalid400Un número de parte fuera de rango o una parte con un tamaño fuera de lo permitido.
uploads.checksum_mismatch422El SHA-256 del archivo ensamblado no es el que envió. Una parte se dañó o falta: vuelva a enviar las partes y a completar la subida.
uploads.not_ready409La subida todavía no se ha completado y comprobado. Espere a ready.
uploads.already_used409La subida ya se ha usado. Cada subida se usa una sola vez.
uploads.expired410Han pasado más de 24 horas. Cree una subida nueva.
uploads.rejected422El archivo no superó sus comprobaciones; por ejemplo, su formato no coincide con su nombre. El mensaje indica el motivo.
uploads.not_found404No existe esa subida en la organización.