ข้ามไปยังเนื้อหา
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_mismatch422ค่า SHA-256 ของไฟล์ที่ประกอบแล้วไม่ตรงกับค่าที่คุณส่ง มีส่วนที่เสียหายหรือขาดหายไป ให้ส่งทุกส่วนใหม่แล้วทำการอัปโหลดให้เสร็จอีกครั้ง
uploads.not_ready409การอัปโหลดยังไม่เสร็จหรือยังตรวจไม่เสร็จ ให้รอจนเป็น ready
uploads.already_used409การอัปโหลดนี้ถูกใช้ไปแล้ว การอัปโหลดแต่ละรายการใช้ได้ครั้งเดียว
uploads.expired410ผ่านไปแล้วมากกว่า 24 ชั่วโมง ให้สร้างการอัปโหลดใหม่
uploads.rejected422ไฟล์ไม่ผ่านการตรวจ เช่น รูปแบบไฟล์ไม่ตรงกับชื่อไฟล์ ข้อความแจ้งข้อผิดพลาดบอกสาเหตุ
uploads.not_found404ไม่มีการอัปโหลดนี้ในองค์กร