Aller au contenu
Sursa Developers

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 :

  1. Créez un téléversement : indiquez à Sursa le nom, le type et la taille du fichier.
  2. Envoyez les parties, dans n’importe quel ordre et en parallèle si vous le souhaitez. Une partie en échec est renvoyée seule.
  3. Terminez le téléversement avec le SHA-256 du fichier entier. Sursa assemble le fichier et le vérifie.
  4. Utilisez l’identifiant du téléversement à la place du fichier : pour une fiche, une archive ou un document d’une source personnalisée.
LimiteValeur
Taille du fichierJusqu’à 5 Go
Taille d’une partieChoisie par Sursa dans part_size, de 5 Mio à 100 Mio ; la dernière partie peut être plus petite
Nombre de partiesJusqu’à 1000
Délai pour terminer24 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"
}
purposeLe téléversement servira pour
card_fileUne fiche : POST /v1/cards.
archiveUne archive de documents : POST /v1/cards/archive.
import_attachmentUn 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))
done

Renvoyer 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

CodeStatutCause
uploads.too_large413Le fichier dépasse 5 Go, ou l’espace de stockage total de l’offre de l’organisation.
limits.storage402L’organisation n’a plus assez d’espace de stockage libre pour le fichier.
uploads.part_invalid400Un numéro de partie hors limites, ou une partie dont la taille sort des valeurs autorisées.
uploads.checksum_mismatch422Le 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_ready409Le téléversement n’est pas encore terminé et vérifié. Attendez ready.
uploads.already_used409Le téléversement a déjà été utilisé. Chaque téléversement ne sert qu’une fois.
uploads.expired410Plus de 24 heures se sont écoulées. Créez un nouveau téléversement.
uploads.rejected422Le 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_found404Ce téléversement n’existe pas dans l’organisation.