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:
- 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.
- 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.
- 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ó.
- 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ạn | Giá trị |
|---|---|
| Kích thước tệp | Tối đa 5 GB |
| Kích thước phần | Do Sursa chọn trong part_size, từ 5 MiB đến 100 MiB; phần cuối có thể nhỏ hơn |
| Số phần | Tối đa 1.000 |
| Thời gian để hoàn tất | 24 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ất | 24 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"
}
purpose | Lượt tải lên sẽ được dùng cho |
|---|---|
card_file | Một thẻ: POST /v1/cards. |
archive | Một tệp lưu trữ nén chứa tài liệu: POST /v1/cards/archive. |
import_attachment | Tệ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))
doneGử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ái | Nguyên nhân |
|---|---|---|
uploads.too_large | 413 | Tệ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.storage | 402 | Tổ chức không còn đủ dung lượng lưu trữ trống cho tệp. |
uploads.part_invalid | 400 | Số 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_mismatch | 422 | SHA-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_ready | 409 | Lượ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_used | 409 | Lượ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.expired | 410 | Đã quá 24 giờ. Hãy tạo lượt tải lên mới. |
uploads.rejected | 422 | Tệ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_found | 404 | Không có lượt tải lên này trong tổ chức. |