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_tokenthu đượ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.
| ID | Step | Flow | API Path | Description |
|---|---|---|---|---|
| 1 | Khởi tạo tải lên | [POST] /open/:version/file_upload/init | Trướ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. | |
| 2 | Tải lên phân đoạn và hoàn tất | [PUT] <uploader_url> (do API init trả về) | Sau khi lấy được upload_url và upload_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. | ||||
| 3 | Liên kết tài nguyên | Ví dụ, trong nghiệp vụ Customer Service, API liên kết tài nguyên là | ||
[POST]/customer_service/:version/conversations/{conversation_id}/messages | Sau 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:
| Parameter | Type | Description |
|---|---|---|
file_size | Integer | Tổng dung lượng tệp tính bằng byte. |
file_name | String | Tên tệp, bao gồm phần mở rộng (ví dụ: "my_video.mp4"). |
total_chunk_count | Integer | Tổng số phân đoạn. |
file_type | String | Loại tệp. Giá trị enum: video, image, pdf. |
target_path | String | Đườ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:
| Parameter | Type | Description |
|---|---|---|
upload_token | String | Upload token thu được từ API khởi tạo. |
chunk_num | Integer | Số thứ tự của phân đoạn hiện tại, bắt đầu từ 1. |
app_key | string | AppKey của bạn. |
shop_cipher | String | Sử 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:
| Header | Type | Description |
|---|---|---|
Content-Type | String | Kiể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-token | String | Access 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_idcuối cùng.
JSONWord Wrap
{
"resource_id": "{final_resource_id}"
}
- Supported Content-Types:
| File Category | Supported 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_idthu đượ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:
- 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. - Kiểm tra Content-Type:
Content-Typecủa mỗi phân đoạn được xác thực để đảm bảo nhất quán vớifile_typeđã khai báo lúc khởi tạo. - 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 APICommitUploadPartscủ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
- 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"
}'
- 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:
- Nhấp vào Check for updates;
- 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;
- 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.