31 июл. 2025 г.·8 мин чтения

Архитектура загрузки файлов для браузерных и мобильных приложений

Архитектура загрузки файлов для браузерных и мобильных приложений: чанкирование, повторы и проверки метаданных помогают удержать один общий поток загрузки для всех клиентов.

Архитектура загрузки файлов для браузерных и мобильных приложений

Почему логика загрузки распадается на части

Команды редко спорят в самом начале. Веб-команда тестирует загрузки в стабильной вкладке браузера, команда iPhone обходит ограничения фоновой работы, а команда Android планирует перезапуски приложения и особенности хранилища. Каждая группа решает задачу у себя перед глазами, и один общий поток постепенно превращается в три.

Большие файлы ускоряют это расхождение. Аватар весом 2 МБ обычно загружается везде. Видео на 900 МБ при слабом Wi‑Fi в отеле — уже нет. Браузеры обновляются, телефоны теряют сигнал, приложения перезапускаются, а серверы выходят по тайм-ауту. После этого каждый клиент добавляет свой размер чанка, свое время повторов и свои правила прогресса.

Проблема обычно возникает из-за мелких несовпадений, а не из-за громких падений. Один клиент отправляет upload ID как UUID, другой использует локальный временный ID, а третий создает новый ID после перезапуска. Тогда сервер уже не понимает, надо ли продолжать загрузку, отклонять чанк или сохранять дубликат файла.

Та же схема повторяется в нескольких местах:

  • коды статуса для «чанк уже получен»
  • правила checksum для каждого чанка или всего файла
  • поля метаданных вроде имени файла, размера и типа содержимого
  • точное значение слова «готово»

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

Лучше начать просто и строго. Определите один контракт до того, как команды браузера и мобильных приложений добавят свою логику. Решите, кто создает upload ID, какие метаданные должен отправлять каждый клиент, как нумеруются чанки, какие ответы запускают повтор и когда сервер считает загрузку завершенной. Это и есть основа надежной архитектуры загрузки файлов. Код клиентов может отличаться, но правила — нет.

Что должен отправлять каждый клиент

Если загрузки из браузера и с мобильных устройств расходятся, проблема часто начинается еще до отправки первого чанка. Каждый клиент передает один и тот же файл, но упаковывает его в разные имена, ID и временные метки. После этого серверу приходится гадать, что с чем связано, а такие догадки быстро превращаются в баги.

Начните с одного upload ID. Клиент создает его до начала загрузки и сохраняет, если страница перезагружается или приложение закрывается. Когда пользователь возвращается, тот же upload ID позволяет спросить: «Какие чанки у вас уже есть?» Без этого единого ID возобновление быстро превращается в путаницу.

Держите базовые поля отдельно. Не запихивайте их в один свободный blob и не перегружайте одно поле двумя смыслами. Чистый запрос обычно включает:

  • upload_id
  • file_name
  • file_size
  • mime_type
  • user_title

Такое разделение важно. file_name — это то, что пришло с устройства. user_title — это то, что человек ввел в интерфейсе. Это не одно и то же, и если смешать их, возникают странности: один клиент редактирует названия, а другой — нет.

Для чанков тоже нужен единый принцип, который смогут повторить все клиенты. Выберите фиксированный размер чанка, например 5 МБ, и заставьте все клиенты использовать его, кроме последнего чанка. Не позволяйте веб-приложению брать 4 МБ, iOS — 6 МБ, а Android менять размер на медленной сети. Предсказуемые границы чанков сильно упрощают проверки при возобновлении.

Используйте один формат и для вспомогательных данных. Если отправляете checksum, выберите один алгоритм и одно кодирование. Если отправляете временные метки, используйте один UTC-формат. Если данные авторизации идут в каждом запросе, оставьте одинаковыми имена заголовков и форму токена везде.

Этот кусок архитектуры загрузки файлов кажется скучным, но потом экономит массу времени на доработках. Когда все клиенты отправляют один и тот же контракт, сервер может быть строгим, а строгие системы ломаются реже.

Как сделать чанки предсказуемыми

Чанкирование должно быть скучным. Если браузер режет файл одним способом, iOS — другим, а Android — третьим, вся сложность оказывается на сервере. Хорошая архитектура загрузки файлов использует одно правило чанков для всех клиентов, даже когда качество сети меняется.

Разбивайте большие файлы на чанки фиксированного размера. Не меняйте размер чанка посреди загрузки и не давайте каждому клиенту свой дефолт. Чанк размером 4 или 5 МБ — хороший старт: он достаточно мал для мобильной сети и при этом эффективен на десктопе. Слишком маленькие чанки создают слишком много запросов. Слишком большие чаще падают на слабом соединении.

Нумеруйте чанки одинаково везде. Выберите нумерацию с нуля или с единицы и никогда не смешивайте их. Если chunk 0 — это первый фрагмент в браузере, он должен быть первым и на мобильном устройстве. Каждый запрос должен содержать upload ID, номер чанка и общее количество чанков, чтобы сервер мог проверять прогресс без догадок.

Когда соединение рвется, клиент должен спросить сервер, какие чанки уже дошли. Сервер отвечает номерами полученных чанков, а клиент продолжает с этого места. Это убирает лишнюю работу и не дает каждому приложению придумывать свою логику повторов.

Последний загруженный чанк не должен молча завершать процесс. После того как пришли все чанки, клиент отправляет один явный вызов «complete upload». Только после этого сервер должен собрать части и пометить файл как готовый. Загрузка фото из вкладки браузера и загрузка видео из мобильного приложения могут ощущаться по-разному, но базовые правила чанкинга под ними должны оставаться одинаковыми.

Как должны вести себя повторы

Хорошая система повторов исправляет мелкие сетевые сбои, не заставляя пользователя начинать заново. Если не прошел chunk 37, приложение должно отправить chunk 37 еще раз. Оно не должно перезапускать весь файл, если только сервер не сообщает, что сессия загрузки уже исчезла.

На телефонах это особенно важно. Пользователь может переключить приложение, потерять сигнал в лифте или заблокировать экран на минуту. В нормальной архитектуре загрузки файлов такие обычные прерывания не создают новую загрузку и не делают вторую копию того же чанка.

Каждый повтор должен использовать тот же upload ID и тот же номер чанка. Это дает серверу устойчивую точку опоры. Если первый запрос частично дошел до сервера, второй сможет сопоставиться с ним, а не создать хаос.

После каждой неудачной попытки увеличивайте паузу. Хорошо работает простая схема: подождать 2 секунды, потом 5, потом 15, потом остановиться. Такая пауза защищает сервер во время сбоев и не дает браузерным и мобильным загрузкам одновременно бомбить один и тот же endpoint.

Некоторые ошибки нужно прекращать сразу. Если истек токен, тип файла заблокирован или метаданные не совпадают с сессией загрузки, повторные попытки не помогут. Покажите простое сообщение вроде «Срок сессии истек. Пожалуйста, войдите снова» или «Этот файл больше не совпадает с данными загрузки». Пользователь сможет на это отреагировать.

Локальное хранение прогресса тоже очень полезно. Сохраняйте на устройстве upload ID, номер завершенных чанков, размер файла и fingerprint файла. Когда приложение возвращается из фона, оно может спросить сервер, какие чанки уже готовы, и продолжить отсюда.

Простой тест-кейс — видео 120 МБ на слабом мобильном интернете. Если приложение может поставить загрузку на паузу, переподключиться, переслать только недостающие чанки и завершить все с тем же upload ID, значит, логика повторов работает как надо.

Как сервер остается в синхронизации

Стройте с поддержкой CTO
Получите практическую помощь по backend-потоку, состоянию клиента и тестированию запуска.

Хорошая архитектура загрузки файлов держится на одном простом правиле: сервер решает, что существует, чего не хватает и что можно завершить. Если каждый клиент хранит свое состояние загрузки, поведение браузера и мобильных приложений начнет расходиться уже через несколько недель.

Начните с создания сессии загрузки до прихода первого чанка. Клиент запрашивает сессию, сервер возвращает session ID, правила размера чанка и срок действия. После этого каждый чанк принадлежит этой сессии, а не болтается сам по себе.

Храните всю запись сессии в одном месте. В ней должны быть имя файла, общий размер, тип содержимого, ожидаемое количество чанков, индексы полученных чанков, диапазоны байтов, данные checksum, если вы их используете, и срок жизни сессии. Когда телефон теряет сеть и подключается позже, сервер может отвечать фактами, а не предположениями.

Одна проверка статуса удерживает все клиенты на одном пути. Вкладка браузера, приложение iPhone и приложение Android должны уметь задавать один и тот же вопрос: «Что у вас уже есть?» Ответ может быть коротким:

  • состояние сессии
  • полученные или недостающие чанки
  • следующий разрешенный шаг
  • время истечения

Одна такая проверка убирает лишнюю работу. Клиентам не нужно загружать файл заново только потому, что приложение перезапустилось.

Финальная сборка требует блокировки. Два запроса на завершение могут прийти почти одновременно, особенно после повторов. Если сервер соберет файл дважды, появятся дубли записей, двойная обработка или поврежденное состояние. Заблокируйте сессию, проверьте наличие всех чанков, соберите файл один раз, пометьте его завершенным и заставьте все последующие вызовы завершения возвращать тот же готовый результат.

Регулярно очищайте заброшенные сессии. Удаляйте частичные чанки и просроченные записи сессий после безопасного окна. Если этого не делать, старые загрузки будут накапливаться, расходы на хранение расти, а проверки статуса начнут опираться на устаревшие данные, которые уже никому не нужны.

Проверяйте метаданные заранее

Если метаданные неверные, загрузка может выглядеть нормальной несколько минут, а потом упасть в самом конце. Это тратит заряд на телефонах, расходует трафик и оставляет на сервере частичные чанки, которые вообще не стоило принимать. Хорошая архитектура загрузки файлов останавливает плохие загрузки еще до старта chunk 1.

Сначала клиент должен отправить небольшой запрос с метаданными. Включите в него размер файла, имя файла, MIME-тип, checksum всего файла, если он обязателен, и любые бизнес-поля, которые серверу нужны для маршрутизации загрузки.

Что проверять до chunk 1

Сервер должен сделать несколько быстрых проверок заранее:

  • Отклонять файлы, которые превышают лимит размера для этого типа документа или аккаунта.
  • Нормализовать имя файла, удалять небезопасные символы и при необходимости сохранять безопасное отображаемое имя.
  • Сравнивать заявленный MIME-тип с реальной сигнатурой файла, а не только с расширением.
  • Требовать checksum в ожидаемом формате и отклонять отсутствующие или некорректные значения.
  • Хранить такие поля, как customer ID или document type, в метаданных, а не внутри бинарного потока.

Такое разделение убирает массу грязных крайних случаев. Файл с именем invoice.pdf на самом деле может оказаться ZIP-архивом. Фотография с телефона может иметь слишком общий MIME-тип, хотя байты явно показывают HEIC или JPEG. Если сервер проверяет все заранее, каждый клиент получает одинаковый ответ еще до начала большого переноса.

Checksum требует еще одного контроля в конце. После прихода последнего чанка сервер должен проверить, что собранный файл совпадает с checksum, объявленным в начале. Если совпадения нет, отклоните загрузку и удалите собранный файл. Не позволяйте одному клиенту пропускать эту проверку, а другому — требовать ее.

Простой пример показывает, почему это важно. Допустим, клиент загружает фото удостоверения с мобильного, но приложение отправляет неверный customer ID и имя файла с небезопасными символами. Если сервер поймает обе ошибки на этапе проверки метаданных, приложение сможет исправить форму и повторить попытку один раз. Если сервер подождет до финальной сборки, пользователь потратит две минуты на загрузку файла, который с самого начала был неверным.

Ранняя проверка кажется строгой, но она удерживает браузерные и мобильные загрузки на одном пути. Обычно именно это и отличает одну систему загрузки от трех полу-совместимых.

Постройте поток по шагам

Начните с самой маленькой версии, которая способна пройти весь путь от начала до конца. В хорошей архитектуре загрузки файлов это означает один API-контракт и четыре действия, которые каждый клиент выполняет в одном и том же порядке.

  • создать загрузку
  • отправить чанк
  • проверить статус загрузки
  • завершить загрузку

Определите эти действия до написания кода для браузера или мобильных приложений. Сохраняйте и серверные состояния небольшими: created, uploading, complete и failed обычно закрывают большинство случаев. Если слишком рано добавить слишком много особых состояний, каждый клиент начнет делать свои догадки, и именно так браузерные и мобильные загрузки расходятся.

Оба клиента должны использовать одну и ту же машину состояний. Веб-приложение и приложение iPhone могут выглядеть по-разному, но правила загрузки должны совпадать: создать один раз, отправлять чанки по порядку, спрашивать сервер, что у него уже есть, и завершать только тогда, когда на месте все чанки. Запишите эти правила и считайте их общим поведением продукта, а не логикой отдельного приложения.

Сначала постройте счастливый путь. Проведите один файл от начала до конца без прерываний. Когда это заработает, добавьте поддержку возобновления. Именно здесь команды часто слишком рано усложняют решение. Оставьте все простым: когда приложение переподключается, оно спрашивает сервер, какие чанки дошли, а затем продолжает с первого недостающего чанка.

Логирование помогает больше, чем многие ожидают. Логируйте каждое изменение состояния с upload ID и на клиенте, и на сервере. Когда кто-то говорит: «На Android загрузки падают» или «Safari застревает на 95%», вы сможете пройти по точному шагу, а не гадать.

Рабочий порядок обычно такой:

  1. зафиксировать форму API
  2. построить серверный поток
  3. построить одного клиента поверх него
  4. перенести те же правила состояний на второго клиента
  5. добавить повторы и возобновление в самом конце

Такой порядок экономит переделки. И он не дает одной системе загрузки незаметно превратиться в три разные.

Простой пример с одной фотографией

Разберите ваш поток загрузки
Получите практичный взгляд со стороны на чанки, повторы и логику возобновления.

Пользователь выбирает одну фотографию на телефоне, стоя в месте со слабым сигналом. Приложение не пытается отправить весь файл сразу. Оно открывает одну сессию загрузки, делит фото на чанки фиксированного размера и отправляет первые несколько чанков, используя один и тот же upload ID в каждом запросе.

После chunk 3 соединение обрывается. Приложение не пытается угадать, что уже есть у сервера. Оно сохраняет локальный прогресс: upload ID, размер чанка, размер фото и номера чанков, которые сервер уже подтвердил.

Эта мелочь делает поток спокойным. Телефон может остановиться, уснуть или потерять сигнал, а сессия загрузки все равно будет иметь смысл позже.

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

Сервер отвечает коротким статусом: чанки 1, 2 и 3 получены, чанки 4–10 отсутствуют. После этого браузер отправляет только недостающие чанки. Он использует тот же размер чанка и тот же upload ID, чтобы сервер мог положить каждый фрагмент на свое место.

Повторы в этом примере остаются простыми. Если chunk 6 не проходит дважды, браузер повторяет chunk 6. Он не отправляет весь файл заново и не открывает вторую сессию для той же фотографии. Это важнее, чем кажется, потому что дублирующиеся сессии быстро создают странные баги.

Проверка метаданных связывает загрузку с телефона и загрузку из браузера с одним и тем же файлом. Оба клиента передают одни и те же базовые факты перед возобновлением:

  • размер файла
  • тип содержимого
  • исходное имя файла
  • время последнего изменения
  • hash или fingerprint файла

Если эти факты совпадают, сервер позволяет браузеру продолжить сессию. Если хотя бы одно поле меняется, сервер останавливает попытку возобновления и просит начать новую загрузку. Это защищает от смешивания двух похожих фотографий с одним и тем же именем.

В итоге пользователь ощущает все очень просто. Он начал на нестабильной мобильной сети, продолжил позже в браузере и завершил одну загрузку вместо трех разных потоков, которые притворялись одним. Обычно именно этого и ждут пользователи, и именно так должны работать хорошие chunked uploads.

Ошибки, из-за которых поведение распадается

Потоки загрузки распадаются, когда каждая команда клиентов решает одну и ту же задачу по-своему. Веб-приложение называет неудачный фрагмент temporary_error, iOS пишет try_again, а Android возвращает простой 409. Теперь серверу приходится угадывать, что это значит, и архитектура загрузки файлов быстро становится грязной.

То же самое происходит с отслеживанием чанков. Если один клиент использует смещения байтов, а другой — номера чанков, случаи поддержки становятся очень неприятными. Сервер может думать, что chunk 3 начинается с 10 МБ, а мобильное приложение — что он начинается после последнего подтвержденного байта. Логика возобновления ломается, появляются дубликаты данных, а загрузки зависают без понятной причины.

Метаданные создают еще одно расхождение. Многие команды доверяют расширению файла, потому что это легко. Это работает, пока кто-то не переименует большое видео в «.jpg» или телефон не отправит странный MIME-тип. Проверяйте содержимое файла и базовые правила заранее: тип, размер, формат checksum и upload ID. Гораздо дешевле отклонить плохой ввод до третьего повтора.

У повторов должны быть лимиты. Для некоторых сбоев нужна еще одна попытка, например при обрыве соединения. Для других нужен пользовательский шаг, например при истекшей авторизации, заблокированном типе файла или нехватке памяти на устройстве. Если приложение повторяет бесконечно, оно разряжает телефон, расходует трафик в браузере и скрывает настоящую проблему.

Еще одна частая ошибка — хранить состояние загрузки только в памяти. Обновление вкладки браузера, падение приложения или перезапуск телефона стирают сессию. После этого один клиент начинает сначала, а другой пытается продолжить с потерянными данными. Сохраняйте upload ID, текущий чанк и fingerprint файла локально, чтобы каждый клиент мог продолжить с того же места.

Простое правило помогает: один протокол, одна модель состояний, один словарь ошибок. Разные экраны — нормально. Разные значения — нет.

Быстрые проверки перед запуском

Сократите сбои при загрузке
Проверьте состояние сервера, срок жизни сессий и фоновые очистки до того, как вырастет поток тикетов.

Архитектура загрузки файлов обычно ломается на краях, а не на счастливом пути. Самый быстрый способ поймать это — прогнать один реальный файл через браузер, iPhone и Android с одним и тем же чек-листом.

Используйте одно и то же имя файла, размер и тип в каждом прогоне. Так легче заметить, когда один клиент меняет размер чанка, пропускает метаданные или дважды отправляет запрос завершения.

Короткий чек-лист перед запуском делает большую часть работы:

  • Загрузите один и тот же файл из браузера, с iPhone и с Android, затем сравните количество чанков, метаданные и итоговое состояние на сервере.
  • Оборвите сеть на раннем чанке и проверьте, что клиент продолжает с первого недостающего чанка, а не с нуля.
  • Отправьте неверные MIME-данные, например JPG, помеченный как PDF, и убедитесь, что сервер отклоняет его простым сообщением.
  • Дважды вызовите endpoint завершения и проверьте, что сервер создает одну завершенную загрузку, а не две.
  • Прочитайте и логи, и сообщения для пользователя, а затем уберите расплывчатый текст вроде «загрузка не удалась».

Проверка логов важнее, чем ожидает большинство команд. Если браузер пишет «network error», Android — «server issue», а серверный лог не говорит ничего полезного, люди будут обвинять не тот слой и потеряют часы.

Хорошие сообщения должны быть конкретными и короткими. «Chunk 3 не совпал с upload ID» — полезно. «Тип файла не совпадает с содержимым» — полезно. «Что-то пошло не так» — нет.

Если вы хотите одну систему загрузки вместо трех чуть разных, ищите расхождения в поведении, а не только в результате pass/fail. Общий тест-план помогает увидеть эти расхождения, пока их еще дешево исправить.

Что делать дальше

Напишите один контракт загрузки до того, как какая-либо команда добавит еще один крайний случай. Оставьте все простым: обязательные поля, правила размера чанка, формат checksum, лимиты повторов, время истечения и финальный вызов завершения. Если браузер, iPhone-приложение и Android-приложение следуют одному контракту, у вас остается одна система вместо трех версий, которые постепенно расходятся.

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

Помогает короткий чек-лист:

  • Напишите один общий upload spec и заставьте все команды клиентов использовать его
  • Зафиксируйте одну политику повторов для всех клиентов
  • Зафиксируйте одну схему метаданных до появления новых типов файлов
  • Проверьте стоимость хранения, срок жизни загрузок и фоновые задачи очистки
  • Прочитайте обращения в поддержку и найдите повторяющиеся сбои загрузки

Проверьте расходы до запуска, а не после первого сюрприза в счете. Chunked uploads могут оставлять частичные файлы, а просроченные сессии могут накапливаться быстрее, чем ожидают многие команды. Обращения в поддержку тоже важны. Если пользователи постоянно спрашивают, почему загрузка «застряла на 99%», значит, вашему потоку нужны более понятные статусы и более точные ответы сервера.

Если вашей команде нужен второй взгляд, Oleg Sotnikov может посмотреть на дизайн в роли Fractional CTO. Он помогает стартапам и небольшим компаниям с продуктовой архитектурой, инфраструктурой и практическими техническими решениями, включая то, как удержать одну систему загрузки для web и mobile.

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