chore(upload): document implementation steps and remove unused nom
import
This commit is contained in:
+251
-49
@@ -1,63 +1,265 @@
|
||||
# Статус разработки проекта
|
||||
|
||||
## 📌 Текущее состояние
|
||||
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет.
|
||||
Проверочная строка: smikhai@DebHOME:~$ printf '\x4E\x43\x4C\x44\x01\x01\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x03\xFB' \
|
||||
> | nc 192.168.1.213 32768
|
||||
выдаёт ошибку парсера:
|
||||
Сервер слушает 192.168.1.213:32768
|
||||
Обработчик 1 создан для 192.168.1.170:50978
|
||||
Обработчик 1: Критическая ошибка! Неверный формат заголовка.
|
||||
Обработчик 1: ошибка: Invalid header format
|
||||
что радует.
|
||||
* **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
|
||||
* архитектурно красивей.
|
||||
* Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
|
||||
* Ниже - чеклист для реализации.
|
||||
* **Проблема:**
|
||||
* **Следующий шаг:**
|
||||
## Чек-лист: исправление парсера общего заголовка
|
||||
* - нет проблем 😇
|
||||
## Чек-лист реализации `UPLOAD`
|
||||
|
||||
* [✔] Исправить имя функции `parce_header` → `parse_header`.
|
||||
* [✔] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод.
|
||||
* [✔] Убрать `.await` при вызове `parse_header`.
|
||||
* [ ] Исправить порядок полей согласно протоколу:
|
||||
### 1. Подготовить диспетчеризацию команд
|
||||
|
||||
* `bytes[0..4]` — `magic`;
|
||||
* `bytes[4]` — `version`;
|
||||
* `bytes[5]` — `command`.
|
||||
* [✔] Проверять `magic` как четыре байта `NCLD`.
|
||||
* [✔] Проверять версию как число `1`, а не ASCII-символ `b'1'`.
|
||||
* [✔] Пока разрешать только версию протокола `1`.
|
||||
* [✔] Исправить команды `80 | 81` на `0x80 | 0x81`.
|
||||
* [✔] Записывать в `Header.version` значение из `bytes[4]`.
|
||||
* [✔] Для первого теста отправлять заголовок с `body_size = 0`.
|
||||
* [ ] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`.
|
||||
* [ ] Убедиться, что `connection.rs` больше не вызывает `header.validate()`.
|
||||
* [ ] После получения `Header` выполнить `match` по `header.command()`.
|
||||
* [ ] Для `0x01` вызвать обработчик `UPLOAD`.
|
||||
* [ ] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.
|
||||
* [ ] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`.
|
||||
|
||||
## Проверка исправлений
|
||||
Ожидаемый промежуточный результат:
|
||||
|
||||
* [✔] Отправить корректный заголовок.
|
||||
* [✔] Убедиться, что сервер вывел:
|
||||
```text
|
||||
Получена команда UPLOAD
|
||||
```
|
||||
|
||||
```text
|
||||
Header {
|
||||
version: 1,
|
||||
command: 1,
|
||||
flags: 0,
|
||||
request_id: 17,
|
||||
body_size: 0,
|
||||
}
|
||||
```
|
||||
### 2. Создать постоянный модуль загрузки
|
||||
|
||||
* [ ] Создать `upload.rs`.
|
||||
* [ ] Подключить его в `main.rs`:
|
||||
|
||||
```rust
|
||||
mod upload;
|
||||
```
|
||||
|
||||
* [ ] Создать в нём асинхронную функцию `handle()`.
|
||||
* [ ] Передать в неё:
|
||||
|
||||
* `&mut` сетевой поток;
|
||||
* проверенный `Header`.
|
||||
* [ ] Пока только вывести заголовок и вернуть `Ok(())`.
|
||||
* [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`.
|
||||
|
||||
Первая форма может иметь такой смысл:
|
||||
|
||||
```rust
|
||||
pub async fn handle(
|
||||
stream: &mut TcpStream,
|
||||
header: Header,
|
||||
) -> io::Result<()> {
|
||||
println!("Обработчик UPLOAD получил заголовок: {header:?}");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Убрать чтение всего тела в `Vec`
|
||||
|
||||
* [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`.
|
||||
* [ ] Не создавать:
|
||||
|
||||
```rust
|
||||
vec![0_u8; header.body_size() as usize]
|
||||
```
|
||||
|
||||
* [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд.
|
||||
* [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела.
|
||||
|
||||
### 4. Описать метаданные загрузки
|
||||
|
||||
* [ ] В `protocol.rs` создать структуру:
|
||||
|
||||
```rust
|
||||
pub struct UploadMetadata {
|
||||
file_size: u64,
|
||||
file_name: String,
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Добавить геттеры для `file_size` и `file_name`.
|
||||
* [ ] Добавить константу максимальной длины имени:
|
||||
|
||||
```rust
|
||||
MAX_FILENAME_SIZE
|
||||
```
|
||||
|
||||
* [ ] Пока не добавлять в структуру содержимое файла.
|
||||
|
||||
### 5. Прочитать фиксированную часть метаданных
|
||||
|
||||
Формат:
|
||||
|
||||
```text
|
||||
file_size: 8 байт
|
||||
name_size: 2 байта
|
||||
```
|
||||
|
||||
* [ ] В `upload::handle()` создать буфер размером 10 байт.
|
||||
* [ ] Прочитать в него ровно 10 байт через `read_exact()`.
|
||||
* [ ] Пока вывести полученные байты.
|
||||
* [ ] Проверить обрыв соединения внутри этих десяти байт.
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
Получены 10 байт метаданных UPLOAD
|
||||
```
|
||||
|
||||
### 6. Разобрать фиксированную часть в `protocol.rs`
|
||||
|
||||
* [ ] Создать приватный парсер `parse_upload_prefix()`.
|
||||
* [ ] Разобрать:
|
||||
|
||||
* `file_size`;
|
||||
* `name_size`.
|
||||
* [ ] Создать промежуточную структуру:
|
||||
|
||||
```rust
|
||||
UploadPrefix {
|
||||
file_size: u64,
|
||||
name_size: u16,
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Создать публичную функцию `decode_upload_prefix()`.
|
||||
* [ ] Скрыть ошибки `nom` внутри `protocol.rs`.
|
||||
* [ ] Проверить, что парсер использовал все 10 байт.
|
||||
* [ ] Вернуть проверенный `UploadPrefix`.
|
||||
|
||||
### 7. Проверить длину имени
|
||||
|
||||
* [ ] Отклонить `name_size = 0`.
|
||||
* [ ] Отклонить `name_size > MAX_FILENAME_SIZE`.
|
||||
* [ ] Не выделять память для имени до проверки размера.
|
||||
* [ ] Добавить отдельные ошибки:
|
||||
|
||||
* пустое имя;
|
||||
* слишком длинное имя.
|
||||
|
||||
### 8. Прочитать имя файла
|
||||
|
||||
* [ ] После проверки `name_size` выделить `Vec<u8>` только под имя.
|
||||
* [ ] Прочитать ровно `name_size` байт.
|
||||
* [ ] Передать эти байты в `protocol.rs`.
|
||||
* [ ] Проверить UTF-8.
|
||||
* [ ] Получить `String`.
|
||||
* [ ] Собрать `UploadMetadata`.
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
UploadMetadata {
|
||||
file_size: 5,
|
||||
file_name: "a.txt",
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Проверить согласованность размеров
|
||||
|
||||
* [ ] Вычислить ожидаемый размер тела:
|
||||
|
||||
```text
|
||||
10 + name_size + file_size
|
||||
```
|
||||
|
||||
* [ ] Использовать `checked_add()`.
|
||||
* [ ] Сравнить результат с `header.body_size()`.
|
||||
* [ ] Отклонить пакет, если размеры не совпадают.
|
||||
* [ ] Добавить ошибку переполнения размера.
|
||||
* [ ] Добавить ошибку несовпадения `body_size`.
|
||||
|
||||
### 10. Добавить лимит размера файла
|
||||
|
||||
* [ ] Добавить временную настройку `MAX_FILE_SIZE`.
|
||||
* [ ] Проверять `file_size` до создания файла.
|
||||
* [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера.
|
||||
* [ ] Позднее вынести лимит в конфигурацию сервера.
|
||||
|
||||
### 11. Создать модуль хранилища
|
||||
|
||||
* [ ] Создать `storage.rs`.
|
||||
* [ ] Подключить его в `main.rs`.
|
||||
* [ ] Создать тип `Storage`.
|
||||
* [ ] Передавать `Storage` в `Connection`.
|
||||
* [ ] Передавать ссылку на `Storage` в `upload::handle()`.
|
||||
* [ ] Пока реализовать только выбор каталога хранения.
|
||||
|
||||
### 12. Проверить имя на уровне хранилища
|
||||
|
||||
* [ ] Запретить пустое имя.
|
||||
* [ ] Запретить абсолютный путь.
|
||||
* [ ] Запретить компоненты `..`.
|
||||
* [ ] Запретить нулевой байт.
|
||||
* [ ] Убедиться, что итоговый путь остаётся внутри хранилища.
|
||||
* [ ] Определить политику перезаписи существующего файла.
|
||||
|
||||
### 13. Создать временный файл
|
||||
|
||||
* [ ] Формировать имя с `request_id`, например:
|
||||
|
||||
```text
|
||||
a.txt.nocloud-part-17
|
||||
```
|
||||
|
||||
* [ ] Создавать временный файл только после всех проверок метаданных.
|
||||
* [ ] Не создавать сразу итоговый файл.
|
||||
* [ ] Убедиться, что ошибка создания корректно возвращается обработчику.
|
||||
|
||||
### 14. Принять содержимое файла порциями
|
||||
|
||||
* [ ] Создать фиксированный буфер, например 64 КиБ.
|
||||
* [ ] Завести счётчик `remaining_file_bytes = file_size`.
|
||||
* [ ] На каждой итерации читать не больше:
|
||||
|
||||
```text
|
||||
min(remaining_file_bytes, buffer.len())
|
||||
```
|
||||
|
||||
* [ ] Записывать прочитанную порцию во временный файл.
|
||||
* [ ] Уменьшать `remaining_file_bytes`.
|
||||
* [ ] Завершить цикл при достижении нуля.
|
||||
* [ ] Не читать байты следующего сообщения.
|
||||
|
||||
### 15. Обработать оборванную передачу
|
||||
|
||||
* [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой.
|
||||
* [ ] Закрыть временный файл.
|
||||
* [ ] Удалить временный файл либо сохранить для будущей докачки.
|
||||
* [ ] Пока выбрать простую политику: удалять.
|
||||
* [ ] Не создавать итоговый файл при ошибке.
|
||||
|
||||
### 16. Завершить загрузку
|
||||
|
||||
* [ ] Сбросить буферы файла на диск.
|
||||
* [ ] Закрыть временный файл.
|
||||
* [ ] Переименовать временный файл в итоговый.
|
||||
* [ ] Считать переименование точкой успешного завершения операции.
|
||||
|
||||
### 17. Отправить ответ
|
||||
|
||||
* [ ] Сформировать `RESPONSE` с тем же `request_id`.
|
||||
* [ ] При успехе вернуть статус `0`.
|
||||
* [ ] При ошибке вернуть соответствующий код.
|
||||
* [ ] Не отправлять текстовую строку вместо сообщения протокола.
|
||||
* [ ] После ответа вернуться в цикл `Connection`.
|
||||
|
||||
### 18. Провести испытания
|
||||
|
||||
* [ ] Корректный маленький файл.
|
||||
* [ ] Пустой файл размером `0`.
|
||||
* [ ] Имя длиной `0`.
|
||||
* [ ] Имя длиннее лимита.
|
||||
* [ ] Некорректный UTF-8.
|
||||
* [ ] `body_size` меньше вычисленного.
|
||||
* [ ] `body_size` больше вычисленного.
|
||||
* [ ] `file_size` превышает лимит.
|
||||
* [ ] Обрыв во время метаданных.
|
||||
* [ ] Обрыв посередине файла.
|
||||
* [ ] Попытка передать `../test.txt`.
|
||||
* [ ] Повторная загрузка существующего файла.
|
||||
* [ ] Два последовательных `UPLOAD` в одном соединении.
|
||||
|
||||
* [✔] Отправить заголовок с неправильным `magic`.
|
||||
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
|
||||
* [✔] Отправить заголовок с неподдерживаемой версией.
|
||||
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
|
||||
* [✔] Отправить заголовок с неизвестной командой.
|
||||
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
|
||||
* [✔] Отправить заголовок с запрещённым флагом, например `0x0008`.
|
||||
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
|
||||
|
||||
## Следующая итерация — пока не выполнять
|
||||
|
||||
* [ ] Ограничить допустимый `body_size`.
|
||||
* [ ] Не выделять буфер размером со всё тело сообщения.
|
||||
* [ ] Начать отдельный разбор тела команды `UPLOAD`.
|
||||
* [ ] Читать данные файла порциями, а не целиком в память.
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -4,7 +4,10 @@ pub const MAX_BODY_SIZE: u64 = 1024 * 1024;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
combinator::verify,
|
||||
// verify — комбинатор nom: проверяет результат парсера по условию.
|
||||
// Пока не использую, т.к. собственная валидация через thiserror
|
||||
// позволяет возвращать более конкретные ошибки.
|
||||
// use nom::combinator::verify;
|
||||
number::complete::{
|
||||
be_u8,
|
||||
be_u16,
|
||||
|
||||
Reference in New Issue
Block a user