Проект “Облачное хранилище файлов” #
Многопользовательское файловое облако. Пользователи сервиса могут использовать его для загрузки и хранения файлов. Источником вдохновения для проекта является Google Drive.
Что нужно знать #
- Java - коллекции, ООП
- Maven/Gradle
- Backend
- Spring Boot, Spring Security, Spring Sessions
- REST, Swagger, Upload файлов
- Cookies, cессии
- Базы данных
- SQL
- Spring Data JPA
- Миграции
- Представление о NoSQL хранилищах
- Frontend - HTML/CSS, Bootstrap
- Тесты - интеграционное тестирование, JUnit, Testcontainers
- Docker - контейнеры, образы, volumes, Docker Compose
- Деплой - облачный хостинг, командная строка Linux, Tomcat
Мотивация проекта #
- Использование возможностей Spring Boot
- Практика с Docker и Docker Compose
- Первый проект, где студент самостоятельно разрабатывает структуру БД
- Знакомство с NoSQL хранилищами - S3 для файлов, Redis для сессий
- Интеграция по REST с одностраничным frontend приложением на React
Функционал приложения #
Работа с пользователями:
- Регистрация
- Авторизация
- Logout
Работа с файлами и папками:
- Загрузка (upload) файлов и папок
- Создание новой пустой папки (аналогично созданию новой папки в проводнике)
- Удаление
- Переименование и перемещение
- Скачивание файлов и папок
REST API #
- Архитектурный стиль - RPC для авторизации и регистрации, REST для всего остального.
- Все эндпоинты существуют под общим путём
/api
. Пути ниже относительны его, пример -/api/auth/sign-up
. - Механизм авторизации - сессии.
- Формат запросов и ответов - JSON, кроме скачивания и аплоада файлов.
Ответ в случае ошибки #
Актуально для всех методов.
Тело ответа:
{
"message": "Текст ошибки"
}
Тело ответа может содержать другие поля (которые по-умолчанию отправляет Spring).
Регистрация и авторизация #
Регистрация #
POST /auth/sign-up
Тело запроса (application/json
):
{
"username": "user_1",
"password": "password"
}
Ответ в случае успеха: 201 Created
со следующим телом:
{
"username": "user_1"
}
При регистрации юзеру сразу создаётся сессия и выставляется кука.
Коды ошибок:
- 400 - ошибки валидации (пример - слишком короткий username)
- 409 - username занят
- 500 - неизвестная ошибка
Авторизация #
POST /auth/sign-in
Тело запроса (application/json
):
{
"username": "user_1",
"password": "password"
}
Тело ответа в случае успеха (200 OK
):
{
"username": "user_1"
}
Коды ошибок:
- 400 - ошибки валидации (пример - слишком короткий username)
- 401 - неверные данные (такого пользователя нет, или пароль неправильный)
- 500 - неизвестная ошибка
Выход из аккаунта (логаут) #
POST /auth/sign-out
Тела запроса нет.
Тело ответа в случае успеха (204 No Content
) пустое.
Коды ошибок:
- 401 - запрос исполняется неавторизованным юзером
- 500 - неизвестная ошибка
Пользователи #
Текущий пользователь #
GET /user/me
Ответ в случае успеха: 200 OK
со следующим телом:
{
"username": "user_1"
}
Коды ошибок:
- 401 - пользователь не авторизован
- 500 - неизвестная ошибка
Работа с файлами и папками #
Терминология:
- Ресурс - файл или папка
- Путь к ресурсу - полный путь состоит из иерархии папок, плюс имени ресурса
- Пример для файла -
folder1/folder2/file.txt
- Пример для папки -
folder1/folder2/
- Пример для файла -
Ресурсы #
Получение информации о ресурсе
GET /resource?path=$path
Для всех запросов ниже, параметр path - полный путь к ресурсу в url-encoded формате. Путь к папке должен заканчиваться на /
. Это необходимо, чтобы отличить папку и файл с одинаковым названием, которые могут сосуществовать вместе в одной корневой директории.
Ответ в случае успеха: 200 OK
со следующим телом (appication/json
):
{
"path": "folder1/folder2/", // путь к папке, в которой лежит ресурс
"name": "file.txt",
"size": 123, // размер файла в байтах. Если ресурс - папка, это поле отсутствует
"type": "FILE" // DIRECTORY или FILE
}
Коды ошибок:
- 400 - невалидный или отсутствующий путь
- 401 - пользователь не авторизован
- 404 - ресурс не найден
- 500 - неизвестная ошибка
Удаление ресурса
DELETE /resource?path=$path
Ответ в случае успеха: 204 No Content
без тела:
Коды ошибок:
- 400 - невалидный или отсутствующий путь
- 401 - пользователь не авторизован
- 404 - ресурс не найден
- 500 - неизвестная ошибка
Скачивание ресурса
GET /resource/download?path=$path
Ответ в случае успеха - 200 OK
и бинарное содержимое файла с Content-Type: application/octet-stream
.
Папка скачивается в формате zip архива её содержимого.
Коды ошибок:
- 400 - невалидный или отсутствующий путь
- 401 - пользователь не авторизован
- 404 - ресурс не найден
- 500 - неизвестная ошибка
Переименование/перемещение ресурса
GET /resource/move?from=$from&to=$to
GET параметры - старый и новый полные пути к ресурсу в URL-encoded формате.
- При переименовании меняется только имя файла
- При перемещении меняется только путь к файлу
Ответ в случае успеха - 200 OK
. Тело (application/json
):
{
"path": "folder1/folder2/", // путь к папке, в которой лежит перемещённый ресурс
"name": "file.txt", // имя перемещённого ресурса
"size": 123, // размер файла в байтах. Если ресурс - папка, это поле отсутствует
"type": "FILE" // DIRECTORY или FILE
}
Коды ошибок:
- 400 - невалидный или отсутствующий путь
- 401 - пользователь не авторизован
- 404 - ресурс не найден
- 409 - ресурс, лежащий по пути
to
уже существует - 500 - неизвестная ошибка
Поиск
GET /resource/search?query=$query
GET параметр query
- поисковый запрос в URL-encoded формате.
Ответ в случае успеха: 200 OK
со следующим телом (appication/json
). Коллекция найденных ресурсов:
[
{
"path": "folder1/folder2/", // путь к папке, в которой лежит ресурс
"name": "file.txt",
"size": 123, // размер файла в байтах. Если ресурс - папка, это поле отсутствует
"type": "FILE" // DIRECTORY или FILE
}
]
Коды ошибок:
- 400 - невалидный или отсутствующий поисковый запрос
- 401 - пользователь не авторизован
- 500 - неизвестная ошибка
Аплоад
POST resource?path=$path
GET параметр path
- путь к папке, в которую мы загружаем ресурс(ы).
Тело запроса содержит данные из file input в формате MultipartFile. Если в имени файла будет указана поддиректория (например, upload_folder/test.txt) - то при загрузке в storage_folder/ будет создана такая директория. В итоге файл после загрузки будет находиться в storage_folder/upload_folder/test.txt. Это позволяет загружать файлы, папки и рекурсивно вложенные подпапки одним запросом.
Ответ в случае успеха: 201 Created
со следующим телом (appication/json
). Коллекция загруженных ресурсов:
[
{
"path": "folder1/folder2/", // путь к папке, в которой лежит ресурс
"name": "file.txt",
"size": 123, // размер файла в байтах. Если ресурс - папка, это поле отсутствует
"type": "FILE" // DIRECTORY или FILE
}
]
Коды ошибок:
- 400 - невалидное тело запроса
- 409 - файл уже существует
- 401 - пользователь не авторизован
- 500 - неизвестная ошибка
Папки
Получение информации о содержимом папки
GET /directory?path=$path
Ответ в случае успеха: 200 OK
со следующим телом (appication/json
). Коллекция ресурсов, лежащих в папке (не рекурсивно):
[
{
"path": "folder1/folder2/", // путь к папке, в которой лежит ресурс
"name": "file.txt",
"size": 123, // размер файла в байтах. Если ресурс - папка, это поле отсутствует
"type": "FILE" // DIRECTORY или FILE
}
]
Коды ошибок:
- 400 - невалидный или отсутствующий путь
- 401 - пользователь не авторизован
- 404 - папка не существует
- 500 - неизвестная ошибка
Создание пустой папки
POST /directory?path=$path
Ответ в случае успеха: 201 Created
со следующим телом (appication/json
). Ресурс созданной папки:
[
{
"path": "folder1/folder2/", // путь к папке, в которой лежит ресурс
"name": "folder3",
"size": 123, // размер файла в байтах. Если ресурс - папка, это поле отсутствует
"type": "DIRECTORY" // DIRECTORY или FILE
}
]
Коды ошибок:
- 400 - невалидный или отсутствующий путь к новой папке
- 401 - пользователь не авторизован
- 404 - Родительская папка не существует
- 409 - папка уже существует
- 500 - неизвестная ошибка
Работа с сессиями, авторизацией, регистрацией #
В предыдущем проекте мы управляли сессиями пользователей вручную, в этом проекте воспользуемся возможности экосистемы Spring Boot.
За авторизацию, управление доступом к страницам отвечает Spring Security.
За работу с сессиями отвечает Spring Sessions. По умолчанию Spring Boot хранит сессии внутри приложения, и они теряются после каждого перезапуска приложения. Мы воспользуемся Redis для хранения сессий. Пример - https://www.baeldung.com/spring-session. Redis - NoSQL хранилище, имеющее встроенный TTL (time to live) атрибут для записей, что делает его удобным для хранения сессий - истекшие сессии автоматически удаляются.
Swagger #
Задачи:
- Интегрировать Swagger в проект
- Документировать REST API методы с помощью аннотаций
- Проверить работоспособность методов API через Swagger UI
SQL база данных #
В этом проекте студент самостоятельно разрабатывает структуру базы данных для хранения пользователей (файлы и сессии располагаются в других хранилищах). Предлагаю использовать Postgres/MySQL/MariaDB.
Ориентироваться стоит на интеграцию с Spring Security. Эта библиотека экосистемы Spring подразумевает определённые атрибуты, которыми должен обладать пользователь, и список которых и станет основой колонок для таблицы Users
.
Пример интеграции между Spring Security и Spring Data JPA - https://www.baeldung.com/registration-with-spring-mvc-and-spring-security.
Важно помнить о создании необходимых индексов в таблице Users
. Например, логин пользователя должен быть уникальным.
Миграции #
Схема БД в этом проекте очень простая, но тем не менее рекомендую попрактиковаться с миграциями. Если в прошлом проекте вы использовали Flyway, в этом можно взять Liquibase, или наоборот.
Хранилище файлов S3 #
Для хранения файлов будем пользоваться S3 - simple storage service. Проект, разработанный Amazon Cloud Services, представляет из себя облачный сервис и протокол для файлового хранилища. Чтобы не зависеть от платных сервисов Amazon в этом проекте, воспользуемся альтернативным S3-совместимым хранилищем, которое можно запустить локально - https://min.io/
- Докер образ для локального запуска MinIO - https://hub.docker.com/r/minio/minio/
- Для работы с протоколом S3 воспользуемся Minio Java SDK
Структура S3 хранилища #
В SQL мы оперируем таблицами, в S3 таблиц не существует, вместо этого S3 оперирует бакетами (bucket - корзина) с файлами. Чтобы понять что такое бакет, можно провести аналогию с диском или флешкой.
Внутри бакета можно создавать файлы и папки.
Для хранения файлов всех пользователей в проекте создадим для них бакет под названием user-files
. В корне бакета для каждого пользователя будет создана папка с именем в формате user-${id}-files
, где id
является идентификатором пользователя из SQL базы.
Каждая из таких папок является корнем для хранения папок данного пользователя. Пример - файл docs/test.txt
пользователя с id 1
должен быть сохранён в путь user-1-files/docs/test.txt
.
Работа с S3 из Java #
Как было упомянуто выше, для работы с S3 воспользуемся Minio Java SDK. Необходимо будет научиться пользоваться этой библиотекой, чтобы:
- Создавать файлы
- Переименовывать файлы
- “Переименовывать” папки. Насколько знаю в S3 нет такой операции, переименование папки по сути представляет собой создание папки под новым именем и перенос туда файлов
- Удалять файлы
Фронтенд #
Для проекта написан одностраничный фронтенд на React, спасибо Андрею @MrShoffen - https://github.com/zhukovsd/cloud-storage-frontend/.
Демо фронтенда с мокнутным API (фронтенд отображает “фейковые” данные) - https://zhukovsd.github.io/cloud-storage-frontend/files/.
Основные страницы:
- Содержимое корневой папки
- Содержимое вложенной папки
- Формы регистрации и авторизации доступны через пункт “Выход” в меню заголовка главной страницы
Интеграция фронтенда #
Собранное React приложение представляет собой набор статических файлов - https://github.com/zhukovsd/cloud-storage-frontend/tree/master/dist.
Самый простой способ интеграции - добавить эти файлы в Spring Boot проекта.
Необходимо, чтобы точка входа в React приложение (index.html
) была доступна по корневому адресу в запущенном Spring Boot сервисе. Остальные файлы должны быть доступны по соответствующим относительным путям.
Эндпоинты API при этом будут существовать под общим путём /api
. Пример - /api/auth/sign-up
.
Альтернативный способ интеграции - Docker #
В случае, если есть желание отделить фронтенд от бека, можно раздавать статику React приложения через запущенный в Docker контейнере Nginx:
- В файле
public/config.js необходимо прописать адрес API бэкенда. Пример -
http://localhost:8080
- Пересобрать Docker образ из Dockerfile
- Запустить контейнер вручную или через Docker Compose
Тесты #
Интеграционные тесты сервиса по работе с пользователями #
Как и в прошлом проекте, покроем тестами связку слоя данных с классами-сервисами, отвечающими за пользователей.
Вместо с H2 предлагаю воспользоваться Testcontainers для запуска тестов в контексте полноценной (а не in-memory) базы данных. Это позволяет приблизить окружение тестов к рабочему окружению, и тестировать нюансы, специфичные для конкретных движков БД.
Примеры тест кейсов:
- Вызов метода “создать пользователя” в сервисе, отвечающем за работу с пользователями, приводит к появлению новой записи в таблице
users
- Создание пользователя с неуникальным username приводит к ожидаемому типу исключения
Интеграционные тесты сервиса по работе с файлами и папками #
Опциональное задание повышенной сложности - покрыть тестами взаимодействие с сервисом хранения данных, работающим Minio.
Примеры тест кейсов:
- Загрузка файла приводит к его появлению в bucket’е Minio в корневой папке текущего пользователя
- Переименование, удаление файлов и папок приводит к ожидаемому результату
- Проверка прав доступа - пользователь не должен иметь доступа к чужим файлам
- Поиск - пользователь может находить свои файлы, но не чужие
Что потребуется:
- Интеграция JUnit и Spring Security
- Реализация GenericContainer для интеграции Minio и Testcontainers
Docker #
В данном проекте впервые воспользуемся Docker для удобного запуска необходимых приложений - SQL базы, файлового хранилища MinIO и хранилища сессий Redis.
Необходимо:
- Найти образы для каждого нужного приложения из списка выше
- Написать Docker Compose файл для запуска стека с приложениями (по контейнеру для каждого)
- Знать Docker Compose команды для работы со стеком
Как будет выглядеть работа с Docker:
- Для работы над проектом запускаем стек из контейнеров
- Уничтожаем или останавливаем контейнеры (с сохранением данных на volumes), когда работа не ведётся
- По необходимости уничтожаем данные на volumes, если хотим очистить то или иное хранилище, запустить
Деплой #
Будем вручную деплоить jar артефакт. Для его запуска не требуется Tomcat, потому что в собранное Spring Boot приложение уже встроен веб-сервер. Все остальные приложения этого проекта (SQL, Redis, MinIO) запускаем через Docker Compose.
Шаги:
- Локально собрать jar артефакт приложения
- В хостинг-провайдере по выбору арендовать облачный сервер на Linux
- Установить JRE, Docker
- Скопировать на удалённый сервер Docker Compose файл для запуска Postgres, Redis, MinIO
- Скопировать на удалённый сервер локально собранный jar, запустить
Ожидаемый результат - приложение доступно по адресу http://$server_ip:8080/
.
План работы над приложением #
- Docker Compose - добавить Postgres
- Spring Boot - с помощью Spring Security и Spring Data JPA реализовать методы API для регистрации и авторизации пользователей
- Интеграционные тесты для сервиса регистрации
- Docker Compose - добавить MinIO
- Spring Boot - интегрировать Minio Java SDK и научиться совершать операции с файлами в бакете, написать сервис, инкапсулирующий необходимые для приложения операции
- Реализовать загрузку файлов и папок
- Реализовать методы API для получения файлов и папок, действия с файлами (удаление, переименование)
- Поиск файлов - сервис, контроллер и методы API
- Интегрировать фронтенд
- (Опционально) интеграционные тесты для сервиса, отвечающего за работу с файлами и папками
- Docker Compose - добавить Redis
- Spring Sessions - сконфигурировать хранение сессий внутри Redis
- Деплой
Ресурсы для работы над ошибками #
- Реализации проекта другими студентами и мои ревью этих реализаций
- Чеклист для самопроверки с типовыми ошибками (в конце страницы)
- Присылайте законченные проекты в чат, добавляю их в список, сообщество делает ревью проектов
Чеклист для самопроверки #
❗️Спойлеры: советую не читать этот список до того момента, пока не допишете первую самостоятельную работающую версию проекта❗️
Функциональные проблемы:
- Несоответствие формата API требуемому, особенно в вопросах обработки ошибок
- Ошибки валидации имён файлов и папок
- Затирание файлов и папок при переименовании или перемещении (пример - папка с файлами
1
и2
, при переименовании2
в1
, оригинальный файл1
будет утерян) - Битые файлы и архивы папок при скачивании
- Возможность попасть в несуществующую папку, вместо того чтобы увидеть ошибку 404
- Возможность найти файл другого пользователя через поиск, или скачать его (через ручное формирование ссылки)
- Низкие лимиты на максимальный размер загружаемого файла или папки
“Протекание” деталей реализации хранения файлов в Minio в пользовательский интерфейс, REST ответы, код контроллеров:
- В пути к файлу содержится его корневая папка
user-${id}-files
- Не скрытая в сервисах, отвечающих за работу с Minio работа с пустыми папками (через, например, создание 0-byte файла с особым именем)