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:
- Cree una subida: indique a Sursa el nombre, el tipo y el tamaño del archivo.
- Envíe las partes, en cualquier orden y, si lo desea, en paralelo. Una parte fallida se vuelve a enviar por separado.
- Complete la subida con el SHA-256 del archivo completo. Sursa ensambla el archivo y lo comprueba.
- Use el ID de la subida en lugar del archivo: para una tarjeta, un archivo comprimido o un documento de una fuente personalizada.
| Límite | Valor |
|---|---|
| Tamaño del archivo | Hasta 5 GB |
| Tamaño de una parte | Lo elige Sursa en part_size, de 5 MiB a 100 MiB; la última parte puede ser más pequeña |
| Número de partes | Hasta 1000 |
| Plazo para completarla | 24 horas desde que se crea la subida |
| Plazo para usar una subida completada | 24 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"
}
purpose | La subida se usará para |
|---|---|
card_file | Una tarjeta: POST /v1/cards. |
archive | Un archivo comprimido de documentos: POST /v1/cards/archive. |
import_attachment | Un 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))
doneEnviar 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ódigo | Estado | Causa |
|---|---|---|
uploads.too_large | 413 | El archivo supera los 5 GB o todo el almacenamiento del plan de la organización. |
limits.storage | 402 | A la organización no le queda suficiente almacenamiento libre para el archivo. |
uploads.part_invalid | 400 | Un número de parte fuera de rango o una parte con un tamaño fuera de lo permitido. |
uploads.checksum_mismatch | 422 | El 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_ready | 409 | La subida todavía no se ha completado y comprobado. Espere a ready. |
uploads.already_used | 409 | La subida ya se ha usado. Cada subida se usa una sola vez. |
uploads.expired | 410 | Han pasado más de 24 horas. Cree una subida nueva. |
uploads.rejected | 422 | El archivo no superó sus comprobaciones; por ejemplo, su formato no coincide con su nombre. El mensaje indica el motivo. |
uploads.not_found | 404 | No existe esa subida en la organización. |