12k
All articles

Uppy 6.0 и возобновляемая загрузка файлов

Настройте возобновляемую загрузку файлов с Uppy и Tus, изучите изменения Uppy 6.0 для подписи S3 и пакетов core, а также ключевые этапы перехода с версии 5.

OpenReplay Team
OpenReplay Team
Uppy 6.0 и возобновляемая загрузка файлов

Uppy — это JavaScript-загрузчик файлов с открытым исходным кодом. В связке с плагином @uppy/tus он возобновляет загрузку больших файлов после обрыва соединения или закрытия вкладки, и пользователю не приходится отправлять файл заново с самого начала.

Большинство команд приходят к готовой библиотеке после одного и того же баг-репорта: пользователь в гостиничном Wi-Fi загрузил 90% двухгигабайтного видео, соединение оборвалось, и прогресс сбросился до нуля. Если вы написали собственный загрузчик по нашим руководствам о создании dropzone для загрузки файлов или о кастомной загрузке файлов в React, то именно здесь такого подхода становится недостаточно.

В этой статье мы разберём, почему важна возобновляемая загрузка, как выглядит минимальная настройка Uppy, что изменилось в Uppy 6.0 и сколько усилий потребует переход с версии 5.x.

Ключевые выводы

  • Tus — открытый протокол поверх HTTP, который делает загрузку возобновляемой. Обрыв соединения или закрытие вкладки не приводят к потере прогресса: загрузка продолжается с последнего байта, полученного сервером.
  • В Uppy 6.0 плагин @uppy/aws-s3 переписан вокруг трёх режимов подписи запросов (getCredentials, signRequest и companionEndpoint), и в каждой конфигурации используется ровно один из них.
  • В Uppy 6.0 пакеты @uppy/utils, @uppy/store-default, @uppy/companion-client и @uppy/provider-views поставляются в составе @uppy/core в виде subpath-экспортов.
  • Проектам, использующим метапакет uppy или сборку с CDN, для перехода на 6.0 почти или совсем не потребуется менять код.
  • Self-hosted Companion необходимо обновить до Companion 7 до того, как клиент перейдёт на @uppy/core 6, поскольку OAuth-токены теперь передаются через WebSocket-соединение Companion. Кроме того, Companion 7 работает на Express 5.

Почему Uppy, а не самописный загрузчик?

Самописный загрузчик, построенный на одном POST-запросе через fetch или XMLHttpRequest, не умеет продолжать загрузку с места остановки. При обрыве соединения все уже отправленные байты теряются. Tus решает эту задачу на уровне протокола: клиент спрашивает у сервера, какой объём данных уже получен, и отправляет только оставшуюся часть. Session replay наглядно показывает, во что обходится отсутствие такой возможности: после неудачной загрузки видно, как пользователь возвращается к окну выбора файла и снова выбирает тот же большой файл.

Для возобновляемой загрузки по Tus нужна серверная часть. Документация @uppy/tus рекомендует tusd; Tus-серверы могут хранить файлы локально или во внешнем хранилище, например в S3.

Минимальная настройка Uppy с Dashboard и Tus

Полноценный возобновляемый загрузчик собирается из трёх пакетов: @uppy/core хранит состояние, @uppy/dashboard предоставляет UI, а @uppy/tus отвечает за передачу данных.

import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Tus from '@uppy/tus';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';

new Uppy()
	.use(Dashboard, { inline: true, target: 'body' })
	// Point this at your own tus server, e.g. a tusd instance
	.use(Tus, { endpoint: 'https://your-tus-server/files/' });

inline: true встраивает Dashboard непосредственно в целевой элемент, а не открывает его в модальном окне. Если вы предпочитаете собственный UI для отображения прогресса, в нашей статье о создании индикатора прогресса загрузки описаны лежащие в основе события.

Удалённые источники, например Google Drive, работают через Companion — серверный компонент Uppy. Companion скачивает файл на стороне сервера, поэтому данные вообще не проходят через браузер пользователя. Здесь важна одна деталь: параметр chunkSize в @uppy/tus действует только для файлов, отправляемых с устройства пользователя. Загрузки через Companion подчиняются собственному параметру chunkSize в Companion, поэтому, если вам нужно ограничить размер каждого запроса, задайте оба.

По умолчанию @uppy/tus повторяет отправку неудачного чанка после серии задержек и ограничивает число одновременных загрузок. Если сервер возвращает HTTP 429, стандартная логика onShouldRetry приостанавливает все запросы в очереди и выжидает перед повторной попыткой, последовательно проходя по задержкам из retryDelays.

Что нового в Uppy 6.0

В Uppy 6.0 три главных изменения: @uppy/aws-s3 переписан с нуля, четыре вспомогательных пакета объединены с @uppy/core, а @uppy/instagram удалён. Согласно анонсу релиза Uppy 6.0, мажорную версию повысили шесть пакетов: @uppy/core, @uppy/companion, @uppy/aws-s3, @uppy/tus, @uppy/components и uppy.

В Uppy 5 у старого плагина @uppy/aws-s3 было до восьми колбэков, и нужный набор зависел от конфигурации: multipart или одиночная загрузка, с Companion или без, подпись в браузере или на сервере. Переписанный плагин заменяет всё это тремя режимами подписи, из которых нужно выбрать ровно один:

РежимГде выполняется подписьНужен ли Companion
getCredentialsВ браузере (SigV4) с временными учётными данными, полученными от вашего эндпоинтаНет
signRequestВаш собственный сервис подписиНет
companionEndpointCompanionДа
import Uppy from '@uppy/core';
import AwsS3 from '@uppy/aws-s3';

new Uppy().use(AwsS3, {
	// Swap this for an R2, MinIO or Spaces endpoint
	s3Endpoint: 'https://my-bucket.s3.eu-west-1.amazonaws.com',
	region: 'eu-west-1',
	getCredentials: async () => fetch('/s3-credentials').then((r) => r.json()),
});

Companion больше не обязателен для загрузки в S3. В режиме getCredentials браузер сам подписывает каждый запрос, а плагин работает с любым S3-совместимым сервисом, включая Cloudflare R2, MinIO и DigitalOcean Spaces. В справочнике @uppy/aws-s3 перечислены поля, которые должна возвращать getCredentials, поэтому в примере выше ваш эндпоинт /s3-credentials должен отдавать ответ именно такой структуры. В анонсе релиза также говорится, что переработка устранила давние баги, связанные с backoff при повторных попытках, истечением срока действия учётных данных, определением офлайн-режима и приостановкой или возобновлением multipart-загрузок. CI-тесты плагина теперь выполняются на реальном сервере MinIO, а не на моках.

Как перейти с Uppy 5 на 6?

Если вы используете метапакет uppy или сборку с CDN, при обновлении до 6.0 почти или совсем не придётся менять код. Всем остальным стоит пройтись по списку ниже, а подробности есть в официальном руководстве по миграции:

  • Отдельные пакеты: перенесите импорты из @uppy/utils, @uppy/store-default, @uppy/companion-client и @uppy/provider-views на соответствующие subpath-пути @uppy/core. Старые пакеты помечены в npm как deprecated.
  • Self-hosted Companion: сначала обновитесь до Companion 7, а затем переведите клиент на @uppy/core 6. Companion 7 по-прежнему поддерживает старые клиенты Uppy, но клиент Uppy 6 не будет работать с Companion 6 и более ранними версиями. Companion 7 построен на Express 5 и не будет работать как middleware в приложении на Express 4, поэтому перед переходом переведите приложение на Express 5. companion.socket() теперь требует companionOptions в качестве второго аргумента. Пользователям hosted Companion ничего делать не нужно.
  • Обработчики ошибок Tus: перепроверьте код, обрабатывающий upload-error, поскольку теперь response заполняется.
- import { fetcher } from '@uppy/utils'
+ import { fetcher } from '@uppy/core/utils'

- import { RequestClient } from '@uppy/companion-client'
+ import { RequestClient } from '@uppy/core/companion-client'

CompanionClientProvider и CompanionClientSearchProvider также удалены — вместо них импортируйте Provider из @uppy/core/companion-client. Объединение пакетов призвано предотвратить конкретную проблему: lock-файл фиксировал старую копию @uppy/utils рядом с более новым @uppy/core, и в одном дереве зависимостей оказывались две копии одного и того же пакета.

Начиная с Uppy 6.0, @uppy/tus передаёт статус и тело ответа сервера в событие upload-error и в file.response. Предыдущие версии генерировали ошибку без ответа сервера.

uppy.on('upload-error', (file, error, response) => {
	if (error.isNetworkError) return showOfflineNotice(); // placeholder helper
	console.log(response?.status);
	console.log(response?.body.xhr.responseText);
});

response по-прежнему равен undefined в двух случаях: если сервер так и не ответил (например, когда он недоступен) или если файл был удалённым и загружался через Companion.

Использование Uppy с React и другими фреймворками

Обёртки Uppy для фреймворков позволяют компонентам подписываться на состояние Uppy без ручной настройки обработчиков событий. В React хук useUppyState из @uppy/react принимает экземпляр Uppy и селектор и перерисовывает компонент при изменении выбранной части состояния. Полное описание API приведено в документации по интеграции с React.

