# Статус разработки проекта ## 📌 Текущее состояние - **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но - архитектурно красивей. - Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/ - Ниже - чеклист для реализации. - **Проблема:** - - нет проблем 😇 ## Чек-лист реализации `UPLOAD` ### 1. Подготовить диспетчеризацию команд - [✔] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`. - [✔] Убедиться, что `connection.rs` больше не вызывает `header.validate()`. - [✔] После получения `Header` выполнить `match` по `header.command()`. - [✔] Для `0x01` вызвать обработчик `UPLOAD`. ~~[✔] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.~~ - [✔] Для остальных вызвать свои обработчики - [✔] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`. Ожидаемый промежуточный результат: ```text Получена команда UPLOAD ``` ### 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` только под имя. - [ ] Прочитать ровно `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` в одном соединении.