Към съдържанието
Sursa Developers

Качване на големи файлове

Файлът може да се изпрати с една заявка POST /v1/cards, ако не надвишава лимита за качване на инсталацията (по подразбиране 200 MB). Над този размер или в мрежа, където дълга заявка вероятно ще прекъсне, качвайте файла на части:

  1. Създайте качване: съобщете на Sursa името, типа и размера на файла.
  2. Изпратете частите в произволен ред и, ако желаете, паралелно. Неуспешна част се изпраща отново самостоятелно.
  3. Завършете качването със SHA-256 на целия файл. Sursa сглобява файла и го проверява.
  4. Използвайте ID на качването вместо файла: за карта, за архив или за документ на собствен източник.
ЛимитСтойност
Размер на файлаДо 5 GB
Размер на часттаИзбира го Sursa в part_size, от 5 MiB до 100 MiB; последната част може да е по-малка
Брой частиДо 1000
Срок за завършване24 часа след създаването на качването
Срок за използване на завършено качване24 часа

1. Създайте качването

POST /v1/uploads приема името, типа и размера на файла, както и за какво ще го използвате:

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Качването ще се използва за
card_fileКарта: POST /v1/cards.
archiveАрхив с документи: POST /v1/cards/archive.
import_attachmentФайл към документ на собствен източник, в upload_id или attachments.

Ключът се нуждае от обхвата на мястото, за което е качването: cards:write – за card_file и archive, import:write – за import_attachment.

Sursa избира part_size и ви казва колко части да изпратите. Ако знаете предварително SHA-256 на файла, може да го подадете и тук като sha256.

2. Изпратете частите

Разделете файла на part_count парчета по part_size байта (последното е остатъкът) и изпратете всяко с PUT /v1/uploads/{upload_id}/parts/{n}. Частите се номерират от 1. Тялото е необработените байтове на частта.

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

Повторното изпращане на част я заменя, затова част, която е била неуспешна или за която е изтекло времето за изчакване, просто се изпраща отново. До 4 части паралелно обикновено натоварват напълно една бърза връзка.

Проверка на частите

Всяка част минава през API: няма преки връзки към хранилището. Отговорът за част има заглавка ETag – SHA-256 на частта в кавички, и същата стойност в etag в тялото на JSON отговора. GET /v1/uploads/{upload_id} изброява в parts получените досега части, затова след прекъсване изпращате само липсващите.

3. Завършете качването

POST /v1/uploads/{upload_id}/complete приема SHA-256 на целия файл като 64 шестнайсетични знака и по желание etag на всяка част в parts, за да се проверят и частите:

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 сглобява частите, сравнява хеша и проверява файла, както проверява всяко качване: формата му и, където е включена, антивирусна проверка. Отговорът е 200, когато проверката е приключила, и 202, докато тя още тече. Статусът на качването преминава от processing в ready или в rejected с причината в reject_reason. Прочетете го с GET /v1/uploads/{upload_id}.

4. Използвайте качването

Подайте upload_id там, където бихте изпратили файла. Всяко качване се използва веднъж: след това статусът му е consumed.

За карта и за архив изпратете upload_id в JSON тяло вместо във формата multipart/form-data. Това тяло приема колекцията по нейното име в 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\"}"

Пълна програма за качване

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));
  }
}

Грешки

КодСтатусПричина
uploads.too_large413Файлът е по-голям от 5 GB или от цялото хранилище по плана на организацията.
limits.storage402В хранилището на организацията не е останало достатъчно свободно място за файла.
uploads.part_invalid400Номер на част извън диапазона или част с размер извън допустимия.
uploads.checksum_mismatch422SHA-256 на сглобения файл не съвпада с изпратения от вас. Някоя част е повредена или липсва: изпратете частите отново и завършете качването пак.
uploads.not_ready409Качването още не е завършено и проверено. Изчакайте ready.
uploads.already_used409Качването вече е използвано. Всяко качване се използва веднъж.
uploads.expired410Изминали са повече от 24 часа. Създайте ново качване.
uploads.rejected422Файлът не е преминал проверките, например форматът му не съответства на името му. Съобщението казва защо.
uploads.not_found404В организацията няма такова качване.