10 KiB
10 KiB
Статус разработки проекта
📌 Текущее состояние
- Где остановился: Окультурил контракт между 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_RELATIVE_PATH_BYTES
- [✔] Пока не добавлять в структуру содержимое файла.
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в одном соединении.