Chuyển tới nội dung chính

Tải lên tệp dung lượng lớn

Hiện trạng và bối cảnh

Để đáp ứng nhu cầu kinh doanh ngày càng tăng và vượt qua giới hạn dung lượng tệp 10MB hiện tại trong các thông số kỹ thuật tải lên tệp, TTS Partner Team chính thức ra mắt giải pháp tải lên tệp dung lượng lớn. Giải pháp trước đây nạp toàn bộ tệp vào bộ nhớ, gây ra nguy cơ tràn bộ nhớ (Out of Memory - OOM) tiềm ẩn cho dịch vụ gateway và hạn chế việc mở rộng các tình huống kinh doanh.

Giải pháp mới hướng đến việc cung cấp khả năng tải lên tệp dung lượng lớn ổn định và hiệu quả, hỗ trợ tệp đơn lên đến 1GB nhằm đáp ứng nhu cầu tải lên video, hình ảnh độ phân giải cao và các tệp dung lượng lớn khác.

  • Tình huống Customer Service hỗ trợ dung lượng tệp tối đa là 100 MB, trong khi tình huống Shoppable Video hỗ trợ dung lượng tệp tối đa là 500 MB.

Tổng quan giải pháp kỹ thuật

Giải pháp này áp dụng phương pháp "business file gateway + tải lên theo phân đoạn (chunked upload)" để hỗ trợ chuyển tệp dung lượng lớn. Cốt lõi của chiến lược này là cắt nhỏ tệp lớn ở phía client, tải từng phân đoạn lên một file gateway chuyên dụng, và cuối cùng để gateway hợp nhất chúng ở phía server.

  • Tải lên trực tiếp cho tệp nhỏ: Với các tệp nhỏ hơn 10MB, nhà phát triển có thể tiếp tục sử dụng phương thức tải lên trực tiếp hiện có mà không cần phân đoạn, giúp đơn giản hóa quy trình cho tệp nhỏ.
  • Tải lên theo phân đoạn (Chunked Upload): Với các tệp lớn hơn 10MB, bắt buộc phải sử dụng mô hình tải lên theo phân đoạn được cung cấp trong giải pháp này.

Để đảm bảo quy trình tải lên ổn định và an toàn, giải pháp bao gồm một số khả năng quan trọng:

  • Upload Token: upload_token thu được từ API khởi tạo tải lên là thông tin xác thực duy nhất cho các thao tác tải lên phân đoạn tiếp theo. Token này có giới hạn thời gian nhằm đảm bảo mỗi phiên tải lên là độc lập và an toàn.
  • Bảo mật và xác thực: Giải pháp kế thừa hệ thống xác thực phổ quát của API gateway và bao gồm cơ chế xác thực tùy chỉnh theo từng tình huống cụ thể cho việc tải lên tệp, nhằm hỗ trợ các lời gọi từ những bên kinh doanh khác nhau. Ngoài ra, các cơ chế giới hạn tần suất và dung lượng ở cấp người dùng được áp dụng để ngăn chặn các hành vi tải lên độc hại.
  • Tuân thủ: File gateway và API gateway dùng chung tên miền và tái sử dụng các chính sách điều phối lưu lượng hiện có, đảm bảo dữ liệu được truyền trong phạm vi đơn vị tuân thủ tương ứng.

Cách tích hợp

Chi tiết quy trình tải lên tệp dung lượng lớn

Việc tải lên tệp dung lượng lớn được chia thành ba bước chính: 1. Khởi tạo tải lên -> 2. Tải lên phân đoạn và hoàn tất -> 3. Liên kết tài nguyên kinh doanh.

