feat(protocol): add typed command decoding and dispatch
- add Command enum for protocol command codes - convert raw u8 command values via TryFrom - validate commands through Command conversion - dispatch decoded commands in Connection - reject unknown command codes
This commit is contained in:
+118
-118
@@ -1,22 +1,25 @@
|
||||
# Статус разработки проекта
|
||||
|
||||
## 📌 Текущее состояние
|
||||
* **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
|
||||
* архитектурно красивей.
|
||||
* Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
|
||||
* Ниже - чеклист для реализации.
|
||||
* **Проблема:**
|
||||
* - нет проблем 😇
|
||||
|
||||
- **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
|
||||
- архитектурно красивей.
|
||||
- Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
|
||||
- Ниже - чеклист для реализации.
|
||||
- **Проблема:**
|
||||
- - нет проблем 😇
|
||||
|
||||
## Чек-лист реализации `UPLOAD`
|
||||
|
||||
### 1. Подготовить диспетчеризацию команд
|
||||
|
||||
* [ ] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`.
|
||||
* [ ] Убедиться, что `connection.rs` больше не вызывает `header.validate()`.
|
||||
* [ ] После получения `Header` выполнить `match` по `header.command()`.
|
||||
* [ ] Для `0x01` вызвать обработчик `UPLOAD`.
|
||||
* [ ] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.
|
||||
* [ ] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`.
|
||||
- [✔] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`.
|
||||
- [✔] Убедиться, что `connection.rs` больше не вызывает `header.validate()`.
|
||||
- [✔] После получения `Header` выполнить `match` по `header.command()`.
|
||||
- [✔] Для `0x01` вызвать обработчик `UPLOAD`.
|
||||
~~[✔] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.~~
|
||||
- [✔] Для остальных вызвать свои обработчики
|
||||
- [✔] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`.
|
||||
|
||||
Ожидаемый промежуточный результат:
|
||||
|
||||
@@ -26,20 +29,21 @@
|
||||
|
||||
### 2. Создать постоянный модуль загрузки
|
||||
|
||||
* [ ] Создать `upload.rs`.
|
||||
* [ ] Подключить его в `main.rs`:
|
||||
- [ ] Создать `upload.rs`.
|
||||
- [ ] Подключить его в `main.rs`:
|
||||
|
||||
```rust
|
||||
mod upload;
|
||||
```
|
||||
|
||||
* [ ] Создать в нём асинхронную функцию `handle()`.
|
||||
* [ ] Передать в неё:
|
||||
- [ ] Создать в нём асинхронную функцию `handle()`.
|
||||
- [ ] Передать в неё:
|
||||
|
||||
* `&mut` сетевой поток;
|
||||
* проверенный `Header`.
|
||||
* [ ] Пока только вывести заголовок и вернуть `Ok(())`.
|
||||
* [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`.
|
||||
- `&mut` сетевой поток;
|
||||
- проверенный `Header`.
|
||||
|
||||
- [ ] Пока только вывести заголовок и вернуть `Ok(())`.
|
||||
- [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`.
|
||||
|
||||
Первая форма может иметь такой смысл:
|
||||
|
||||
@@ -55,19 +59,19 @@ pub async fn handle(
|
||||
|
||||
### 3. Убрать чтение всего тела в `Vec`
|
||||
|
||||
* [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`.
|
||||
* [ ] Не создавать:
|
||||
- [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`.
|
||||
- [ ] Не создавать:
|
||||
|
||||
```rust
|
||||
vec![0_u8; header.body_size() as usize]
|
||||
```
|
||||
|
||||
* [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд.
|
||||
* [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела.
|
||||
- [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд.
|
||||
- [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела.
|
||||
|
||||
### 4. Описать метаданные загрузки
|
||||
|
||||
* [ ] В `protocol.rs` создать структуру:
|
||||
- [ ] В `protocol.rs` создать структуру:
|
||||
|
||||
```rust
|
||||
pub struct UploadMetadata {
|
||||
@@ -76,14 +80,14 @@ pub struct UploadMetadata {
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Добавить геттеры для `file_size` и `file_name`.
|
||||
* [ ] Добавить константу максимальной длины имени:
|
||||
- [ ] Добавить геттеры для `file_size` и `file_name`.
|
||||
- [ ] Добавить константу максимальной длины имени:
|
||||
|
||||
```rust
|
||||
MAX_FILENAME_SIZE
|
||||
```
|
||||
|
||||
* [ ] Пока не добавлять в структуру содержимое файла.
|
||||
- [ ] Пока не добавлять в структуру содержимое файла.
|
||||
|
||||
### 5. Прочитать фиксированную часть метаданных
|
||||
|
||||
@@ -94,10 +98,10 @@ file_size: 8 байт
|
||||
name_size: 2 байта
|
||||
```
|
||||
|
||||
* [ ] В `upload::handle()` создать буфер размером 10 байт.
|
||||
* [ ] Прочитать в него ровно 10 байт через `read_exact()`.
|
||||
* [ ] Пока вывести полученные байты.
|
||||
* [ ] Проверить обрыв соединения внутри этих десяти байт.
|
||||
- [ ] В `upload::handle()` создать буфер размером 10 байт.
|
||||
- [ ] Прочитать в него ровно 10 байт через `read_exact()`.
|
||||
- [ ] Пока вывести полученные байты.
|
||||
- [ ] Проверить обрыв соединения внутри этих десяти байт.
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
@@ -107,12 +111,13 @@ name_size: 2 байта
|
||||
|
||||
### 6. Разобрать фиксированную часть в `protocol.rs`
|
||||
|
||||
* [ ] Создать приватный парсер `parse_upload_prefix()`.
|
||||
* [ ] Разобрать:
|
||||
- [ ] Создать приватный парсер `parse_upload_prefix()`.
|
||||
- [ ] Разобрать:
|
||||
|
||||
* `file_size`;
|
||||
* `name_size`.
|
||||
* [ ] Создать промежуточную структуру:
|
||||
- `file_size`;
|
||||
- `name_size`.
|
||||
|
||||
- [ ] Создать промежуточную структуру:
|
||||
|
||||
```rust
|
||||
UploadPrefix {
|
||||
@@ -121,29 +126,29 @@ UploadPrefix {
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Создать публичную функцию `decode_upload_prefix()`.
|
||||
* [ ] Скрыть ошибки `nom` внутри `protocol.rs`.
|
||||
* [ ] Проверить, что парсер использовал все 10 байт.
|
||||
* [ ] Вернуть проверенный `UploadPrefix`.
|
||||
- [ ] Создать публичную функцию `decode_upload_prefix()`.
|
||||
- [ ] Скрыть ошибки `nom` внутри `protocol.rs`.
|
||||
- [ ] Проверить, что парсер использовал все 10 байт.
|
||||
- [ ] Вернуть проверенный `UploadPrefix`.
|
||||
|
||||
### 7. Проверить длину имени
|
||||
|
||||
* [ ] Отклонить `name_size = 0`.
|
||||
* [ ] Отклонить `name_size > MAX_FILENAME_SIZE`.
|
||||
* [ ] Не выделять память для имени до проверки размера.
|
||||
* [ ] Добавить отдельные ошибки:
|
||||
- [ ] Отклонить `name_size = 0`.
|
||||
- [ ] Отклонить `name_size > MAX_FILENAME_SIZE`.
|
||||
- [ ] Не выделять память для имени до проверки размера.
|
||||
- [ ] Добавить отдельные ошибки:
|
||||
|
||||
* пустое имя;
|
||||
* слишком длинное имя.
|
||||
- пустое имя;
|
||||
- слишком длинное имя.
|
||||
|
||||
### 8. Прочитать имя файла
|
||||
|
||||
* [ ] После проверки `name_size` выделить `Vec<u8>` только под имя.
|
||||
* [ ] Прочитать ровно `name_size` байт.
|
||||
* [ ] Передать эти байты в `protocol.rs`.
|
||||
* [ ] Проверить UTF-8.
|
||||
* [ ] Получить `String`.
|
||||
* [ ] Собрать `UploadMetadata`.
|
||||
- [ ] После проверки `name_size` выделить `Vec<u8>` только под имя.
|
||||
- [ ] Прочитать ровно `name_size` байт.
|
||||
- [ ] Передать эти байты в `protocol.rs`.
|
||||
- [ ] Проверить UTF-8.
|
||||
- [ ] Получить `String`.
|
||||
- [ ] Собрать `UploadMetadata`.
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
@@ -156,110 +161,105 @@ UploadMetadata {
|
||||
|
||||
### 9. Проверить согласованность размеров
|
||||
|
||||
* [ ] Вычислить ожидаемый размер тела:
|
||||
- [ ] Вычислить ожидаемый размер тела:
|
||||
|
||||
```text
|
||||
10 + name_size + file_size
|
||||
```
|
||||
|
||||
* [ ] Использовать `checked_add()`.
|
||||
* [ ] Сравнить результат с `header.body_size()`.
|
||||
* [ ] Отклонить пакет, если размеры не совпадают.
|
||||
* [ ] Добавить ошибку переполнения размера.
|
||||
* [ ] Добавить ошибку несовпадения `body_size`.
|
||||
- [ ] Использовать `checked_add()`.
|
||||
- [ ] Сравнить результат с `header.body_size()`.
|
||||
- [ ] Отклонить пакет, если размеры не совпадают.
|
||||
- [ ] Добавить ошибку переполнения размера.
|
||||
- [ ] Добавить ошибку несовпадения `body_size`.
|
||||
|
||||
### 10. Добавить лимит размера файла
|
||||
|
||||
* [ ] Добавить временную настройку `MAX_FILE_SIZE`.
|
||||
* [ ] Проверять `file_size` до создания файла.
|
||||
* [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера.
|
||||
* [ ] Позднее вынести лимит в конфигурацию сервера.
|
||||
- [ ] Добавить временную настройку `MAX_FILE_SIZE`.
|
||||
- [ ] Проверять `file_size` до создания файла.
|
||||
- [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера.
|
||||
- [ ] Позднее вынести лимит в конфигурацию сервера.
|
||||
|
||||
### 11. Создать модуль хранилища
|
||||
|
||||
* [ ] Создать `storage.rs`.
|
||||
* [ ] Подключить его в `main.rs`.
|
||||
* [ ] Создать тип `Storage`.
|
||||
* [ ] Передавать `Storage` в `Connection`.
|
||||
* [ ] Передавать ссылку на `Storage` в `upload::handle()`.
|
||||
* [ ] Пока реализовать только выбор каталога хранения.
|
||||
- [ ] Создать `storage.rs`.
|
||||
- [ ] Подключить его в `main.rs`.
|
||||
- [ ] Создать тип `Storage`.
|
||||
- [ ] Передавать `Storage` в `Connection`.
|
||||
- [ ] Передавать ссылку на `Storage` в `upload::handle()`.
|
||||
- [ ] Пока реализовать только выбор каталога хранения.
|
||||
|
||||
### 12. Проверить имя на уровне хранилища
|
||||
|
||||
* [ ] Запретить пустое имя.
|
||||
* [ ] Запретить абсолютный путь.
|
||||
* [ ] Запретить компоненты `..`.
|
||||
* [ ] Запретить нулевой байт.
|
||||
* [ ] Убедиться, что итоговый путь остаётся внутри хранилища.
|
||||
* [ ] Определить политику перезаписи существующего файла.
|
||||
- [ ] Запретить пустое имя.
|
||||
- [ ] Запретить абсолютный путь.
|
||||
- [ ] Запретить компоненты `..`.
|
||||
- [ ] Запретить нулевой байт.
|
||||
- [ ] Убедиться, что итоговый путь остаётся внутри хранилища.
|
||||
- [ ] Определить политику перезаписи существующего файла.
|
||||
|
||||
### 13. Создать временный файл
|
||||
|
||||
* [ ] Формировать имя с `request_id`, например:
|
||||
- [ ] Формировать имя с `request_id`, например:
|
||||
|
||||
```text
|
||||
a.txt.nocloud-part-17
|
||||
```
|
||||
|
||||
* [ ] Создавать временный файл только после всех проверок метаданных.
|
||||
* [ ] Не создавать сразу итоговый файл.
|
||||
* [ ] Убедиться, что ошибка создания корректно возвращается обработчику.
|
||||
- [ ] Создавать временный файл только после всех проверок метаданных.
|
||||
- [ ] Не создавать сразу итоговый файл.
|
||||
- [ ] Убедиться, что ошибка создания корректно возвращается обработчику.
|
||||
|
||||
### 14. Принять содержимое файла порциями
|
||||
|
||||
* [ ] Создать фиксированный буфер, например 64 КиБ.
|
||||
* [ ] Завести счётчик `remaining_file_bytes = file_size`.
|
||||
* [ ] На каждой итерации читать не больше:
|
||||
- [ ] Создать фиксированный буфер, например 64 КиБ.
|
||||
- [ ] Завести счётчик `remaining_file_bytes = file_size`.
|
||||
- [ ] На каждой итерации читать не больше:
|
||||
|
||||
```text
|
||||
min(remaining_file_bytes, buffer.len())
|
||||
```
|
||||
|
||||
* [ ] Записывать прочитанную порцию во временный файл.
|
||||
* [ ] Уменьшать `remaining_file_bytes`.
|
||||
* [ ] Завершить цикл при достижении нуля.
|
||||
* [ ] Не читать байты следующего сообщения.
|
||||
- [ ] Записывать прочитанную порцию во временный файл.
|
||||
- [ ] Уменьшать `remaining_file_bytes`.
|
||||
- [ ] Завершить цикл при достижении нуля.
|
||||
- [ ] Не читать байты следующего сообщения.
|
||||
|
||||
### 15. Обработать оборванную передачу
|
||||
|
||||
* [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой.
|
||||
* [ ] Закрыть временный файл.
|
||||
* [ ] Удалить временный файл либо сохранить для будущей докачки.
|
||||
* [ ] Пока выбрать простую политику: удалять.
|
||||
* [ ] Не создавать итоговый файл при ошибке.
|
||||
- [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой.
|
||||
- [ ] Закрыть временный файл.
|
||||
- [ ] Удалить временный файл либо сохранить для будущей докачки.
|
||||
- [ ] Пока выбрать простую политику: удалять.
|
||||
- [ ] Не создавать итоговый файл при ошибке.
|
||||
|
||||
### 16. Завершить загрузку
|
||||
|
||||
* [ ] Сбросить буферы файла на диск.
|
||||
* [ ] Закрыть временный файл.
|
||||
* [ ] Переименовать временный файл в итоговый.
|
||||
* [ ] Считать переименование точкой успешного завершения операции.
|
||||
- [ ] Сбросить буферы файла на диск.
|
||||
- [ ] Закрыть временный файл.
|
||||
- [ ] Переименовать временный файл в итоговый.
|
||||
- [ ] Считать переименование точкой успешного завершения операции.
|
||||
|
||||
### 17. Отправить ответ
|
||||
|
||||
* [ ] Сформировать `RESPONSE` с тем же `request_id`.
|
||||
* [ ] При успехе вернуть статус `0`.
|
||||
* [ ] При ошибке вернуть соответствующий код.
|
||||
* [ ] Не отправлять текстовую строку вместо сообщения протокола.
|
||||
* [ ] После ответа вернуться в цикл `Connection`.
|
||||
- [ ] Сформировать `RESPONSE` с тем же `request_id`.
|
||||
- [ ] При успехе вернуть статус `0`.
|
||||
- [ ] При ошибке вернуть соответствующий код.
|
||||
- [ ] Не отправлять текстовую строку вместо сообщения протокола.
|
||||
- [ ] После ответа вернуться в цикл `Connection`.
|
||||
|
||||
### 18. Провести испытания
|
||||
|
||||
* [ ] Корректный маленький файл.
|
||||
* [ ] Пустой файл размером `0`.
|
||||
* [ ] Имя длиной `0`.
|
||||
* [ ] Имя длиннее лимита.
|
||||
* [ ] Некорректный UTF-8.
|
||||
* [ ] `body_size` меньше вычисленного.
|
||||
* [ ] `body_size` больше вычисленного.
|
||||
* [ ] `file_size` превышает лимит.
|
||||
* [ ] Обрыв во время метаданных.
|
||||
* [ ] Обрыв посередине файла.
|
||||
* [ ] Попытка передать `../test.txt`.
|
||||
* [ ] Повторная загрузка существующего файла.
|
||||
* [ ] Два последовательных `UPLOAD` в одном соединении.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- [ ] Корректный маленький файл.
|
||||
- [ ] Пустой файл размером `0`.
|
||||
- [ ] Имя длиной `0`.
|
||||
- [ ] Имя длиннее лимита.
|
||||
- [ ] Некорректный UTF-8.
|
||||
- [ ] `body_size` меньше вычисленного.
|
||||
- [ ] `body_size` больше вычисленного.
|
||||
- [ ] `file_size` превышает лимит.
|
||||
- [ ] Обрыв во время метаданных.
|
||||
- [ ] Обрыв посередине файла.
|
||||
- [ ] Попытка передать `../test.txt`.
|
||||
- [ ] Повторная загрузка существующего файла.
|
||||
- [ ] Два последовательных `UPLOAD` в одном соединении.
|
||||
|
||||
Reference in New Issue
Block a user