Облачное хранилище файлов

Проект “Облачное хранилище файлов” #

Многопользовательское файловое облако. Пользователи сервиса могут использовать его для загрузки и хранения файлов. Источником вдохновения для проекта является 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/

Структура 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:

  1. В файле public/config.js необходимо прописать адрес API бэкенда. Пример - http://localhost:8080
  2. Пересобрать Docker образ из Dockerfile
  3. Запустить контейнер вручную или через 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 файла с особым именем)