Téléverser des fichiers volumineux
Un fichier peut être envoyé en une seule requête POST /v1/cards jusqu’à la limite de
téléversement de l’installation (200 Mo par défaut). Au-delà, ou sur un réseau où une longue requête risque d’être
coupée, téléversez le fichier en plusieurs parties :
- Créez un téléversement : indiquez à Sursa le nom, le type et la taille du fichier.
- Envoyez les parties, dans n’importe quel ordre et en parallèle si vous le souhaitez. Une partie en échec est renvoyée seule.
- Terminez le téléversement avec le SHA-256 du fichier entier. Sursa assemble le fichier et le vérifie.
- Utilisez l’identifiant du téléversement à la place du fichier : pour une fiche, une archive ou un document d’une source personnalisée.
| Limite | Valeur |
|---|---|
| Taille du fichier | Jusqu’à 5 Go |
| Taille d’une partie | Choisie par Sursa dans part_size, de 5 Mio à 100 Mio ; la dernière partie peut être plus petite |
| Nombre de parties | Jusqu’à 1000 |
| Délai pour terminer | 24 heures après la création du téléversement |
| Délai pour utiliser un téléversement terminé | 24 heures |
1. Créer le téléversement
POST /v1/uploads prend le nom, le type et la taille du fichier, ainsi que
l’usage que vous allez en faire :
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 | Le téléversement servira pour |
|---|---|
card_file | Une fiche : POST /v1/cards. |
archive | Une archive de documents : POST /v1/cards/archive. |
import_attachment | Un fichier d’un document d’une source personnalisée, dans upload_id ou attachments. |
La clé a besoin de la portée de la destination du téléversement : cards:write pour card_file et archive,
import:write pour import_attachment.
Sursa choisit part_size et vous indique combien de parties envoyer. Si vous connaissez à l’avance le SHA-256 du
fichier, vous pouvez aussi le passer ici dans sha256.
2. Envoyer les parties
Découpez le fichier en part_count morceaux de part_size octets (le dernier contient le reste) et envoyez chacun
avec PUT /v1/uploads/{upload_id}/parts/{n}. Les parties sont numérotées à partir
de 1. Le corps contient les octets bruts de la partie.
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))
doneRenvoyer une partie la remplace : une partie en échec ou dont le délai a expiré est donc simplement renvoyée. Jusqu’à 4 parties en parallèle suffisent généralement à saturer une connexion rapide.
Vérifier les parties
Chaque partie passe par l’API : il n’existe pas de lien direct vers le stockage. La réponse à une partie porte son
en-tête ETag, le SHA-256 de la partie entre guillemets, et la même valeur dans etag du corps JSON.
GET /v1/uploads/{upload_id} liste dans parts les parties reçues jusqu’ici :
après une interruption, vous n’envoyez donc que les parties manquantes.
3. Terminer le téléversement
POST /v1/uploads/{upload_id}/complete prend le SHA-256 du fichier entier
sous forme de 64 caractères hexadécimaux et, si vous le souhaitez, l’etag de chaque partie dans parts, pour
vérifier aussi les parties :
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 assemble les parties, compare l’empreinte et vérifie le fichier comme tout téléversement : son format et, là où
elle est activée, l’analyse antivirus. La réponse est 200 quand la vérification est terminée et 202 tant qu’elle
est en cours. L’état du téléversement passe de processing à ready, ou à rejected avec le motif dans
reject_reason. Lisez-le avec GET /v1/uploads/{upload_id}.
4. Utiliser le téléversement
Passez upload_id là où vous enverriez le fichier. Chaque téléversement ne sert qu’une fois : ensuite, son état
est consumed.
Pour une fiche et une archive, envoyez upload_id dans un corps JSON plutôt que dans le formulaire
multipart/form-data. Ce corps désigne la collection par son nom dans 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 outil de téléversement 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));
}
}Erreurs
| Code | Statut | Cause |
|---|---|---|
uploads.too_large | 413 | Le fichier dépasse 5 Go, ou l’espace de stockage total de l’offre de l’organisation. |
limits.storage | 402 | L’organisation n’a plus assez d’espace de stockage libre pour le fichier. |
uploads.part_invalid | 400 | Un numéro de partie hors limites, ou une partie dont la taille sort des valeurs autorisées. |
uploads.checksum_mismatch | 422 | Le SHA-256 du fichier assemblé n’est pas celui que vous avez envoyé. Une partie est corrompue ou manquante : renvoyez les parties et terminez de nouveau le téléversement. |
uploads.not_ready | 409 | Le téléversement n’est pas encore terminé et vérifié. Attendez ready. |
uploads.already_used | 409 | Le téléversement a déjà été utilisé. Chaque téléversement ne sert qu’une fois. |
uploads.expired | 410 | Plus de 24 heures se sont écoulées. Créez un nouveau téléversement. |
uploads.rejected | 422 | Le fichier n’a pas passé les vérifications, par exemple parce que son format ne correspond pas à son nom. Le message indique pourquoi. |
uploads.not_found | 404 | Ce téléversement n’existe pas dans l’organisation. |