Compare commits
13 Commits
359337bc10
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc62acbc3 | |||
| 46f2721570 | |||
| e45b86cd20 | |||
| 3c3487b554 | |||
| dd6ef1d96d | |||
| 94030c34ca | |||
| b80b8e9e04 | |||
| 80b8c125ad | |||
| 6a269ca087 | |||
| d59e9d83a4 | |||
| 7bacf7ac73 | |||
| 0f2cb0384c | |||
| 9f2d7f635e |
Generated
+37
@@ -163,6 +163,12 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -208,9 +214,20 @@ name = "nocloud-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"local-ip-address",
|
"local-ip-address",
|
||||||
|
"nom",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -346,6 +363,26 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.3"
|
version = "1.52.3"
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ panic = "abort"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
local-ip-address = "0.6"
|
local-ip-address = "0.6"
|
||||||
|
thiserror = "2.0"
|
||||||
|
nom = "8.0.0"
|
||||||
|
|||||||
+104
-31
@@ -1,8 +1,20 @@
|
|||||||
use std::net::SocketAddr;
|
use std::{
|
||||||
|
io,
|
||||||
|
net::SocketAddr,
|
||||||
|
};
|
||||||
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::{
|
||||||
use tokio::net::{TcpStream};
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
use std::io;
|
net::TcpStream,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{protocol::{
|
||||||
|
Command, HEADER_SIZE, Header, decode_header,
|
||||||
|
}, upload};
|
||||||
|
|
||||||
|
struct IncomingMessage {
|
||||||
|
header: Header,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Connection {
|
pub struct Connection {
|
||||||
id: u64,
|
id: u64,
|
||||||
@@ -27,46 +39,107 @@ impl Connection {
|
|||||||
|
|
||||||
pub async fn run(mut self) -> io::Result<()> {
|
pub async fn run(mut self) -> io::Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"Обработчик {} создан для {}",
|
"Соединение {} установлено с {}",
|
||||||
self.id,
|
self.id,
|
||||||
self.client_addr
|
self.client_addr
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut buffer = [0_u8; 1024];
|
while let Some(message) = self.read_message().await? {
|
||||||
|
self.message_count += 1;
|
||||||
|
self.print_message(message).await?;
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
println!(
|
||||||
let bytes_read = self.stream.read(&mut buffer).await?;
|
"Соединение {} с {} закрыто клиентом",
|
||||||
|
self.id,
|
||||||
|
self.client_addr
|
||||||
|
);
|
||||||
|
|
||||||
if bytes_read == 0 {
|
Ok(())
|
||||||
println!(
|
}
|
||||||
"Обработчик {}: клиент {} отключился",
|
|
||||||
self.id,
|
|
||||||
self.client_addr
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(());
|
async fn read_message(
|
||||||
|
&mut self,
|
||||||
|
) -> io::Result<Option<IncomingMessage>> {
|
||||||
|
let Some(header) = self.read_header().await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
//Заголовок скачен и валиден. Получаем команду из заголовка
|
||||||
|
match header.command() {
|
||||||
|
Command::Upload => {
|
||||||
|
println!("Команда UPLOAD");
|
||||||
|
let upl_result=
|
||||||
|
upload::handle(&mut self.stream, header).await.map_err(|error| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("Ошибка UPLOAD:{error:}")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.message_count += 1;
|
Command::Download => {
|
||||||
|
println!("Команда DOWNLOAD");
|
||||||
|
}
|
||||||
|
|
||||||
let received =
|
Command::Delete => {
|
||||||
String::from_utf8_lossy(&buffer[..bytes_read]);
|
println!("Команда DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
Command::Stat => {
|
||||||
"Обработчик {}: сообщение №{}: {:?}",
|
println!("Команда STAT");
|
||||||
self.id,
|
}
|
||||||
self.message_count,
|
|
||||||
received
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = format!(
|
Command::FileData => {
|
||||||
"Обработчик {}: сообщение №{}: {}\n",
|
println!("Команда FILEDATA");
|
||||||
self.id,
|
}
|
||||||
self.message_count,
|
|
||||||
received
|
|
||||||
);
|
|
||||||
|
|
||||||
self.stream.write_all(response.as_bytes()).await?;
|
Command::Response => {
|
||||||
|
println!("Команда RESPONSE");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//let body = self.read_body(header.body_size()).await?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_header(&mut self) -> io::Result<Option<Header>> {
|
||||||
|
let mut buffer = [0_u8; HEADER_SIZE];
|
||||||
|
|
||||||
|
// Первый байт читаем отдельно, чтобы отличить нормальное
|
||||||
|
// закрытие соединения от оборванного заголовка.
|
||||||
|
let bytes_read = self.stream.read(&mut buffer[..1]).await?;
|
||||||
|
|
||||||
|
if bytes_read == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем остальную часть буфера от 1 до конца общего заголовка
|
||||||
|
self.stream.read_exact(&mut buffer[1..HEADER_SIZE]).await?;
|
||||||
|
|
||||||
|
//Парсим скачанный заголовок через nocloud_core::protocol parse_header
|
||||||
|
let header =
|
||||||
|
decode_header(&buffer).map_err(|error| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("Ошибка разбора заголовка: {error:?}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(Some(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn print_message(
|
||||||
|
&mut self,
|
||||||
|
message: IncomingMessage,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
println!(
|
||||||
|
"Соединение {}: сообщение №{}, заголовок: {:?}",
|
||||||
|
self.id,
|
||||||
|
self.message_count,
|
||||||
|
message.header
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+262
-3
@@ -1,6 +1,265 @@
|
|||||||
# Статус разработки проекта
|
# Статус разработки проекта
|
||||||
|
|
||||||
## 📌 Текущее состояние
|
## 📌 Текущее состояние
|
||||||
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет.
|
|
||||||
* **Проблема:** Не удалось проверить эхо-ответ через PowerShell.
|
- **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
|
||||||
* **Следующий шаг:** Найти рабочий способ отправки тестового запроса в PowerShell или использовать альтернативный инструмент.
|
- архитектурно красивей.
|
||||||
|
- Далее, нужно реализовать 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_RELATIVE_PATH_BYTES
|
||||||
|
```
|
||||||
|
|
||||||
|
- [✔] Пока не добавлять в структуру содержимое файла.
|
||||||
|
|
||||||
|
### 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` в одном соединении.
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,6 @@
|
|||||||
mod connection; //модуль отвечающий за соединения
|
mod connection; //модуль отвечающий за соединения
|
||||||
mod protocol; //модуль отвечающий за протокол
|
mod protocol; //модуль отвечающий за протокол ...
|
||||||
|
mod upload; //Модуль отвечающий за загрузку файла
|
||||||
|
|
||||||
use tokio::net::{TcpListener};
|
use tokio::net::{TcpListener};
|
||||||
use tokio::io;
|
use tokio::io;
|
||||||
@@ -30,7 +31,6 @@ async fn main() -> io::Result<()> {
|
|||||||
client_addr,
|
client_addr,
|
||||||
stream,
|
stream,
|
||||||
);
|
);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(error) = connection.run().await {
|
if let Err(error) = connection.run().await {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|||||||
+7
-6
@@ -5,12 +5,13 @@
|
|||||||
2. Вернуть структуру Header c понятными полями заголовка:
|
2. Вернуть структуру Header c понятными полями заголовка:
|
||||||
a. version: u8 //Версия протокола
|
a. version: u8 //Версия протокола
|
||||||
b. command: u8 //Тип сообщения
|
b. command: u8 //Тип сообщения
|
||||||
0x01 UPLOAD клиент → сервер
|
Код Команда Кто отправляет
|
||||||
0x02 DOWNLOAD клиент → сервер
|
0x01 UPLOAD клиент → сервер
|
||||||
0x03 DELETE клиент → сервер
|
0x02 DOWNLOAD клиент → сервер
|
||||||
0x04 STAT клиент → сервер
|
0x03 DELETE клиент → сервер
|
||||||
0x80 RESPONSE в обе стороны
|
0x04 STAT клиент → сервер
|
||||||
0x81 FILE_DATA сервер → клиент
|
0x80 RESPONSE клиент ↔ сервер
|
||||||
|
0x81 FILE_DATA сервер → клиент
|
||||||
c. flags: u16 //Дополнительные признаки
|
c. flags: u16 //Дополнительные признаки
|
||||||
0x0001 — разрешено перезаписать существующий файл
|
0x0001 — разрешено перезаписать существующий файл
|
||||||
0x0002 — передаётся контрольная сумма
|
0x0002 — передаётся контрольная сумма
|
||||||
|
|||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
// Парсим заголовок на базе Nom
|
||||||
|
pub const HEADER_SIZE: usize = 20;
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
bytes::complete::tag,
|
||||||
|
// verify — комбинатор nom: проверяет результат парсера по условию.
|
||||||
|
// Пока не использую, т.к. собственная валидация через thiserror
|
||||||
|
// позволяет возвращать более конкретные ошибки.
|
||||||
|
// use nom::combinator::verify;
|
||||||
|
number::complete::{
|
||||||
|
be_u8,
|
||||||
|
be_u16,
|
||||||
|
be_u32,
|
||||||
|
be_u64,
|
||||||
|
},
|
||||||
|
IResult,
|
||||||
|
Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum HeaderError {
|
||||||
|
#[error("Некорректный формат заголовка")]
|
||||||
|
InvalidFormat,
|
||||||
|
|
||||||
|
#[error("Неподдерживаемая версия протокола: {0}")]
|
||||||
|
UnsupportedVersion(u8),
|
||||||
|
|
||||||
|
#[error("Неизвестная команда протокола: {0}")]
|
||||||
|
UnsupportedCommand(u8),
|
||||||
|
|
||||||
|
#[error("Неизвестный набор флагов: {0}")]
|
||||||
|
UnsupportedFlagSet(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Header {
|
||||||
|
version: u8,
|
||||||
|
command: u8,
|
||||||
|
flags: u16,
|
||||||
|
request_id: u32,
|
||||||
|
body_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Клиент загружает файл на сервер.
|
||||||
|
Upload,
|
||||||
|
|
||||||
|
/// Клиент запрашивает скачивание файла с сервера.
|
||||||
|
Download,
|
||||||
|
|
||||||
|
/// Клиент запрашивает удаление файла на сервере.
|
||||||
|
Delete,
|
||||||
|
|
||||||
|
/// Клиент запрашивает информацию о файле.
|
||||||
|
Stat,
|
||||||
|
|
||||||
|
/// Ответ на ранее отправленный запрос.
|
||||||
|
Response,
|
||||||
|
|
||||||
|
/// Сервер передаёт клиенту запрошенный файл.
|
||||||
|
FileData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for Command {
|
||||||
|
type Error = HeaderError;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0x01 => Ok(Command::Upload), //клиент → сервер
|
||||||
|
0x02 => Ok(Command::Download),
|
||||||
|
0x03 => Ok(Command::Delete),
|
||||||
|
0x04 => Ok(Command::Stat),
|
||||||
|
0x80 => Ok(Command::Response),
|
||||||
|
0x81 => Ok(Command::FileData),
|
||||||
|
other => Err(HeaderError::UnsupportedCommand(other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_header(
|
||||||
|
input: &[u8; HEADER_SIZE],
|
||||||
|
) -> Result<Header, HeaderError> {
|
||||||
|
// Парсим заголовок:
|
||||||
|
let (remaining, header) = parse_header(input)
|
||||||
|
.map_err(|_| HeaderError::InvalidFormat)?;
|
||||||
|
|
||||||
|
// Выходим с ошибкой, если заголовок был недочитан парсером:
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
return Err(HeaderError::InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Валидация заголовка
|
||||||
|
header.validate()?;
|
||||||
|
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(
|
||||||
|
input: &[u8],
|
||||||
|
) -> IResult<&[u8],Header> {
|
||||||
|
//1. Парсим магические байты "NCLD"
|
||||||
|
let (input, _) = tag(&b"NCLD"[..]).parse(input)?;
|
||||||
|
//2. Парсим и валидируем версию
|
||||||
|
let (input, version) = be_u8(input)?;
|
||||||
|
//3. Парсим и валидируем Header
|
||||||
|
let (input, command) = be_u8(input)?;
|
||||||
|
//4. Парсим и валидируем flags
|
||||||
|
let (input, flags) = be_u16(input)?;
|
||||||
|
//5. Парсим Request без валидации
|
||||||
|
let (input, request_id) = be_u32(input)?;
|
||||||
|
//6. Парсим и валидируем величину заголовка
|
||||||
|
let (input, body_size) = be_u64(input)?;
|
||||||
|
|
||||||
|
let local_header = Header{
|
||||||
|
version,
|
||||||
|
command,
|
||||||
|
flags,
|
||||||
|
request_id,
|
||||||
|
body_size,
|
||||||
|
};
|
||||||
|
Ok((input,local_header))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Реализация публичных методов-геттеров для безопасного доступа к приватным полям
|
||||||
|
impl Header {
|
||||||
|
/// Возвращает версию протокола пакета
|
||||||
|
pub fn version(&self) -> u8 {
|
||||||
|
// Так как тип u8 реализует трейт Copy, значение просто копируется наружу
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает код команды (например, 1, 2, 80 или 81)
|
||||||
|
pub fn command(&self) -> Command {
|
||||||
|
Command::try_from(self.command)
|
||||||
|
.expect("Header уже прошёл валидацию команды")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает битовые флаги пакета (уже прошедшие валидацию)
|
||||||
|
pub fn flags(&self) -> u16 {
|
||||||
|
self.flags
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает уникальный идентификатор запроса клиента
|
||||||
|
pub fn request_id(&self) -> u32 {
|
||||||
|
self.request_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает размер тела сообщения в байтах
|
||||||
|
pub fn body_size(&self) -> u64 {
|
||||||
|
self.body_size
|
||||||
|
}
|
||||||
|
|
||||||
|
///Валидаторы для данных из заголовка:
|
||||||
|
///
|
||||||
|
// Валидация Header
|
||||||
|
pub fn validate(&self) -> Result<(), HeaderError> {
|
||||||
|
//Валидация version
|
||||||
|
if self.version != 1 {
|
||||||
|
return Err(
|
||||||
|
HeaderError::UnsupportedVersion(self.version)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация command
|
||||||
|
Command::try_from(self.command)?;
|
||||||
|
|
||||||
|
// Валидация flags
|
||||||
|
if !(0..=7).contains(&self.flags){
|
||||||
|
return Err(HeaderError::UnsupportedFlagSet (self.flags));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use tokio::{io::{self, AsyncReadExt}, net::TcpStream, stream};
|
||||||
|
|
||||||
|
use crate::protocol::Header;
|
||||||
|
|
||||||
|
const UPLOAD_PREFIX_SIZE: usize = 10; //Размер префикса в UPLOAD
|
||||||
|
pub const MAX_RELATIVE_PATH_BYTES: usize = 128 * 1024; //Размер пути с именем
|
||||||
|
|
||||||
|
struct UploadMetadata {
|
||||||
|
path_size: u64,
|
||||||
|
file_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn handle(stream: &mut TcpStream, header: Header) -> io::Result<()> {
|
||||||
|
println!("Обработчик UPLOAD получил заголовок: {header:?}");
|
||||||
|
|
||||||
|
let mut prefix_buffer = [0_u8; UPLOAD_PREFIX_SIZE];
|
||||||
|
stream.read_exact(&mut buffer).await?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Получены {UPLOAD_PREFIX_SIZE} байт метаданных UPLOAD: {buffer:?}"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UploadMetadata {
|
||||||
|
pub(crate) fn new(
|
||||||
|
file_size: u64,
|
||||||
|
relative_path: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
file_size,
|
||||||
|
relative_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_size(&self) -> u64 {
|
||||||
|
self.file_size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relative_path(&self) -> &str {
|
||||||
|
&self.relative_path
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user