Chuyển tới nội dung
Sursa Developers

Tải lên tệp lớn

Một tệp có thể được gửi trong một yêu cầu POST /v1/cards duy nhất nếu không vượt giới hạn tải lên của bản cài đặt (mặc định 200 MB). Khi lớn hơn mức đó, hoặc trên mạng mà yêu cầu dài dễ bị ngắt, hãy tải tệp lên theo từng phần:

  1. Tạo lượt tải lên: cho Sursa biết tên, loại và kích thước của tệp.
  2. Gửi các phần, theo thứ tự bất kỳ và song song nếu muốn. Phần bị lỗi được gửi lại riêng.
  3. Hoàn tất lượt tải lên với SHA-256 của toàn bộ tệp. Sursa ghép tệp và kiểm tra nó.
  4. Dùng ID của lượt tải lên thay cho tệp: cho một thẻ, một tệp lưu trữ nén, hoặc một tài liệu của nguồn tùy chỉnh.
Giới hạnGiá trị
Kích thước tệpTối đa 5 GB
Kích thước phầnDo Sursa chọn trong part_size, từ 5 MiB đến 100 MiB; phần cuối có thể nhỏ hơn
Số phầnTối đa 1.000
Thời gian để hoàn tất24 giờ sau khi lượt tải lên được tạo
Thời gian để dùng lượt tải lên đã hoàn tất24 giờ

1. Tạo lượt tải lên

POST /v1/uploads nhận tên, loại và kích thước của tệp, cùng mục đích bạn sẽ dùng nó:

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"
}
purposeLượt tải lên sẽ được dùng cho
card_fileMột thẻ: POST /v1/cards.
archiveMột tệp lưu trữ nén chứa tài liệu: POST /v1/cards/archive.
import_attachmentTệp của một tài liệu thuộc nguồn tùy chỉnh, trong upload_id hoặc attachments.

Khóa cần phạm vi quyền của nơi lượt tải lên được dùng tới: cards:write cho card_file và archive, import:write cho import_attachment.

Sursa chọn part_size và cho bạn biết cần gửi bao nhiêu phần. Nếu biết trước SHA-256 của tệp, bạn cũng có thể truyền nó ở đây dưới dạng sha256.

2. Gửi các phần

Cắt tệp thành part_count phần, mỗi phần part_size byte (phần cuối là phần còn lại), và gửi từng phần bằng PUT /v1/uploads/{upload_id}/parts/{n}. Các phần được đánh số từ 1. Phần thân là các byte thô của phần đó.

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

Gửi lại một phần sẽ thay thế nó, nên phần bị lỗi hoặc hết thời gian chờ chỉ đơn giản được gửi lại. Thường thì tối đa 4 phần song song đã đủ dùng hết băng thông của một kết nối nhanh.

Kiểm tra các phần

Mọi phần đều đi qua API: không có liên kết trực tiếp tới kho lưu trữ. Phản hồi cho một phần có header ETag, là SHA-256 của phần đó đặt trong dấu ngoặc kép, và cùng giá trị đó trong etag của phần thân JSON. GET /v1/uploads/{upload_id} liệt kê các phần đã nhận được cho tới lúc đó trong parts, nên sau khi bị gián đoạn bạn chỉ cần gửi các phần còn thiếu.

3. Hoàn tất lượt tải lên

POST /v1/uploads/{upload_id}/complete nhận SHA-256 của toàn bộ tệp dưới dạng 64 ký tự thập lục phân và, nếu muốn, etag của từng phần trong parts để kiểm tra cả các phần:

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 ghép các phần, so sánh giá trị băm, và kiểm tra tệp như với mọi lượt tải lên: định dạng của tệp và, ở nơi được bật, quét virus. Phản hồi là 200 khi việc kiểm tra đã xong và 202 khi nó vẫn đang chạy. Trạng thái của lượt tải lên chuyển từ processing sang ready, hoặc sang rejected với lý do trong reject_reason. Hãy đọc nó bằng GET /v1/uploads/{upload_id}.

4. Dùng lượt tải lên

Truyền upload_id ở chỗ bạn vẫn gửi tệp. Mỗi lượt tải lên được dùng một lần: sau đó, trạng thái của nó là consumed.

Với thẻ và tệp lưu trữ nén, hãy gửi upload_id trong phần thân JSON thay cho biểu mẫu multipart/form-data. Phần thân này nhận bộ sưu tập theo tên của nó trong 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\"}"

Chương trình tải lên hoàn chỉnh

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

Lỗi

MãTrạng tháiNguyên nhân
uploads.too_large413Tệp lớn hơn 5 GB, hoặc lớn hơn toàn bộ dung lượng lưu trữ trong gói của tổ chức.
limits.storage402Tổ chức không còn đủ dung lượng lưu trữ trống cho tệp.
uploads.part_invalid400Số thứ tự phần nằm ngoài khoảng cho phép, hoặc kích thước phần ngoài giới hạn cho phép.
uploads.checksum_mismatch422SHA-256 của tệp đã ghép không khớp với giá trị bạn gửi. Một phần bị hỏng hoặc bị thiếu: hãy gửi lại các phần và hoàn tất lại.
uploads.not_ready409Lượt tải lên chưa được hoàn tất và kiểm tra. Hãy chờ trạng thái ready.
uploads.already_used409Lượt tải lên đã được dùng. Mỗi lượt tải lên chỉ được dùng một lần.
uploads.expired410Đã quá 24 giờ. Hãy tạo lượt tải lên mới.
uploads.rejected422Tệp không vượt qua các bước kiểm tra, ví dụ định dạng không khớp với tên tệp. Thông báo lỗi cho biết lý do.
uploads.not_found404Không có lượt tải lên này trong tổ chức.