Блог

API на Go (Gin-Gonic) для Загрузки Файлов

Предположим вы разрабатываете новый Facebook и вам необходимо реализовать возможность загрузки файлов, чтобы пользователи могли выставлять раз в год фото из отпуска, а в остальное время постить котиков. Какой архитектурный подход вы выберите для решения данной задачи? Где вы будете хранить файлы? Как вы будете их загружать с клиента?

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

Однако, если ваше приложение являет собой более сложную систему, вам не обойтись без отдельно-выделенного хранилища объектов (object storage), например AWS S3, MiniO или DigitalOcean Spaces.

S3 на сегодняшний день — один из самых популярных облачных сервисов для хранения файлов от Amazon, по этому MiniO и DigitalOcean Spaces реализуют его API протокол, что позволяет нам с вами с легкостью использовать существующие решения для работы с этими хранилищами.

Ну а сейчас, предлагаю перейти от теории к практике, и рассмотреть реализацию данного функционала на Go. Сам проект вы можете найти тут.

Разбор проекта

Чтобы не покупать инфраструктуру или настраивать свой файловых сервер, мы будем работать с localstack — инструментом, который позволяет работать с локальными версиями большинства AWS сервисов. Сам сервер и localstack S3 мы будем поднимать в докер контейнерах с помощью docker-compose.

Естественно, при желании вы можете заменить localstack S3 на реальный S3 или его аналоги — MiniO / DigitalOcean Spaces. Но для демонстрации мы обойдемся локальной средой.

Загрузка файлов по HTTP происходит с помощью multipart запросов. Что это такое и как это работает под капотом рекомендую ознакомиться самостоятельно.

Наше API по пути POST /api/upload парсит тело запроса и по ключу “file” извлекает сам файл. Обработчик для этого пути будет выглядить следующим образом:

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

c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, MAX_UPLOAD_SIZE)

Как вы можете увидеть, Go позволяет из под коробки работать с multipart запросами, и чтобы извлечь файл по ключу “file” нам достаточно одной строчки.

Далее я добавил валидацию типа файла по content-type в заголовках. Не хорошо, если пользователи смогут вместо фото котиков и отпуска загружать какие либо скрипты или бинарники на сервер.

Так же хочу заметить, что при данном запросе, gin/gin-gonic создает tmp файл в файловой системе хоста, который удаляеться по выполнению запроса. Но для этого не забудьте вызвать file.Close()

Ну а далее происходит вся “магия” выгрузки этого файла на наше хранилище.

Работа с файловым хранилищем

Давайте теперь заглянем под капот метода uploader.Upload() который загружает файлы в хранилище.



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

Теперь давайте я поясню, зачем плодить несколько слоев абстракции.

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

Но это ведь можно сделать в хендлере, не так ведь? Да, можно.

Но я за разделение зоны ответственности между разными уровнями приложения. Транспортный уровень должен принимать данные, валидировать их и передавать дальше на уровень реализации бизнес логики. Генерация имени файла — бизнес логика, ей не место в хендлере.

А теперь давайте перейдем от лирического отступления к самой реализации клиента файлового хранилища.
Для работы с S3-подобными хранилищами существуют разные sdk для Go, например сам aws-sdk-s3 или minio-go. Мой выбор остановился на последнем, т.к. с ним я до этого еще не работал :)
Чтобы поместить файл в “корзину” нам необходимо обладать ендпоинтом сервиса (в нашем случае ендпоинтом localstack s3), именем “корзины”, самим файлом и его метаданными. Обратите внимание на поле UserMetadata в обьекте PutObjectOptions.

Данная пара ключ-значение позволит сделать файл доступным в миру т.к. по дефолту все
файлы приватны, и просто так доступа к ним не получишь.

Также при успешной записи файла, мы генерируем URL этого файла, чтобы возвращаться его по API.

API в действии

Теперь давайте посмотрим на работу данного API в действии.

Сделаем запрос с помощью Postman, передав файлы с помощью запроса multipart/form-data:


Наш сервер ответил статус кодом 200 и вернул ссылку на обьект в хранилище. Давайте откроем ссылку в браузере и посмотрим что там.

Как видите, реализовать подобный функционал совсем не сложно. Сейчас существует множество сервисов с готовым API и разработанным SDK, что значительно упрощает нашу жизнь как разработчиков.

Для более детального разбора проекта рекомендую ознакомиться с исходниками и самому поднять API на своей машине.

Пишите качественный код, и не забывайте закрывать файлы в хендлере!

Чести и удачи!

Разработка