Пример реализации протокола Tus
Tus — это открытый протокол для возобновляемой загрузки файлов. Протокол позволяет возобновлять загрузку файлов после обрыва соединения, загружать большие файлы по частям (чанками) и контролировать процесс загрузки.
Когда вам нужен Tus
Используйте Tus, если вы загружаете большие файлы и хотите:
- Возобновлять загрузку после обрыва сети — пользователь не потеряет прогресс
- Отправлять файл частями (chunk upload) — более надёжно для больших файлов
- Показывать прогресс пользователю и управлять повторными попытками
Для разработчиков: Официальная документация протокола: https://tus.io/protocols/resumable-upload.html
Библиотеки на разных языках: https://tus.io/implementations.html
Как устроена загрузка через Tus (4 шага)
- Клиент выбирает файл и делает запрос на ваш бэкенд (например,
POST /upload), передавая имя файла и размер (без передачи самого файла). - Бэкенд вызывает Kinescope API
POST /v2/initи получает в ответ уникальный Tus endpoint. - Бэкенд возвращает этот endpoint клиенту (как redirect или в JSON).
- Клиент загружает файл по Tus протоколу напрямую в Kinescope по полученному endpoint.
init (получить уникальный Tus endpoint), а дальше браузер загружает файл напрямую в Kinescope по выданному endpoint.Что нужно подготовить
Перед началом работы убедитесь, что у вас есть:
- API-токен Kinescope (храните на сервере, не публикуйте в браузере)
- ID проекта или папки (
parent_id), куда будет загружен файл - Ваш endpoint на бэкенде, например
POST /upload, который будет:- принимать метаданные файла от клиента
- вызывать
https://uploader.kinescope.io/v2/init - отдавать клиенту Tus endpoint для загрузки
Схема взаимодействия
Вот как выглядит процесс загрузки:
sequenceDiagram
participant Client as Client_Browser
participant Backend as Backend_UserServer
participant API as Kinescope_API
participant TusEndpoint as Kinescope_TusEndpoint
Note over Backend: "Через бэкенд не проходит файл"
Client->>Backend: "POST /upload (metadata,size)"
Backend->>API: "POST /v2/init (Bearer TOKEN, parent_id, filename, filesize, type)"
API-->>Backend: "201 Created (endpoint)"
Backend-->>Client: "endpoint (Location/redirect или JSON)"
Note over Client,TusEndpoint: "Дальше файл грузится напрямую в Kinescope"
Client->>TusEndpoint: "PATCH chunk_1 (file bytes)"
TusEndpoint-->>Client: "204 No Content (Upload-Offset)"
Client->>TusEndpoint: "PATCH chunk_2 (file bytes)"
TusEndpoint-->>Client: "204 No Content (Upload-Offset)"
Note over Client,TusEndpoint: "Повторяется до завершения загрузки"
Client->>TusEndpoint: "PATCH last_chunk (file bytes)"
TusEndpoint-->>Client: "204 No Content (Upload завершён)"
Контракт метода /upload
Что принимает клиент → ваш бэкенд
Если вы используете tus-js-client, удобный вариант — принимать стандартные заголовки Tus:
Upload-Length: размер файла в байтахUpload-Metadata: метаданные (например,filenameв base64)
Для разработчиков: В примерах ниже метаданные берутся из заголовка
Upload-Metadata, а размер — изUpload-Length. Это не единственный вариант: можно принимать эти значения и в JSON, если так проще вашему фронтенду.
Что возвращает ваш бэкенд → клиент
Есть два рабочих варианта:
- Вариант A (как в примерах ниже): ответ
201 Created+ заголовокLocation: <tus-endpoint>, либо redirect на endpoint - Вариант B:
200 OK+ JSON{ "endpoint": "<tus-endpoint>" }(и на фронтенде вы используетеuploadURL)
Если вы используете Вариант A, убедитесь, что CORS разрешает клиенту прочитать заголовок Location (нужно Access-Control-Expose-Headers: Location).
Пример запроса к Kinescope /v2/init
Ваш бэкенд должен отправить запрос на инициализацию загрузки:
curl -X POST 'https://uploader.kinescope.io/v2/init' \
-H 'Authorization: Bearer <KINESCOPE_API_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"parent_id": "<PROJECT_OR_FOLDER_ID>",
"type": "video",
"filename": "example.mp4",
"title": "example.mp4",
"filesize": 123456789
}'
В ответ Kinescope вернёт 201 Created и объект, содержащий data.endpoint — это и есть Tus endpoint для загрузки.
Пример реализации бэкенда
Вот пример обработчика на бэкенде на Go:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
)
const (
kinescopeAPIToken = "11111111-1111-1111-1111-111111111111"
kinescopeUploadInitURL = "https://uploader.kinescope.io/v2/init"
)
type KinescopeInitResponse struct {
Data struct {
ID string `json:"id"`
Endpoint string `json:"endpoint"`
} `json:"data"`
}
// Обработчик запроса на инициализацию загрузки
func handleUploadInit(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Настройка CORS заголовков
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers",
"Origin, Content-Type, Tus-Resumable, Upload-Length, Upload-Metadata")
w.Header().Set("Access-Control-Allow-Methods",
"POST, GET, HEAD, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Expose-Headers", "Location")
}
// Обработка OPTIONS запроса
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusOK)
return
}
// Парсинг метаданных из заголовка Upload-Metadata
metadata := parseMetadataHeader(r.Header.Get("Upload-Metadata"))
// Парсинг размера файла из заголовка Upload-Length
filesize, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
if err != nil || filesize <= 0 {
http.Error(w, "bad header Upload-Length", http.StatusBadRequest)
return
}
// Формирование запроса к Kinescope API
requestBody := map[string]interface{}{
"client_ip": r.RemoteAddr, // IP клиента
"parent_id": "тут ID проекта или папки",
"type": "video",
"title": metadata["filename"],
"filename": metadata["filename"],
"filesize": filesize,
}
// Вызов Kinescope API для инициализации загрузки
body, _ := json.Marshal(requestBody)
req, _ := http.NewRequest("POST", kinescopeUploadInitURL, strings.NewReader(string(body)))
req.Header.Set("Authorization", "Bearer "+kinescopeAPIToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusCreated {
http.Error(w, fmt.Sprintf("kinescope api response status=%d", resp.StatusCode),
http.StatusBadRequest)
return
}
defer resp.Body.Close()
var result KinescopeInitResponse
json.NewDecoder(resp.Body).Decode(&result)
// Возврат redirect на Tus endpoint
w.Header().Set("Location", result.Data.Endpoint)
w.WriteHeader(http.StatusCreated)
}
// Парсинг заголовка Upload-Metadata
func parseMetadataHeader(header string) map[string]string {
meta := make(map[string]string)
if header == "" {
return meta
}
elements := strings.Split(header, ",")
for _, element := range elements {
parts := strings.Fields(strings.TrimSpace(element))
if len(parts) != 2 {
continue
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
continue
}
meta[parts[0]] = string(decoded)
}
return meta
}
Пример реализации фронтенда
Для работы с протоколом Tus в браузере используется библиотека tus-js-client.
Пример HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo Upload Tus</title>
</head>
<body>
<input type="file" id="file-input">
</body>
<script src="https://cdn.jsdelivr.net/npm/tus-js-client@latest/dist/tus.js"></script>
<script src="upload.js"></script>
</html>
Пример JavaScript кода (upload.js):
// Инициализация загрузки файла
function initializeUpload(file, backendEndpoint) {
const upload = new tus.Upload(file, {
// Выберите один из вариантов:
// Вариант 1: endpoint на ваш бэкенд (рекомендуется)
endpoint: backendEndpoint, // URL на ваш бэкенд, который возвращает redirect на Tus endpoint
// Вариант 2: прямой uploadURL (если у вас уже есть Tus endpoint)
// uploadURL: "https://uploader.kinescope.io/v2/upload/0966958f-638b-4aab-bf4a-7f9860a57a93",
retryDelays: [0, 3000, 5000, 10000, 20000], // Задержки между повторными попытками
chunkSize: 10000000, // 10 MB на чанк
metadata: {
filename: file.name,
filetype: file.type
},
onError: function(error) {
console.error("Ошибка загрузки:", error);
},
onProgress: function(bytesUploaded, bytesTotal) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
console.log(`Загружено: ${bytesUploaded} / ${bytesTotal} (${percentage}%)`);
},
onSuccess: function() {
console.log(`Файл ${upload.file.name} успешно загружен. URL: ${upload.url}`);
}
});
// Поиск предыдущих загрузок для возобновления
upload.findPreviousUploads()
.then(function(previousUploads) {
if (previousUploads.length > 0) {
// Возобновляем загрузку с предыдущей попытки
upload.resumeFromPreviousUpload(previousUploads[0]);
}
// Начинаем загрузку
upload.start();
})
.catch(function(error) {
console.error("Ошибка при поиске предыдущих загрузок:", error);
upload.start(); // Начинаем загрузку с начала
});
}
// Обработчик выбора файла
document.addEventListener('DOMContentLoaded', function() {
const fileInput = document.getElementById('file-input');
if (!fileInput) {
console.error('Элемент file-input не найден');
return;
}
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) {
return;
}
// URL вашего бэкенда, который обрабатывает /upload
const backendEndpoint = 'https://your-backend.com/upload';
initializeUpload(file, backendEndpoint);
});
});
Решение проблем
CORS-ошибка и не видно заголовка Location
Проблема: В браузере CORS-ошибка и не видно заголовка Location.
Решение: Проверьте, что ваш бэкенд выставляет Access-Control-Expose-Headers: Location.
403/401 от Kinescope при вызове /v2/init
Проблема: Kinescope возвращает ошибку авторизации.
Решение: Проверьте токен и права доступа, убедитесь что токен не протух/не отозван.
Загрузка не продолжается после обрыва
Проблема: После обрыва соединения загрузка не возобновляется.
Решение: Включите findPreviousUploads()/resumeFromPreviousUpload() и не меняйте endpoint/uploadURL для одного и того же файла.
Слишком частые ошибки сети
Проблема: Постоянные ошибки сети при загрузке.
Решение: Уменьшите chunkSize и настройте retryDelays.
Что отправить в поддержку
Если вам нужна помощь техподдержки, приложите:
- URL страницы/приложения, где происходит загрузка
- время ошибки и часовой пояс
- лог ошибок из консоли браузера (см. статью «Копирование ошибок из консоли браузера»)
- HAR-файл, если проблема похожа на сетевую (см. статью «Сохранение взаимодействия браузера и сервера в HAR-файл»)
- пример
curlзапроса вашего бэкенда кPOST https://uploader.kinescope.io/v2/init(без токенов и персональных данных)
Канал поддержки: бот поддержки.
Всё! Теперь вы можете настроить загрузку больших файлов через протокол Tus.
Что дальше?
После настройки загрузки через Tus рекомендуем:
- Загрузка файлов через API — другие способы загрузки видео
- Общие правила API — авторизация и формат запросов
- Kinescope API — полная документация API
Остались вопросы? Напишите в чат поддержки — специалисты помогут!