IDStepFlowAPI PathDescription
1Khởi tạo tải lênImage[POST] /open/:version/file_upload/initTrước khi tải lên bất kỳ phân đoạn tệp nào, bạn phải gọi API khởi tạo để thông báo cho server về thông tin cơ bản của tệp và lấy upload_url cùng upload_token cần thiết cho các thao tác tiếp theo.
2Tải lên phân đoạn và hoàn tấtImage[PUT] <uploader_url> (do API init trả về)Sau khi lấy được upload_urlupload_token, bạn cần chia tệp thành nhiều phân đoạn ở phía client và đánh số từng phân đoạn (chunk_num, bắt đầu từ 1). Sau đó, tải từng phân đoạn lên upload_url bằng cách gọi lặp lại yêu cầu PUT.
Lưu ý quan trọng: Gateway tự động xử lý việc hợp nhất tệp. Sau khi phân đoạn cuối cùng được tải lên thành công, gateway sẽ kích hoạt thao tác hợp nhất và trả về resource_id cuối cùng trực tiếp trong phản hồi của lần tải lên phân đoạn cuối. Không cần gọi thêm API nào để thông báo cho server hợp nhất tệp.
3Liên kết tài nguyênImageVí dụ, trong nghiệp vụ Customer Service, API liên kết tài nguyên là
[POST]/customer_service/:version/conversations/{conversation_id}/messagesSau khi lấy được resource_id, bạn cần gọi API "liên kết tài nguyên" cụ thể theo nghiệp vụ để gắn resource_id này với một thực thể kinh doanh (ví dụ: một sản phẩm, một bài viết), giúp nó có thể sử dụng được trong ngữ cảnh kinh doanh của bạn.
Trong nghiệp vụ Customer Service, API liên kết tài nguyên là Send Message, và resource_id cần được truyền dưới dạng tham số vid.

Chi tiết API

1. Khởi tạo tải lên

API Upload File Init được dùng để lấy URL đích và token xác thực cần thiết cho việc tải lên theo phân đoạn.

  • Endpoint: [POST] /open/:version/file/init
  • Common Query Parameters:

access_token: {Your access token} app_key: {Your app key} timestamp: {Current timestamp} sign: {Request signature} Common Query Parameters

: - access_token: {Your access token}

  • app_key: {Your app key}

  • timestamp: {Current timestamp}

  • sign: {Request signature}

  • Request Body:

ParameterTypeDescription
file_sizeIntegerTổng dung lượng tệp tính bằng byte.
file_nameStringTên tệp, bao gồm phần mở rộng (ví dụ: "my_video.mp4").
total_chunk_countIntegerTổng số phân đoạn.
file_typeStringLoại tệp. Giá trị enum: video, image, pdf.
target_pathStringĐường dẫn API của endpoint liên kết tài nguyên đích, dùng để kiểm tra quyền và định tuyến. Ví dụ: [POST]/customer_service/:version/conversations/{conversation_id}/messages.
  • Success Response:

JSONWord Wrap

{
"upload_url": "{uploader_url}",
"upload_token": "{your_unique_upload_token}"
}

2. Tải lên phân đoạn tệp

API này được dùng để tải lên một phân đoạn tệp đơn lẻ. Chúng tôi yêu cầu một phân đoạn không được nhỏ hơn 5MB và không lớn hơn 30MB.

  • Endpoint: [PUT] <uploader_url> (do API init trả về)
  • Query Parameters:
ParameterTypeDescription
upload_tokenStringUpload token thu được từ API khởi tạo.
chunk_numIntegerSố thứ tự của phân đoạn hiện tại, bắt đầu từ 1.
app_keystringAppKey của bạn.
shop_cipherStringSử dụng thuộc tính này để truyền thông tin cửa hàng khi gọi API. Việc không truyền đúng giá trị khi gọi API cho các cửa hàng xuyên biên giới sẽ trả về phản hồi không chính xác.
Lấy bằng API Get Authorization Shop
Trường này là bắt buộc khi authorization user_type = 0 (Seller) hoặc user_type = 3(Partner) và sẽ bị bỏ qua khi authorization user_type = 1 (Creator).
  • Headers:
HeaderTypeDescription
Content-TypeStringKiểu MIME của phân đoạn hiện tại. Phải tương thích với file_type đã khai báo trong quá trình khởi tạo.
x-tts-access-tokenStringAccess token của người dùng dùng để xác thực.
  • Request Body:

(binary): The binary data of the file. Request Body

: - (binary): The binary data of the file.

  • Response:

