Files
nocloud-core/src/main.MD
T
Stanislav N Mikhailov 3c3487b554 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
2026-08-07 17:46:07 +03:00

10 KiB
Raw Blame History

Статус разработки проекта

📌 Текущее состояние

  • Где остановился: Окультурил контракт между protocol connection, работает точно также, но
  • архитектурно красивей.
  • Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
  • Ниже - чеклист для реализации.
  • Проблема:
    • нет проблем 😇

Чек-лист реализации UPLOAD

1. Подготовить диспетчеризацию команд

  • [✔] Оставить decode_header() единственной публичной функцией получения проверенного Header.
  • [✔] Убедиться, что connection.rs больше не вызывает header.validate().
  • [✔] После получения Header выполнить match по header.command().
  • [✔] Для 0x01 вызвать обработчик UPLOAD. [✔] Для остальных команд пока возвращать ошибку UnsupportedCommand.
  • [✔] Для остальных вызвать свои обработчики
  • [✔] Проверить: тестовый заголовок command = 0x01 попадает в обработчик UPLOAD.

Ожидаемый промежуточный результат:

Получена команда UPLOAD

2. Создать постоянный модуль загрузки

  • Создать upload.rs.
  • Подключить его в main.rs:
mod upload;
  • Создать в нём асинхронную функцию handle().

  • Передать в неё:

    • &mut сетевой поток;
    • проверенный Header.
  • Пока только вывести заголовок и вернуть Ok(()).

  • Убедиться, что после вызова управление возвращается в цикл Connection.

Первая форма может иметь такой смысл:

pub async fn handle(
    stream: &mut TcpStream,
    header: Header,
) -> io::Result<()> {
    println!("Обработчик UPLOAD получил заголовок: {header:?}");
    Ok(())
}

3. Убрать чтение всего тела в Vec

  • Не вызывать текущий read_body() для команды UPLOAD.
  • Не создавать:
vec![0_u8; header.body_size() as usize]
  • Удалить или временно оставить read_body() только для будущих небольших команд.
  • Убедиться, что UPLOAD получает поток, стоящий точно перед первым байтом тела.

4. Описать метаданные загрузки

  • В protocol.rs создать структуру:
pub struct UploadMetadata {
    file_size: u64,
    file_name: String,
}
  • Добавить геттеры для file_size и file_name.
  • Добавить константу максимальной длины имени:
MAX_FILENAME_SIZE
  • Пока не добавлять в структуру содержимое файла.

5. Прочитать фиксированную часть метаданных

Формат:

file_size: 8 байт
name_size: 2 байта
  • В upload::handle() создать буфер размером 10 байт.
  • Прочитать в него ровно 10 байт через read_exact().
  • Пока вывести полученные байты.
  • Проверить обрыв соединения внутри этих десяти байт.

Ожидаемый результат:

Получены 10 байт метаданных UPLOAD

6. Разобрать фиксированную часть в protocol.rs

  • Создать приватный парсер parse_upload_prefix().

  • Разобрать:

    • file_size;
    • name_size.
  • Создать промежуточную структуру:

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.

Ожидаемый результат:

UploadMetadata {
    file_size: 5,
    file_name: "a.txt",
}

9. Проверить согласованность размеров

  • Вычислить ожидаемый размер тела:
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, например:
a.txt.nocloud-part-17
  • Создавать временный файл только после всех проверок метаданных.
  • Не создавать сразу итоговый файл.
  • Убедиться, что ошибка создания корректно возвращается обработчику.

14. Принять содержимое файла порциями

  • Создать фиксированный буфер, например 64 КиБ.
  • Завести счётчик remaining_file_bytes = file_size.
  • На каждой итерации читать не больше:
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 в одном соединении.