Пропустить навигацию

Пример реализации протокола Tus

Обновлено: 28.12.2025

Tus — это открытый протокол для возобновляемой загрузки файлов. Протокол позволяет возобновлять загрузку файлов после обрыва соединения, загружать большие файлы по частям (чанками) и контролировать процесс загрузки.

Когда вам нужен Tus

Используйте Tus, если вы загружаете большие файлы и хотите:

  • Возобновлять загрузку после обрыва сети — пользователь не потеряет прогресс
  • Отправлять файл частями (chunk upload) — более надёжно для больших файлов
  • Показывать прогресс пользователю и управлять повторными попытками

Для разработчиков: Официальная документация протокола: https://tus.io/protocols/resumable-upload.html
Библиотеки на разных языках: https://tus.io/implementations.html

Как устроена загрузка через Tus (4 шага)

  1. Клиент выбирает файл и делает запрос на ваш бэкенд (например, POST /upload), передавая имя файла и размер (без передачи самого файла).
  2. Бэкенд вызывает Kinescope API POST /v2/init и получает в ответ уникальный Tus endpoint.
  3. Бэкенд возвращает этот endpoint клиенту (как redirect или в JSON).
  4. Клиент загружает файл по Tus протоколу напрямую в Kinescope по полученному endpoint.
Важно: Бэкенд не участвует в передаче файла. Через ваш сервер не идёт файл и не идёт основной трафик загрузки: бэкенд нужен только для init (получить уникальный Tus endpoint), а дальше браузер загружает файл напрямую в Kinescope по выданному endpoint.

Что нужно подготовить

Перед началом работы убедитесь, что у вас есть:

  • API-токен Kinescope (храните на сервере, не публикуйте в браузере)
  • ID проекта или папки (parent_id), куда будет загружен файл
  • Ваш endpoint на бэкенде, например POST /upload, который будет:
    • принимать метаданные файла от клиента
    • вызывать https://uploader.kinescope.io/v2/init
    • отдавать клиенту Tus endpoint для загрузки
Важно: токен Kinescope нельзя передавать на фронтенд. Клиент должен общаться только с вашим бэкендом и 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.

Что отправить в поддержку

Если вам нужна помощь техподдержки, приложите:

Канал поддержки: бот поддержки.

Всё! Теперь вы можете настроить загрузку больших файлов через протокол Tus.

Что дальше?

После настройки загрузки через Tus рекомендуем:

  1. Загрузка файлов через API — другие способы загрузки видео
  2. Общие правила API — авторизация и формат запросов
  3. Kinescope API — полная документация API

Остались вопросы? Напишите в чат поддержки — специалисты помогут!