Tải lên phân đoạn thành công: Trả về part_id. Tất cả phân đoạn đã được tải lên: Sau khi phân đoạn cuối cùng được tải lên thành công, trả về resource_id cuối cùng. Response

: - Tải lên phân đoạn thành công: Trả về part_id.

  • Tất cả phân đoạn đã được tải lên: Sau khi phân đoạn cuối cùng được tải lên thành công, trả về resource_id cuối cùng.

JSONWord Wrap

{
"resource_id": "{final_resource_id}"
}

  • Supported Content-Types:
File CategorySupported Content-Types
Video (video)video/mp4, video/quicktime, video/webm
Image (image)image/png, image/jpeg

3. Liên kết tài nguyên

Dùng để gắn tài nguyên tệp đã tải lên thành công với logic kinh doanh của bạn.

  • Endpoint: Khác nhau tùy theo nghiệp vụ. Ví dụ, Customer Service sử dụng: [POST]/customer_service/:version/conversations/{conversation_id}/messages.
  • Request/Response: Bạn cần truyền resource_id thu được ở bước trước dưới dạng tham số.

Tham số và quy trình kiểm tra

Trong quá trình tải lên theo phân đoạn, gateway thực hiện kiểm tra nghiêm ngặt:

  1. Kiểm tra cộng dồn dung lượng: Mỗi lần một phân đoạn được tải lên, gateway cộng dồn tổng dung lượng các phân đoạn đã tải lên và so sánh với file_size đã khai báo lúc khởi tạo để đảm bảo tổng dung lượng tải lên không vượt quá mức dự kiến.
  2. Kiểm tra Content-Type: Content-Type của mỗi phân đoạn được xác thực để đảm bảo nhất quán với file_type đã khai báo lúc khởi tạo.
  3. Kích hoạt hợp nhất: Khi gateway phát hiện tất cả các phân đoạn (dựa trên total_chunk_count) đã được tải lên thành công, nó tự động gọi API CommitUploadParts của dịch vụ lưu trữ nền tảng để hoàn tất việc hợp nhất tệp.

Ví dụ mã

Ví dụ cURL

  1. Khởi tạo tải lên

BASHWord Wrap

curl -X POST 'https://your-api-domain.com/open/202505/file/init?access_token=YOUR_ACCESS_TOKEN&app_key=YOUR_APP_KEY×tamp=...&sign=...' \  
-H 'Content-Type: application/json' \
-d '{
"file_size": 20971520,
"file_name": "test_video.mp4",
"total_chunk_count": 1,
"file_type": "video",
"target_path": "[POST]/affiliate_creator/202505/videos"
}'

  1. Tải lên phân đoạn

BASHWord Wrap

# Assume the previous step returned upload_url and upload_token  
curl -X PUT 'https://returned-uploader-url.com/file/v1/upload?upload_token=RETURNED_UPLOAD_TOKEN&chunk_num=1' \
-H 'Content-Type: video/mp4' \
-H 'Content-Length: 20971520' \
-H 'x-tts-access-token: USER_ACCESS_TOKEN' \
--data-binary '@/path/to/your/local/file_chunk_1.dat'

Ví dụ Golang

Ví dụ sau minh họa cách đọc một tệp cục bộ, chia nó thành các phân đoạn có dung lượng xác định, và tải lên tuần tự.

GOWord Wrap

package api_call  

import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"log"
"mime"
"net/http"
"os"
"sort"
"strconv"
"time"
)

const chunkSize = 10 * 1024 * 1024
const minTailSize = 5 * 1024 * 1024

type OpenAPIClient struct {
BaseURL string
Token string
AppKey string
AppSecret string
HTTP *http.Client
}

func New(baseURL, token, appKey, appSecret string) *OpenAPIClient {
return &OpenAPIClient{BaseURL: baseURL, Token: token, AppKey: appKey, AppSecret: appSecret, HTTP: &http.Client{}}
}

type UploadInitRequest struct {
FileSize int64 `json:"file_size"`
TotalChunkCount int `json:"total_chunk_count"`
FileType string `json:"file_type"`
TargetPath string `json:"target_path"`
FileName string `json:"file_name"`
}

