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:
- Crea un caricamento: comunica a Sursa nome, tipo e dimensione del file.
- Invia le parti, in qualsiasi ordine e, se vuoi, in parallelo. Una parte non riuscita si invia di nuovo da sola.
- Completa il caricamento con lo SHA-256 dell’intero file. Sursa ricompone il file e lo verifica.
- Usa l’ID del caricamento al posto del file: per una scheda, un archivio o un documento di una fonte personalizzata.
| Limite | Valore |
|---|---|
| Dimensione del file | Fino a 5 GB |
| Dimensione della parte | Scelta da Sursa in part_size, da 5 MiB a 100 MiB; l’ultima parte può essere più piccola |
| Numero di parti | Fino a 1000 |
| Tempo per completare | 24 ore dalla creazione del caricamento |
| Tempo per usare un caricamento completato | 24 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"
}
purpose | Il caricamento servirà per |
|---|---|
card_file | Una scheda: POST /v1/cards. |
archive | Un archivio di documenti: POST /v1/cards/archive. |
import_attachment | Un 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))
doneInviare 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
| Codice | Stato | Causa |
|---|---|---|
uploads.too_large | 413 | Il file è più grande di 5 GB o dell’intero spazio di archiviazione del piano dell’organizzazione. |
limits.storage | 402 | All’organizzazione non resta abbastanza spazio di archiviazione libero per il file. |
uploads.part_invalid | 400 | Un numero di parte fuori intervallo, oppure una parte fuori dalla dimensione consentita. |
uploads.checksum_mismatch | 422 | Lo 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_ready | 409 | Il caricamento non è ancora completato e verificato. Attendi ready. |
uploads.already_used | 409 | Il caricamento è già stato usato. Ogni caricamento si usa una sola volta. |
uploads.expired | 410 | Sono passate più di 24 ore. Crea un nuovo caricamento. |
uploads.rejected | 422 | Il file non ha superato i controlli, per esempio il suo formato non corrisponde al nome. Il messaggio dice perché. |
uploads.not_found | 404 | Nessun caricamento con questo ID nell’organizzazione. |