file-server
В этом репозитории содержится исполнение тестового задания на позицию Entry Middle разработчика.
Что было реализовано
- Регистрация пользователя
- Аутентификация пользователя
- Загрузка нового документа
- Получение списка документов
- Получение одного документа
- Удаление документа
- Кеширование в Redis
Основные компоненты приложения
- file-service — этот проект
- Redis — хранение кеша пользователя
- PostgreSQL — хранение метаданных документов и данных пользователей
- Minio — хранение бинарных данных
Краткое описание работы сервиса
Регистрация и аутентификация пользователей
При регистрации клиент передает запрос, содержащий токен администратора, логин и пароль.
Сначала проверяется валидность токена администратора, затем соответствие логина и пароля указанным требованиям.
Если всё хорошо, создается bcrypt-хеш пароля, и данные добавляются в базу.
При аутентификации клиент передает запрос с логином и паролем, после чего по логину пользователя данные получаются из БД, и пароль сравнивается с хешем, используя библиотеку bcrypt.
В обоих случаях генерируется и возвращается JWT-токен для использования в других запросах.
Работа с документами
Загрузка нового документа
При загрузке документа инвалидируется кеш пользователя, метаданные документа добавляются в базу данных, а файл загружается в Minio Object Storage по мере поступления данных от клиента. То есть файл никогда не находится в памяти полностью.
Кстати, здесь можно было бы ещё применить другой подход. Мы можем сделать так, чтобы Minio сам отправлял метаданные в БД после успешной загрузки файла.
Получение списка документов
При получении списка документов сначала происходит попытка получения всех метаданных документов пользователя из кеша. Если их там нет, то данные получаются из БД и добавляются в кеш.
После этого идёт парсинг параметров фильтрации и сама фильтрация с последующим возвращением данных клиенту.
Список фильтров и их параметров описан в пакете filters.
Получение одного документа
При получении запроса на загрузку документа приложение обращается к кешу и, при необходимости, к БД для получения метаданных документа. Начинается проверка доступа (также через фильтрацию, как при получении списка всех документов, только limit установлен на 1, и в качестве фильтров используется фильтр доступа). Если файл доступен пользователю, то происходит запрос в Minio для получения файла. Файл отдается, используя http.ServeContent для поддержки Range-запросов и использования буферизированной записи, чтобы не загружать файл из Minio полностью перед отдачей его клиенту. Если файл — это JSON-объект, то он просто отдается из базы данных, так как хранится вместе с метаданными.
Удаление документа
При получении запроса на удаление документа происходит проверка на то, имеет ли пользователь на это право, и в случае успеха очищается кеш пользователя. Метаданные в БД помечаются как удаленные, чтобы не пересчитывать индексы, а сам файл просто удаляется из Object Storage.
Кеш
Кеш реализован в Redis. Модель работы кеша предполагает удаление кеша пользователя при получении запросов, изменяющих состояние, и полное обновление кеша при запросах на получение данных, если они не были сохранены раньше.
Также есть ещё варианты с обновлением кеша и его версионированием. У всех вариантов есть свои плюсы и минусы, но мной был выбран вариант, указанный в ТЗ.
Фильтрация
Фильтрация данных на стороне приложения была выбрана для упрощения логики работы с БД и кешем. Можно было бы использовать механизм для формирования запроса по фильтрам, но это заняло бы значительно больше времени на имплементацию.
Реализовано всё через структуру DocumentsFilter. Сначала создается экземпляр указанной структуры с указанием limit и offset (последнего не было в ТЗ, но я решил его добавить. По умолчанию он будет равен 0, так что не страшно, если он не будет передан). После этого можно добавить фильтры через соответствующую функцию. Она содержит логику выбора подходящего фильтра по ключу и добавления его в список фильтров. Подробнее можно увидеть в коде, там всё достаточно понятно реализовано.
Используемые внешние библиотеки
- github.com/golang-jwt/jwt/v4 — для генерации JWT-токенов.
- github.com/google/uuid — для работы с UUID.
- github.com/spf13/pflag — зависимость пакета configloader. Позволяет очень гибко парсить флаги.
- github.com/stretchr/testify — используется в тестах.
- github.com/minio/minio-go/v7 — SDK для работы с S3-compatible Object Storage.
- github.com/redis/go-redis/v9 — SDK для Redis. Используется для кеширования.
- github.com/golang-migrate/migrate/v4 — необходим для запуска миграций.
- github.com/jackc/pgx — библиотека для работы с PostgreSQL.
- github.com/mailru/easyjson — используется для генерации функций для маршалинга и анмаршалинга объектов без использования рефлексии.
Возможные улучшения
- Добавить Docker Compose для запуска приложения вместе с зависимостями.
- Добавить в пакет configloader функционал определения типа конфигурационного файла и анмаршалинг его нужным декодером.
- Покрыть код тестами. Я написал несколько тестов, но для production-кода этого, конечно, маловато.
- Добавить бенчмарки к потенциально затратным по расходу ресурсов функциям.
- Добавить возможность сборки dev-билда без оптимизаций линтера и с дополнительным функционалом, например, эндпоинтами для профилирования.
- Стоит также добавить сбор метрик через Prometheus. Это позволит выделить узкие места приложения и оптимизировать TTL кеша.
- Можно ещё добавить трейсинг через Sentry для более удобного отслеживания ошибок.
- На самом деле, многое можно сделать лучше. Я относился к тестовому заданию как к некому proof of concept приложению, так что ещё есть над чем поработать.
Контакты: