Vai al contenuto
Sursa Developers

Caricare file di grandi dimensioni

Un file può essere inviato in una singola richiesta di POST /v1/cards fino al limite di caricamento dell’installazione (200 MB per impostazione predefinita). Oltre questa soglia, o su una rete dove una richiesta lunga rischia di interrompersi, carica il file in parti:

  1. Crea un caricamento: comunica a Sursa nome, tipo e dimensione del file.
  2. Invia le parti, in qualsiasi ordine e, se vuoi, in parallelo. Una parte non riuscita si invia di nuovo da sola.
  3. Completa il caricamento con lo SHA-256 dell’intero file. Sursa ricompone il file e lo verifica.
  4. Usa l’ID del caricamento al posto del file: per una scheda, un archivio o un documento di una fonte personalizzata.
LimiteValore
Dimensione del fileFino a 5 GB
Dimensione della parteScelta da Sursa in part_size, da 5 MiB a 100 MiB; l’ultima parte può essere più piccola
Numero di partiFino a 1000
Tempo per completare24 ore dalla creazione del caricamento
Tempo per usare un caricamento completato24 ore

1. Creare il caricamento

POST /v1/uploads accetta nome, tipo e dimensione del file, e l’uso che ne farai:

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"
}
purposeIl caricamento servirà per
card_fileUna scheda: POST /v1/cards.
archiveUn archivio di documenti: POST /v1/cards/archive.
import_attachmentUn file di un documento di una fonte personalizzata, in upload_id o attachments.

La chiave ha bisogno dell’ambito del punto di destinazione del caricamento: cards:write per card_file e archive, import:write per import_attachment.

Sursa sceglie part_size e ti dice quante parti inviare. Se conosci in anticipo lo SHA-256 del file, puoi passarlo anche qui come sha256.

2. Inviare le parti

Dividi il file in part_count pezzi di part_size byte (l’ultimo contiene il resto) e invia ciascuno con PUT /v1/uploads/{upload_id}/parts/{n}. Le parti sono numerate a partire da 1. Il corpo è costituito dai byte grezzi della 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

Inviare di nuovo una parte la sostituisce, quindi una parte non riuscita o andata in timeout si invia semplicemente di nuovo. Fino a 4 parti in parallelo di solito saturano una connessione veloce.

Verificare le parti

Ogni parte passa attraverso l’API: non ci sono link diretti allo storage. La risposta a una parte ha l’intestazione ETag, lo SHA-256 della parte tra virgolette, e lo stesso valore in etag del corpo JSON. GET /v1/uploads/{upload_id} elenca in parts le parti ricevute finora, quindi dopo un’interruzione invii solo quelle mancanti.

3. Completare il caricamento

POST /v1/uploads/{upload_id}/complete accetta lo SHA-256 dell’intero file come 64 caratteri esadecimali e, facoltativamente, l’etag di ogni parte in parts, per verificare anche le parti:

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 ricompone le parti, confronta l’hash e verifica il file come qualsiasi caricamento: il formato e, dove è attiva, la scansione antivirus. La risposta è 200 quando la verifica è terminata e 202 mentre è ancora in corso. Lo stato del caricamento passa da processing a ready, oppure a rejected con il motivo in reject_reason. Leggilo con GET /v1/uploads/{upload_id}.

4. Usare il caricamento

Passa upload_id dove invieresti il file. Ogni caricamento si usa una sola volta: dopo, il suo stato è consumed.

Per una scheda e un archivio, invia upload_id in un corpo JSON al posto del modulo multipart/form-data. Questo corpo indica la raccolta con il suo nome in 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 programma di caricamento 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));
  }
}

Errori

CodiceStatoCausa
uploads.too_large413Il file è più grande di 5 GB o dell’intero spazio di archiviazione del piano dell’organizzazione.
limits.storage402All’organizzazione non resta abbastanza spazio di archiviazione libero per il file.
uploads.part_invalid400Un numero di parte fuori intervallo, oppure una parte fuori dalla dimensione consentita.
uploads.checksum_mismatch422Lo SHA-256 del file ricomposto non è quello che hai inviato. Una parte è danneggiata o manca: invia di nuovo le parti e completa di nuovo.
uploads.not_ready409Il caricamento non è ancora completato e verificato. Attendi ready.
uploads.already_used409Il caricamento è già stato usato. Ogni caricamento si usa una sola volta.
uploads.expired410Sono passate più di 24 ore. Crea un nuovo caricamento.
uploads.rejected422Il file non ha superato i controlli, per esempio il suo formato non corrisponde al nome. Il messaggio dice perché.
uploads.not_found404Nessun caricamento con questo ID nell’organizzazione.