feat(shares): reply to browse requests with a SharedFileListResponse (code 5)
GetShareFileList (peer code 4) previously replied with a malformed message (a server SharedFolders stats message over a peer connection). It now emits ShareListRequested, and the client builds a real SharedFileListResponse (code 5) from its shares grouped by directory. Adds a std-only code-5 encoder/decoder (build_shared_file_list / parse_shared_file_list) with a roundtrip test, and Shares::directories() to group files. The decoder is the foundation for the Browse feature.
This commit is contained in:
@@ -38,6 +38,8 @@ pub enum PeerMessage {
|
||||
RequestTransfer(Download),
|
||||
/// A peer queued one of our shared files for download (they sent us code 43).
|
||||
IncomingQueueUpload(String),
|
||||
/// A peer asked to browse our shared files (they sent us code 4).
|
||||
ShareListRequested,
|
||||
/// Offer the queued file to that peer: send an upload TransferRequest.
|
||||
ServeUpload {
|
||||
token: u32,
|
||||
@@ -347,6 +349,15 @@ impl PeerActor {
|
||||
);
|
||||
self.send_message(message);
|
||||
}
|
||||
PeerMessage::ShareListRequested => {
|
||||
let requester_key = self.peer_username();
|
||||
if let Err(e) = self
|
||||
.client_channel
|
||||
.send(ClientOperation::ShareListRequested { requester_key })
|
||||
{
|
||||
error!("[peer_actor] forward ShareListRequested: {}", e);
|
||||
}
|
||||
}
|
||||
PeerMessage::RequestTransfer(download) => {
|
||||
let message = MessageFactory::build_transfer_request_message(
|
||||
&download.filename,
|
||||
|
||||
@@ -173,6 +173,10 @@ pub enum ClientOperation {
|
||||
StartUpload {
|
||||
token: u32,
|
||||
},
|
||||
/// A peer asked to browse our shared files; send our SharedFileListResponse.
|
||||
ShareListRequested {
|
||||
requester_key: String,
|
||||
},
|
||||
/// A direct outbound connection to this peer failed before it was
|
||||
/// established — the peer is likely firewalled, so fall back to asking the
|
||||
/// server to broker the connection. Carries the reporting actor's id.
|
||||
@@ -1784,6 +1788,39 @@ impl Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientOperation::ShareListRequested {
|
||||
requester_key,
|
||||
} => {
|
||||
// Reply with our full shared-file listing.
|
||||
let (registry, message) = match client_context
|
||||
.read_safe()
|
||||
{
|
||||
Ok(ctx) => {
|
||||
let dirs = ctx
|
||||
.shares
|
||||
.directories()
|
||||
.into_iter()
|
||||
.map(|(name, files)| {
|
||||
crate::message::peer::SharedDirectory {
|
||||
name,
|
||||
files,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(
|
||||
ctx.peer_registry.clone(),
|
||||
crate::message::peer::build_shared_file_list(&dirs),
|
||||
)
|
||||
}
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some(registry) = registry {
|
||||
let _ = registry.send_to_peer(
|
||||
&requester_key,
|
||||
PeerMessage::SendMessage(message),
|
||||
);
|
||||
}
|
||||
}
|
||||
ClientOperation::PeerConnectFailed(
|
||||
id,
|
||||
username,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::{
|
||||
message::{Message, MessageHandler, server::MessageFactory},
|
||||
message::{Message, MessageHandler},
|
||||
peer::PeerMessage,
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
/// A peer asking to browse our shared files (peer code 4). The client (which
|
||||
/// owns the shares) builds the real SharedFileListResponse in reply.
|
||||
pub struct GetShareFileList;
|
||||
impl MessageHandler<PeerMessage> for GetShareFileList {
|
||||
fn get_code(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
fn handle(&self, _message: &mut Message, sender: Sender<PeerMessage>) {
|
||||
let message = MessageFactory::build_shared_folders_message(100, 800);
|
||||
|
||||
sender.send(PeerMessage::SendMessage(message)).unwrap();
|
||||
let _ = sender.send(PeerMessage::ShareListRequested);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ mod get_share_file_list;
|
||||
mod peer_init;
|
||||
mod place_in_queue_response;
|
||||
mod queue_upload;
|
||||
mod shared_file_list;
|
||||
mod transfer_request;
|
||||
mod transfer_response;
|
||||
mod upload_failed;
|
||||
@@ -15,6 +16,9 @@ pub use get_share_file_list::GetShareFileList;
|
||||
pub use peer_init::PeerInit;
|
||||
pub use place_in_queue_response::PlaceInQueueResponse;
|
||||
pub use queue_upload::QueueUploadHandler;
|
||||
pub use shared_file_list::{
|
||||
SharedDirectory, build_shared_file_list, parse_shared_file_list,
|
||||
};
|
||||
pub use transfer_request::TransferRequest;
|
||||
pub use transfer_response::TransferResponse;
|
||||
pub use upload_failed::UploadFailedHandler;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//! `SharedFileListResponse` (peer code 5): a peer's full shared-file listing,
|
||||
//! sent in reply to `GetShareFileList` (code 4). The payload is zlib-compressed
|
||||
//! and groups files by their virtual directory.
|
||||
|
||||
use crate::message::Message;
|
||||
use crate::utils::zlib::{compress_stored, deflate};
|
||||
|
||||
/// One shared directory and the files directly in it (basename + size).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SharedDirectory {
|
||||
pub name: String,
|
||||
pub files: Vec<(String, u64)>,
|
||||
}
|
||||
|
||||
/// Build a `SharedFileListResponse` (peer code 5) from the directory listing.
|
||||
#[must_use]
|
||||
pub fn build_shared_file_list(dirs: &[SharedDirectory]) -> Message {
|
||||
let mut payload = Message::new();
|
||||
payload.write_int32(dirs.len() as u32);
|
||||
for dir in dirs {
|
||||
payload
|
||||
.write_string(&dir.name)
|
||||
.write_int32(dir.files.len() as u32);
|
||||
for (name, size) in &dir.files {
|
||||
payload
|
||||
.write_int8(1)
|
||||
.write_string(name)
|
||||
.write_int64(*size)
|
||||
.write_string("") // extension
|
||||
.write_int32(0); // attribute count
|
||||
}
|
||||
}
|
||||
payload.write_int32(0); // unknown
|
||||
payload.write_int32(0); // number of private directories
|
||||
|
||||
let compressed = compress_stored(&payload.get_data());
|
||||
Message::new()
|
||||
.write_int32(5)
|
||||
.write_raw_bytes(compressed)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Parse the (zlib-compressed) `SharedFileListResponse` payload. `message` must
|
||||
/// be positioned at the compressed blob (the dispatcher sets pointer 8).
|
||||
///
|
||||
/// Returns an empty listing if the payload is malformed.
|
||||
#[must_use]
|
||||
pub fn parse_shared_file_list(message: &mut Message) -> Vec<SharedDirectory> {
|
||||
let pointer = message.get_pointer();
|
||||
let size = message.get_size();
|
||||
let compressed = message.get_slice(pointer, size);
|
||||
let Ok(data) = deflate(&compressed) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut body = Message::new_with_data(data);
|
||||
let dir_count = body.read_int32();
|
||||
let mut dirs = Vec::new();
|
||||
for _ in 0..dir_count {
|
||||
let name = body.read_string();
|
||||
let file_count = body.read_int32();
|
||||
let mut files = Vec::new();
|
||||
for _ in 0..file_count {
|
||||
body.read_int8(); // code
|
||||
let filename = body.read_string();
|
||||
let file_size = body.read_int64();
|
||||
body.read_string(); // extension
|
||||
let attr_count = body.read_int32();
|
||||
for _ in 0..attr_count {
|
||||
body.read_int32();
|
||||
body.read_int32();
|
||||
}
|
||||
files.push((filename, file_size));
|
||||
}
|
||||
dirs.push(SharedDirectory { name, files });
|
||||
}
|
||||
dirs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_file_list_roundtrips() {
|
||||
let dirs = vec![
|
||||
SharedDirectory {
|
||||
name: "music\\album".to_string(),
|
||||
files: vec![
|
||||
("song one.flac".to_string(), 123),
|
||||
("song two.flac".to_string(), 456),
|
||||
],
|
||||
},
|
||||
SharedDirectory {
|
||||
name: "music".to_string(),
|
||||
files: vec![("top.mp3".to_string(), 789)],
|
||||
},
|
||||
];
|
||||
let message = build_shared_file_list(&dirs);
|
||||
|
||||
// Decode via the same offset the dispatcher would use.
|
||||
let mut decoded = Message::new_with_data(message.get_buffer());
|
||||
decoded.set_pointer(8);
|
||||
assert_eq!(parse_shared_file_list(&mut decoded), dirs);
|
||||
}
|
||||
@@ -128,6 +128,26 @@ impl Shares {
|
||||
&self.files
|
||||
}
|
||||
|
||||
/// Files grouped by their virtual directory (everything before the final
|
||||
/// backslash), as `(directory, [(basename, size)])`. Used to build a
|
||||
/// SharedFileListResponse.
|
||||
#[must_use]
|
||||
pub fn directories(&self) -> Vec<(String, Vec<(String, u64)>)> {
|
||||
let mut by_dir: std::collections::BTreeMap<String, Vec<(String, u64)>> =
|
||||
std::collections::BTreeMap::new();
|
||||
for file in &self.files {
|
||||
let (dir, base) = file
|
||||
.virtual_path
|
||||
.rsplit_once('\\')
|
||||
.unwrap_or(("", file.virtual_path.as_str()));
|
||||
by_dir
|
||||
.entry(dir.to_string())
|
||||
.or_default()
|
||||
.push((base.to_string(), file.size));
|
||||
}
|
||||
by_dir.into_iter().collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn file_count(&self) -> u32 {
|
||||
self.files.len() as u32
|
||||
|
||||
Reference in New Issue
Block a user