func (c *OpenAPIClient) UploadInit(ctx context.Context, body UploadInitRequest) (*http.Response, []byte, error) {
urlStr := c.BaseURL + "/open/202512/file/init"
headers := map[string]string{
"Content-Type": "application/json",
}
if c.Token != "" {
headers["x-tts-access-token"] = c.Token
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(body); err != nil {
return nil, nil, err
}
ts := strconv.FormatInt(time.Now().Unix(), 10)
path := "/open/202512/file/init"
params := map[string]string{
"timestamp": ts,
"app_key": c.AppKey,
}
sign := c.computeSign(path, params, headers["Content-Type"], buf.Bytes())
query := map[string]string{
"timestamp": ts,
"sign": sign,
"app_key": c.AppKey,
}
resp, respBody, err := APICall(urlStr, http.MethodPost, headers, query, buf)
if err != nil {
log.Printf("UploadInit Error=%v", err)
return resp, respBody, err
}

// Print API call result logs
// Only print x-tt-logid from the headers
logid := resp.Header.Get("X-Tt-Logid")
log.Printf("UploadInit resp, statusCode %d, logid %s, body %s", resp.StatusCode, logid, respBody)

return resp, respBody, nil
}

func (c *OpenAPIClient) ChunkUpload(ctx context.Context, urlStr, uploadToken string, filePath string, totalChunkCount int) (*http.Response, []byte, error) {
headers := map[string]string{}
headers["Content-Type"] = "video/mp4"
if c.Token != "" {
headers["x-tts-access-token"] = c.Token
}
// Read a file into memory
data, err := os.ReadFile(filePath)
if err != nil {
return nil, nil, err
}
// Default chunk size is 20 MB; if the last chunk is smaller than 5 MB, it will be merged with the previous chunk.

// Generate chunk boundaries
type rng struct{ start, end int }
var chunks []rng
for start := 0; start < len(data); start += chunkSize {
end := start + chunkSize
if end > len(data) {
end = len(data)
}
chunks = append(chunks, rng{start: start, end: end})
}
// Merge the last chunk (if it is smaller than 5 MB and a previous chunk exists)
if len(chunks) >= 2 {
last := chunks[len(chunks)-1]
if lastLen := last.end - last.start; lastLen > 0 && lastLen < minTailSize {
chunks[len(chunks)-2].end = last.end
chunks = chunks[:len(chunks)-1]
}
}

var lastResp *http.Response
var lastBody []byte
for i, rg := range chunks {
chunk := data[rg.start:rg.end]
query := map[string]string{
"upload_token": uploadToken,
"chunk_num": strconv.Itoa(i + 1),
"app_key": c.AppKey,
}
resp, body, err := APICall(urlStr, http.MethodPut, headers, query, bytes.NewReader(chunk))
if err != nil {
log.Printf("Chunk %d: Error=%v", i+1, err)
return resp, body, err
}
// Print API call result logs
// Only print x-tt-logid from the headers
logid := resp.Header.Get("X-Tt-Logid")
log.Printf("Chunk %d: StatusCode=%d Logid=%s, body=%s", i+1, resp.StatusCode, logid, string(body))

lastResp, lastBody = resp, body
}

return lastResp, lastBody, nil
}

// UploadFile combines UploadInit and ChunkUpload:
// 1) Read the file size and calculate the total number of chunks based on the 20 MB chunk rule
// 2) Call UploadInit to obtain the upload_token
// 3) Use the upload_token to call ChunkUpload for chunked upload
func (c *OpenAPIClient) UploadFile(ctx context.Context, filePath string) (*http.Response, []byte, error) {
fi, err := os.Stat(filePath)
if err != nil {
return nil, nil, err
}
size := fi.Size()
chunkCount := calcChunkCount(size)
// Update the file information and chunk count in the request
initReq := UploadInitRequest{
FileSize: size,
TotalChunkCount: chunkCount,
FileName: fi.Name(),
FileType: "video",
TargetPath: "[POST]/affiliate_creator/202505/videos",
}

initResp, initBody, err := c.UploadInit(ctx, initReq)
if err != nil {
return initResp, initBody, err
}

// Parse the upload_token
var parsed struct {
Code int `json:"code"`
Data struct {
UploadToken string `json:"upload_token"`
UploadURL string `json:"upload_url"`
} `json:"data"`
Message string `json:"message"`
RequestID string `json:"request_id"`
}
if err := json.Unmarshal(initBody, &parsed); err != nil {
return initResp, initBody, err
}
if parsed.Code != 0 {
return initResp, initBody, errors.New(parsed.Message)
}
if parsed.Data.UploadToken == "" {
return initResp, initBody, errors.New("upload_token is empty")
}

return c.ChunkUpload(ctx, parsed.Data.UploadURL, parsed.Data.UploadToken, filePath, chunkCount)
}

// Calculate the number of chunks based on the fixed 20 MB chunk size and the rule for merging the last chunk.
func calcChunkCount(size int64) int {

if size <= 0 {
return 1
}
n := int(size / chunkSize)
rem := size % chunkSize
if rem > 0 {
n++
}
if rem > 0 && rem < minTailSize && n > 1 {
n--
}
if n <= 0 {
n = 1
}
return n
}

func (c *OpenAPIClient) computeSign(path string, params map[string]string, contentType string, body []byte) string {
// Copy and clean parameters: exclude sign and access_token
cleaned := make(map[string]string, len(params))
for k, v := range params {
cleaned[k] = v
}
delete(cleaned, "sign")
delete(cleaned, "access_token")

// Sort the keys in alphabetical order
keys := make([]string, 0, len(cleaned))
for k := range cleaned {
keys = append(keys, k)
}
sort.Strings(keys)

// Concatenate the string: path + key + value (sorted), and append body if necessary
input := path
for _, k := range keys {
input += k + cleaned[k]
}

// If Content-Type is not multipart/form-data, append the body.
if mediaType, _, err := mime.ParseMediaType(contentType); err == nil {
if mediaType != "multipart/form-data" {
input += string(body)
}
} else {
// If parsing fails, handle conservatively: append the body.
input += string(body)
}

// Wrap with the secret and generate HMAC-SHA256.
input = c.AppSecret + input + c.AppSecret
mac := hmac.New(sha256.New, []byte(c.AppSecret))
if _, err := mac.Write([]byte(input)); err != nil {
return ""
}
return hex.EncodeToString(mac.Sum(nil))
}

FAQ

Q1: Tôi nên đặt dung lượng phân đoạn như thế nào?

A:​Khuyến nghị đặt dung lượng phân đoạn trong khoảng 5–20MB, tối đa là 30MB. Khoảng này mang lại sự cân bằng tốt giữa độ ổn định và hiệu năng, đồng thời giảm hiệu quả nguy cơ tải lên thất bại hoặc hết thời gian chờ (timeout).

Q2: Tại sao tệp cục bộ 68MB của tôi lại chỉ hiển thị là 65MB sau khi được tải lên video cloud?

A:​Đây là hiện tượng bình thường.Video trải qua một số xử lý (chẳng hạn như đóng gói hoặc nén) sau khi được tải lên video cloud, nên có thể có chênh lệch nhỏ về dung lượng tệp. Sự chênh lệch này sẽ không ảnh hưởng đến việc xuất bản video hoặc các quy trình đăng tải tiếp theo, vì vậy bạn có thể yên tâm tiếp tục.

Q3: Có giới hạn nào trong môi trường sandbox không?

A:​Có.Giới hạn QPH (Queries Per Hour) cho các app sandbox là 10. Đây là một hạn chế bình thường nhằm mục đích xác minh chức năng và không phù hợp với các tình huống yêu cầu tần suất cao.

Q4: Tôi cần làm gì trước khi tải xuống SDK?

A:​Trước khi tải xuống SDK, hãy chắc chắn thực hiện các bước sau:

  1. Nhấp vào Check for updates;
  2. Cập nhật các công cụ phát triển của bạn lên phiên bản mới nhất;
  3. Sau đó, tải xuống SDK.

Việc không cập nhật công cụ có thể dẫn đến lỗi tải xuống SDK hoặc các vấn đề không khớp phiên bản.

Image

end