Пређите на садржај
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У организацији нема таквог отпремања.