import { useState } from 'react';
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import { useUppyState } from '@uppy/react';

function UploadStatus() {
	const [uppy] = useState(() =>
		new Uppy().use(Tus, { endpoint: 'https://your-tus-server/files/' }),
	);
	const files = useUppyState(uppy, (state) => state.files);
	const progress = useUppyState(uppy, (state) => state.totalProgress);
	return <p>{Object.keys(files).length} files, {progress}% uploaded</p>;
}

Ленивая инициализация через useState создаёт экземпляр Uppy один раз на экземпляр компонента, а не при каждом рендере. У Uppy также есть обёртки для Vue, Svelte и Angular.

Когда Uppy не нужен

Для одного небольшого файла, например аватара в профиле, не нужны ни Uppy, ни Tus. Достаточно обычного POST-запроса формы на ваш эндпоинт, а Tus лишь добавит серверную интеграцию, которую потом придётся поддерживать. Если позже вам понадобится UI Uppy, но с обычным HTTP-эндпоинтом, подойдёт @uppy/xhr-upload: он умеет отправлять стандартную multipart-форму или PUT-запрос. Принимающая сторона описана в нашем руководстве по загрузке файлов в Node с помощью Multer.

Заключение

Возобновляемая загрузка — главная причина добавить Uppy в проект. Uppy 6.0 упрощает его эксплуатацию: теперь это один плагин S3 с единственным режимом подписи на конфигурацию и один core-пакет вместо пяти, версии которых приходилось синхронизировать. Чтобы начать, установите @uppy/core, @uppy/dashboard и @uppy/tus, направьте плагин Tus на экземпляр tusd и загрузите большой файл с искусственно замедленной сетью. Если вы уже используете 5.x, начните с изменений в Companion и upload-error, поскольку именно они могут сломать существующий код.

Часто задаваемые вопросы

Что должна возвращать функция getCredentials в @uppy/aws-s3?

Она возвращает объект со свойством credentials, содержащим временные учётные данные в camelCase — accessKeyId, secretAccessKey и sessionToken, — а также необязательное поле expiration и region, для которого выполняется подпись. AWS STS возвращает эти ключи в PascalCase, поэтому их нужно предварительно преобразовать. Uppy кеширует результат на всё время жизни плагина. При ошибке ExpiredToken или InvalidAccessKeyId он запрашивает новые учётные данные и повторяет запрос один раз. Максимально ограничьте права в привязанной политике, поскольку эти учётные данные хранятся в браузере.

Какие опции @uppy/aws-s3 удалены в Uppy 6.0 и работает ли по-прежнему shouldUseMultipart?

shouldUseMultipart по-прежнему работает в @uppy/aws-s3 6.0, как и getChunkSize, allowedMetaFields и limit. Удалены опции endpoint, headers, cookiesRule, getTemporarySecurityCredentials, getUploadParameters, signPart, createMultipartUpload, listParts, abortMultipartUpload, completeMultipartUpload, uploadPartBytes и retryDelays. Для миграции существующих конфигураций с Companion достаточно переименовать endpoint в companionEndpoint. У headers и cookiesRule нет прямой замены: same-origin cookies по-прежнему передаются в Companion, но для bearer-токенов или cross-site cookies потребуется иная схема, например аутентифицирующий прокси перед Companion.

Чем возобновляемая загрузка по Tus отличается от @uppy/golden-retriever?

Tus позволяет серверу сохранить уже полученные байты, чтобы передача продолжилась, а Golden Retriever восстанавливает выбор файлов на стороне браузера и состояние Uppy после сбоя или закрытия вкладки. В Uppy 6.0 Golden Retriever по умолчанию хранит метаданные, состояние и локальные файлы размером до 10 МиБ в IndexedDB, а для метаданных при необходимости использует localStorage. Для файлов большего размера нужно опциональное хранилище на базе Service Worker, которое переживает закрытие вкладок, но не перезапуск браузера.

Может ли клиент Uppy 6 подключаться к более старому серверу Companion?

Нет. Клиенту Uppy 6 нужен Companion 7, поскольку OAuth-токены теперь передаются через WebSocket-соединение Companion, а не через window.opener. Companion 7 сохраняет postMessage в качестве запасного механизма для клиентов Uppy версий ниже 6.0, поэтому сначала разверните Companion 7, а затем обновите клиент. На сервере передайте в companion.socket() тот же объект опций, что и в companion.app(). Пользователям hosted Companion ничего менять не нужно.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.