Skip to content

Instantly share code, notes, and snippets.

@MrOnlineCoder
Last active August 6, 2021 19:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrOnlineCoder/2bbf30928aa9f24da7ee6d7a59f67d90 to your computer and use it in GitHub Desktop.
Save MrOnlineCoder/2bbf30928aa9f24da7ee6d7a59f67d90 to your computer and use it in GitHub Desktop.

Загрузка фотографий на сервер

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

Введение - общий обзор процесса

Как же все-таки работает та магия что пользователь выбирает файл у себя на устройстве, нажимает кнопку и его фотография оказывается загруженной либо в аватарке либо как основная фотография поста?

Происходит это в следующем порядке:

  1. На веб странице есть элемент типа <input type="file">, который выглядит как поле для ввода с кнопочкой "Обзор..." сбоку. И да, даже там где этого не видно, он все равно есть, просто скрытый (например где просто большая кнопка "+" по нажатию на которую вызывается окно выбора файла, на самом деле это вызывается метод .click() на самом элементе <input>, который например через CSS display: none скрыт от пользователя).
  2. Следующим шагом содержимое файла отправляется на сервер. Это сделать можно разными способами, но в 90% случаев хватит использования обычного POST запроса с телом формата multipart/form-data, который нативно поддерживается бразуером, т.е. сформировать тело такого запроса можно с помощью класса FormData, и отправить хоть через fetch, хоть через axios или любую другую библиотеку.
  3. Сервер записывает полученное содержимое в файл на диск. Поскольку данные отправляются в формате multipart/form-data, нужно дополнительно подключить модуль который сможет распарсить тело такого запроса, самым популярным решением этого является пакет multer.
  4. Дальше в зависимости от ситуации, ссылка на этот файл либо возвращается клиенту (фронту например) и он уже сам решает что делать с картинкой, либо напрямую записывается ссылка в базу данных, например в столбец profile_image для пользователя.

Как загрузить фотографию в MongoDB/GridFS/PostgreSQL/MySQL/любая другая БД?

Никак! Не делайте так, если вы не уверены на 100% что Вам это нужно. В базу данных нужно записать только путь к файлу и его метаданные (размер, дата загрузки, и т.д.). Само содержимое файла записывается на диск. Запись файлов напрямую в базу данных будет пагубно влиять на ее производительность и на мастшабируемость приложения.

Что означает "ссылка на файл" в шаге 4?

Фотография (или любой другой файл) от пользователя записывается на диск сервера в произвольную папку, например upload/. Для того чтобы браузер смог в подальшем загрузить эту фотографию и отобразить ее на странице, он должен иметь возможность загрузить ее с сервера по HTTP. Для этого нужно настроить раздачу статических файлов на веб-сервере. То есть, когда браузер запросит файл по ссылке "http://myapp.com/upload/file_42.jpg", веб сервер просто "отдаст" файл из этой папки по указанному имени. Лучше всего для этой задачи подходит nginx, который эффективно может раздавать статику таким образом. Пример конфигурации для nginx:

location /upload/ {
    alias /home/username/app/upload/;
    autoindex off;
}

При этом название location может быть любым, т.е. папка на диске может называтся upload, но сама ссылка будет звучать как /user_content/file_42.jpg

При этом, на время разработки, можно воспользоваться небольшим трюком, и переложить раздачу статики на саму Node.js, например так (пример для Express):

if (process.env.NODE_ENV === 'development') {
	app.use('/upload', express.static(path.resolve(__dirname, './upload')));
}

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

Какое имя нужно дать файлу при загрузке?

Здесь два решения:

  1. Случайное или полуслучайное имя. Например используя модуль crypto можно сгенерировать случайную строку из HEX символов require('crypto').randomBytes(16).toString('hex'), останется только прибавить к ней расширание файла. Можно также добавить к названию префикс или суффикс, например я лично делаю такой формат: назначениеФайла_идентфикаторПользвотеля_случайнаяСтрока.расширение, - "post_628_paTTa3DeWkPcLW1Qxso54lTS.jpg".

P.S. вместо crypto.randomBytes предпочитаю nanoid для генерации таких случайных идентификаторов.

  1. Если например происходит загрузка аватара пользователя, и он может быть только один, и хранить предыдущие версии файлов не нужно, можно всегда использовать одно и тоже название, например "avatar_<user_id>", в таком случае мы убиваем сразу двух зайцев - по имени файла всегда можно понять к какому пользователю он имеет отношение, а также имея идентификатор пользователя по идеи можно всегда получить его аватарку, вручную воссоздав путь к файлу.

Важно - Безопасность

  1. Для удобства пользователя укажите в атрибутах элемента <input> укажите фильтр на выбор файлов с помощью атрибута accept, например для картинок: <input type="file" accept="image/*">
  2. На клиенте и на сервере установите лимит на размер загружаемых файлов. Для фотографий подойдет ограничение в зависимости от ситуации, до 1 мб, до 5 мб, до 10 мб или до 20 мб. Также установите рейт лимит на количество запросов на загрузку файлов за единицу времени.
  3. Не используйте при сохранении оригинальное название файла от пользователя.
  4. Приводите расширения файла при загрузке к единому формату, игнорируя расширение файла от пользователя. Недопустимые типы файлов не пропускайте на загрузку, например используя модуль https://www.npmjs.com/package/file-type .
  5. Не давайте критическим, приватным, или конфиденциально важным файлам легко-угадываемые имена (напимер 1.jpg, 2.jpg, 3.jpg). Дополнительно проверяйте, авторизован ли пользователь и имеет ли он доступ к ресурсу, который запршивает.

Файлы можно грузить только на диск?

Нет. не только. Если Вы разрабатываете глобальный сервис которым будет пользоватся большое количество пользователей по всему миру либо же планируете горизонтально масштабировать свое приложение, файлы лучше загружать на хранилище а-ля Amazon S3. Там же можно подключить CDN ( например Cloudfront), что значительно ускорит раздачу загруженных файлов по всему миру.

Что делать если мне нужно несколько размеров изображения (например для декстоп версии и для мобиальной версии)?

В этом случае при загрузке фото-оригинала от пользователя, сжимаете его в те размеры которые вам нужны, и сохраняете их как отдельные файлы, например photo_fh319hk1273_original.jpg, photo_fh319hk1273_250x250.jpg, photo_fh319hk1273_500x500.jpg, photo_fh319hk1273_1000x1000.jpg. Файлы оригиналы тоже рекомендуется хранить, чтобы в случае необходимости добавить еще один размер фотографий это было сделать легко и просто, прогнав все оригиналы через скрипт который сделает из них еще один требуемый размер. Сжатие картинок можно производить тем же известным модулем sharp, либо любым другим сервисом, например Firebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment