Протокол Tus

post_post_title__ZXHn9

Что такое TUS?

TUS — это открытый протокол для возобновляемой загрузки файлов поверх HTTP. Он решает главную проблему классической загрузки: если соединение оборвалось на середине, загрузку можно продолжить с того места, где она остановилась, а не начинать заново.

Протокол разработан как открытый стандарт и имеет реализации для множества языков и платформ.

Почему не обычный multipart/form-data?

Стандартная загрузка через <input type="file"> и FormData работает по принципу «всё или ничего»:

  • Загрузили 95% файла на 500 МБ, и Wi-Fi отключился? Начинайте сначала
  • Пользователь случайно закрыл вкладку? Все данные потеряны
  • Таймаут на сервере? Загрузка провалилась

TUS решает эти проблемы через чанкирование (разбиение файла на части) и возобновляемость.

Как работает протокол

  1. Создание загрузки — клиент отправляет POST запрос с метаданными файла, сервер возвращает уникальный URL для загрузки
  2. Загрузка чанками — файл отправляется частями через PATCH запросы
  3. Возобновление — при обрыве клиент спрашивает сервер «сколько байт ты уже получил?» через HEAD запрос и продолжает с нужного места

Использование в Next.js

Рассмотрим практический пример — хук для загрузки видео с прогрессом и возможностью отмены.

Установка

npm install tus-js-client

Реализация хука useVideoUploader

import { useState } from 'react';
import { getVideoUploadUrl } from '../api';
import * as tus from 'tus-js-client';

const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; // 5 МБ

export const useVideoUploader = () => {
  const [progress, setProgress] = useState(0);
  const [upload, setUpload] = useState<tus.Upload>();

  const handleAbort = () => {
    if (!upload) return;
    upload.abort();
  };

  const handleVideoUpload = async (
    video: File,
    options?: {
      onError?: () => void;
      onSuccess?: (videoId: string) => void;
      chunkSize?: number
    },
  ) => {
    try {
      // Получаем URL для загрузки от нашего API
      const { proxyUploadURL, id } = await getVideoUploadUrl(video.size);
      const chunkSize = options?.chunkSize || DEFAULT_CHUNK_SIZE;

      const upload = new tus.Upload(video, {
        endpoint: proxyUploadURL,
        // Задержки между повторными попытками при ошибке
        retryDelays: [0, 3000, 5000, 10000, 20000],
        metadata: {
          filename: video.name,
          filetype: video.type,
        },
        uploadSize: video.size,
        chunkSize: chunkSize,
        onError: options?.onError,
        onProgress: function (bytesUploaded, bytesTotal) {
          const percentage = Number(
            ((bytesUploaded / bytesTotal) * 100).toFixed(2)
          );
          setProgress(percentage);
        },
        onSuccess: () => options?.onSuccess?.(id),
      });

      setUpload(upload);

      // Проверяем, есть ли предыдущие незавершённые загрузки
      upload.findPreviousUploads().then(function (previousUploads) {
        if (previousUploads.length) {
          // Возобновляем с последней точки
          upload.resumeFromPreviousUpload(previousUploads[0]);
        }
        upload.start();
      });
    } catch (e) {
      console.error('Ошибка загрузки:', e);
    }
  };

  return {
    uploadVideo: handleVideoUpload,
    uploadingProgress: progress,
    abortUpload: handleAbort,
  };
};

Разбор ключевых моментов

retryDelays

retryDelays: [0, 3000, 5000, 10000, 20000]

Массив задержек в миллисекундах между повторными попытками. При ошибке сети библиотека автоматически повторит запрос: сначала сразу, потом через 3 секунды, затем через 5, 10 и 20. После исчерпания попыток вызывается onError.

chunkSize

chunkSize: 5 * 1024 * 1024 // 5 МБ

Размер одного чанка. Меньшие чанки = чаще сохраняется прогресс, но больше HTTP-запросов. Для видео 5-10 МБ — хороший компромисс.

findPreviousUploads

upload.findPreviousUploads().then(function (previousUploads) {
  if (previousUploads.length) {
    upload.resumeFromPreviousUpload(previousUploads[0]);
  }
  upload.start();
});

Библиотека хранит информацию о незавершённых загрузках в localStorage. Это позволяет возобновить загрузку даже после перезагрузки страницы.

Пример использования в компоненте

const VideoUploadForm = () => {
  const { uploadVideo, uploadingProgress, abortUpload } = useVideoUploader();

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    uploadVideo(file, {
      onSuccess: (videoId) => {
        console.log('Видео загружено! ID:', videoId);
      },
      onError: () => {
        console.error('Ошибка загрузки');
      },
    });
  };

  return (
    <div>
      <input type="file" accept="video/*" onChange={handleFileChange} />

      {uploadingProgress > 0 && (
        <div>
          <progress value={uploadingProgress} max={100} />
          <span>{uploadingProgress}%</span>
          <button onClick={abortUpload}>Отменить</button>
        </div>
      )}
    </div>
  );
};

Преимущества TUS

  • Возобновляемость — не нужно начинать заново при обрыве
  • Надёжность — автоматические повторные попытки
  • Прогресс — точное отслеживание загрузки
  • Стандартизация — единый протокол для разных платформ
  • Экономия трафика — пользователь не тратит интернет впустую

Когда использовать TUS

TUS особенно полезен когда:

  • Загружаются большие файлы (видео, архивы)
  • Пользователи работают с нестабильным интернетом (мобильные сети)
  • Важен UX при длительных загрузках
  • Нужна возможность паузы/возобновления

Для маленьких файлов (аватарки, документы до 5 МБ) обычный multipart/form-data может быть проще и достаточен.

Заключение

Протокол TUS — элегантное решение для надёжной загрузки файлов. Библиотека tus-js-client делает интеграцию простой: несколько строк кода, и ваши пользователи больше не будут терять прогресс загрузки при проблемах с сетью.

Полезные ссылки: