Файлы и загрузки
Забрать фотографию, документ или видео с устройства в хранилище — и отдать обратно тому, кому их можно показать. Всё просто ровно до момента, когда файл оказывается большим, связь — мобильным интернетом из электрички, а сам файл — не тем, чем он представляется.
Почему это важно. Загрузка файлов — самое частое место, где мобильное приложение расходует впустую трафик и терпение пользователя, а обработчики загрузки — излюбленная точка входа для всех, кто прощупывает приложение. Обе проблемы снимаются проектированием, а не починкой задним числом.
Что нужно понимать
- Где на самом деле проходят байты и обязан ли ваш сервер быть на этом пути
- Чем файл является на самом деле, а не чем его объявляют имя и content type
- Кому можно его прочитать после сохранения и чем это ограничение обеспечено
- Что происходит, когда загрузка обрывается на восьмидесяти процентах
- Во что обходится хранить файлы, отдавать их и не удалять
Основные темы
Отправка
- Напрямую в хранилище по подписанной ссылке вместо загрузки через свой сервер
- Multipart и загрузка по частям, докачка после обрыва
- Прогресс, отмена и фоновая загрузка на мобильных
- Ограничения размера, которые срабатывают до приёма байтов, а не после
Проверка
- Тип содержимого определяется по байтам, а не по тому, что сказано в запросе
- Проверка расширения и magic number — и то, чего она не ловит
- Ограничения на размеры изображения и объём файла
- Отношение к любому загруженному файлу как к враждебному — включая его имя
Хранение
- Объектное хранилище против файловой системы против базы данных
- Ключи, по которым нельзя ни узнать пользователя, ни угадать чужой файл
- Метаданные в базе, байты в хранилище — и согласованность между ними
- Жизненный цикл: сроки хранения, «сироты» и уборка после удалённой записи
Отдача
- Подписанные ссылки с ограниченным сроком жизни для приватных файлов
- CDN и заголовки кеширования
- Производные: превью и перекодированные версии, которые генерируются один раз
- Range-запросы, чтобы видео можно было перематывать
Уровни
| Уровень | Как это выглядит |
|---|---|
| Junior | Загружает файл через сервер и затем показывает его. |
| Middle | Загружает напрямую в хранилище, проверяет файл по содержимому, показывает прогресс, обрабатывает ошибки и отмену. |
| Senior | Проектирует весь путь целиком — подписи, сроки хранения, производные, доступ, стоимость — и добивается того, чтобы большие загрузки переживали плохую связь. |
Практика
Для начала
-
Проверяйте по-настоящему Переименуйте текстовый файл в
.jpg, загрузите его и добейтесь, чтобы сервер его отклонил. -
Покажите прогресс Добавьте к загрузке настоящий индикатор прогресса и работающую отмену.
-
Ограничьте размер Отклоняйте файлы сверх лимита, не дожидаясь приёма всего тела запроса.
Глубже
-
Обойдите свой сервер Переведите загрузку на подписанную ссылку напрямую в хранилище и сравните нагрузку.
-
Докачайте прерванную загрузку Добейтесь, чтобы большая загрузка пережила обрыв связи на середине.
-
Уберите «сирот» Найдите в хранилище файлы, записей о которых в базе уже нет, и перестаньте создавать новые такие.
Проверьте себя
- Проходит ли каждый загружаемый файл через ваш сервер — и нужно ли это?
- Как вы определяете, что за файл к вам пришёл на самом деле?
- Кто сможет прочитать сохранённый файл, если у него есть ссылка?
- Что происходит с байтами, когда пользователь удаляет запись, которая на них ссылалась?
- Что делает загрузка, когда связь пропадает на восьмидесяти процентах?
- Во сколько обходится ваше хранилище и что в нём лежит такого, на что уже ничто не ссылается?
Ресурсы
- OWASP File Upload Cheat Sheet — атаки и проверки, которые их останавливают. Читать до того, как писать обработчик загрузки, а не после инцидента.
- Serverpod: file uploads — подписанные прямые загрузки и разграничение доступа в backend на Dart.
- Amazon S3: presigned URLs — каноническое объяснение подхода, который повторяют все остальные объектные хранилища.
- MDN: Range requests — за счёт чего работает перемотка видео и что для этого должен поддерживать слой отдачи файлов.
- image_picker и file_picker — клиентская часть; поведение платформенных разрешений описано в их README.