feat: browse another user's shared files
Add Client::browse_user (sends GetShareFileList, code 4, connecting first if needed via the same queue-and-flush path as search replies) and take_browse_result. A new SharedFileListResponseHandler parses the peer's code-5 reply into SharedDirectory entries, forwarded as ClientOperation::BrowseResult and stored per user. Verified end to end: a second real client browses a sharer and receives its file listing.
This commit is contained in:
@@ -3,7 +3,8 @@ use crate::client::ClientOperation;
|
||||
use crate::dispatcher::MessageDispatcher;
|
||||
use crate::message::peer::{
|
||||
FileSearchResponse, GetShareFileList, PeerInit, PlaceInQueueResponse,
|
||||
QueueUploadHandler, TransferRequest, TransferResponse, UploadFailedHandler,
|
||||
QueueUploadHandler, SharedDirectory, SharedFileListResponseHandler,
|
||||
TransferRequest, TransferResponse, UploadFailedHandler,
|
||||
};
|
||||
use crate::message::server::MessageFactory;
|
||||
use crate::message::{Handlers, Message, MessageReader, MessageType};
|
||||
@@ -40,6 +41,8 @@ pub enum PeerMessage {
|
||||
IncomingQueueUpload(String),
|
||||
/// A peer asked to browse our shared files (they sent us code 4).
|
||||
ShareListRequested,
|
||||
/// A peer we are browsing sent us their shared-file listing (code 5).
|
||||
ShareListReceived(Vec<SharedDirectory>),
|
||||
/// Offer the queued file to that peer: send an upload TransferRequest.
|
||||
ServeUpload {
|
||||
token: u32,
|
||||
@@ -153,6 +156,7 @@ impl PeerActor {
|
||||
handlers.register_handler(UploadFailedHandler);
|
||||
handlers.register_handler(PlaceInQueueResponse);
|
||||
handlers.register_handler(QueueUploadHandler);
|
||||
handlers.register_handler(SharedFileListResponseHandler);
|
||||
handlers.register_handler(PeerInit);
|
||||
|
||||
self.dispatcher = Some(MessageDispatcher::new(
|
||||
@@ -358,6 +362,17 @@ impl PeerActor {
|
||||
error!("[peer_actor] forward ShareListRequested: {}", e);
|
||||
}
|
||||
}
|
||||
PeerMessage::ShareListReceived(directories) => {
|
||||
let username = self.peer_username();
|
||||
if let Err(e) =
|
||||
self.client_channel.send(ClientOperation::BrowseResult {
|
||||
username,
|
||||
directories,
|
||||
})
|
||||
{
|
||||
error!("[peer_actor] forward BrowseResult: {}", e);
|
||||
}
|
||||
}
|
||||
PeerMessage::RequestTransfer(download) => {
|
||||
let message = MessageFactory::build_transfer_request_message(
|
||||
&download.filename,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
Transfer,
|
||||
actor::{ActorSystem, peer_registry::PeerRegistry},
|
||||
error::{Result, SoulseekRs},
|
||||
message::peer::{FileEntry, build_file_search_response},
|
||||
message::peer::{FileEntry, SharedDirectory, build_file_search_response},
|
||||
peer::{
|
||||
ConnectionType, DownloadPeer, NewPeer, Peer, PeerMessage,
|
||||
listen::Listen,
|
||||
@@ -177,6 +177,11 @@ pub enum ClientOperation {
|
||||
ShareListRequested {
|
||||
requester_key: String,
|
||||
},
|
||||
/// A peer we are browsing returned their shared-file listing.
|
||||
BrowseResult {
|
||||
username: String,
|
||||
directories: Vec<SharedDirectory>,
|
||||
},
|
||||
/// 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.
|
||||
@@ -202,6 +207,8 @@ pub struct ClientContext {
|
||||
uploads: HashMap<u32, UploadJob>,
|
||||
/// Upload tokens waiting for the downloader's address to be resolved.
|
||||
pending_serves: HashMap<String, Vec<u32>>,
|
||||
/// Shared-file listings received from peers we browsed.
|
||||
browse_results: HashMap<String, Vec<SharedDirectory>>,
|
||||
actor_system: Arc<ActorSystem>,
|
||||
}
|
||||
impl Default for ClientContext {
|
||||
@@ -491,6 +498,7 @@ impl ClientContext {
|
||||
pending_peer_messages: HashMap::new(),
|
||||
uploads: HashMap::new(),
|
||||
pending_serves: HashMap::new(),
|
||||
browse_results: HashMap::new(),
|
||||
downloads: DownloadStore::new(),
|
||||
actor_system,
|
||||
}
|
||||
@@ -535,6 +543,23 @@ impl ClientContext {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Store a shared-file listing received from browsing `username`.
|
||||
pub fn store_browse_result(
|
||||
&mut self,
|
||||
username: String,
|
||||
directories: Vec<SharedDirectory>,
|
||||
) {
|
||||
self.browse_results.insert(username, directories);
|
||||
}
|
||||
|
||||
/// Remove and return the shared-file listing browsed from `username`.
|
||||
pub fn take_browse_result(
|
||||
&mut self,
|
||||
username: &str,
|
||||
) -> Option<Vec<SharedDirectory>> {
|
||||
self.browse_results.remove(username)
|
||||
}
|
||||
|
||||
/// Remember that a server-brokered connection to `username` is pending under
|
||||
/// `token`; the peer will quote it back in a PierceFirewall.
|
||||
pub fn add_pending_connect(&mut self, token: u32, username: String) {
|
||||
@@ -721,6 +746,53 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request a peer's shared-file listing. When it arrives it can be
|
||||
/// retrieved with [`Client::take_browse_result`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the client's context lock is poisoned.
|
||||
pub fn browse_user(&self, username: &str) -> Result<()> {
|
||||
let request =
|
||||
crate::message::server::MessageFactory::build_get_share_file_list();
|
||||
let (connected, registry) = {
|
||||
let ctx = self.context.read_safe()?;
|
||||
(
|
||||
ctx.peer_registry
|
||||
.as_ref()
|
||||
.is_some_and(|r| r.contains(username)),
|
||||
ctx.peer_registry.clone(),
|
||||
)
|
||||
};
|
||||
if connected {
|
||||
if let Some(registry) = registry {
|
||||
let _ = registry
|
||||
.send_to_peer(username, PeerMessage::SendMessage(request));
|
||||
}
|
||||
} else {
|
||||
self.context
|
||||
.write_safe()?
|
||||
.queue_peer_message(username, request);
|
||||
if let Some(handle) = &self.server_handle {
|
||||
let _ = handle
|
||||
.send(ServerMessage::GetPeerAddress(username.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove and return a peer's shared-file listing requested via
|
||||
/// [`Client::browse_user`], if it has arrived.
|
||||
#[must_use]
|
||||
pub fn take_browse_result(
|
||||
&self,
|
||||
username: &str,
|
||||
) -> Option<Vec<SharedDirectory>> {
|
||||
self.context
|
||||
.write_safe()
|
||||
.ok()
|
||||
.and_then(|mut ctx| ctx.take_browse_result(username))
|
||||
}
|
||||
|
||||
/// Ask the server for a peer's address and open a direct control
|
||||
/// connection to it. Downloads queued for that peer are sent automatically
|
||||
/// once the connection is established.
|
||||
@@ -1821,6 +1893,18 @@ impl Client {
|
||||
);
|
||||
}
|
||||
}
|
||||
ClientOperation::BrowseResult {
|
||||
username,
|
||||
directories,
|
||||
} => {
|
||||
if let Ok(mut ctx) = client_context.write_safe()
|
||||
{
|
||||
ctx.store_browse_result(
|
||||
username,
|
||||
directories,
|
||||
);
|
||||
}
|
||||
}
|
||||
ClientOperation::PeerConnectFailed(
|
||||
id,
|
||||
username,
|
||||
|
||||
@@ -24,4 +24,5 @@ pub mod prelude {
|
||||
pub use actor::server_actor::{PeerAddress, UserMessage};
|
||||
pub use client::{Client, ClientSettings};
|
||||
pub use error::{Result, SoulseekRs};
|
||||
pub use message::peer::SharedDirectory;
|
||||
pub use types::{DownloadStatus, File, Search, SearchResult, Transfer};
|
||||
|
||||
@@ -17,7 +17,8 @@ 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,
|
||||
SharedDirectory, SharedFileListResponseHandler, build_shared_file_list,
|
||||
parse_shared_file_list,
|
||||
};
|
||||
pub use transfer_request::TransferRequest;
|
||||
pub use transfer_response::TransferResponse;
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
//! 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::message::{Message, MessageHandler};
|
||||
use crate::peer::PeerMessage;
|
||||
use crate::utils::zlib::{compress_stored, deflate};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
/// One shared directory and the files directly in it (basename + size).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -12,6 +14,18 @@ pub struct SharedDirectory {
|
||||
pub files: Vec<(String, u64)>,
|
||||
}
|
||||
|
||||
/// Receives a peer's `SharedFileListResponse` (peer code 5) when browsing them.
|
||||
pub struct SharedFileListResponseHandler;
|
||||
impl MessageHandler<PeerMessage> for SharedFileListResponseHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
5
|
||||
}
|
||||
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
|
||||
let directories = parse_shared_file_list(message);
|
||||
let _ = sender.send(PeerMessage::ShareListReceived(directories));
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `SharedFileListResponse` (peer code 5) from the directory listing.
|
||||
#[must_use]
|
||||
pub fn build_shared_file_list(dirs: &[SharedDirectory]) -> Message {
|
||||
|
||||
@@ -122,6 +122,12 @@ impl MessageFactory {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Ask a peer for their shared-file listing (peer code 4, no body).
|
||||
#[must_use]
|
||||
pub fn build_get_share_file_list() -> Message {
|
||||
Message::new().write_int32(4).clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build_queue_upload_message(filename: &str) -> Message {
|
||||
Message::new()
|
||||
|
||||
@@ -1145,3 +1145,53 @@ fn two_real_clients_search_and_download() {
|
||||
let _ = std::fs::remove_dir_all(share_dir);
|
||||
let _ = std::fs::remove_dir_all(download_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_a_peers_shared_files() {
|
||||
let server = server_or_skip!();
|
||||
|
||||
let share_dir = unique_download_dir();
|
||||
std::fs::create_dir_all(share_dir.join("album")).unwrap();
|
||||
std::fs::write(share_dir.join("album").join("track.flac"), b"xxxx")
|
||||
.unwrap();
|
||||
|
||||
let sharer_port = free_port().expect("sharer port");
|
||||
let mut sharer = Client::with_settings(ClientSettings {
|
||||
shared_directory: Some(share_dir.display().to_string()),
|
||||
..server.listening_settings("e2e_browsee", "pw", sharer_port)
|
||||
});
|
||||
sharer.connect().expect("sharer connect");
|
||||
assert!(sharer.login().expect("sharer login"));
|
||||
|
||||
let browser_port = free_port().expect("browser port");
|
||||
let mut browser = Client::with_settings(server.listening_settings(
|
||||
"e2e_browser",
|
||||
"pw",
|
||||
browser_port,
|
||||
));
|
||||
browser.connect().expect("browser connect");
|
||||
assert!(browser.login().expect("browser login"));
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
browser.browse_user("e2e_browsee").expect("browse request");
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut listing = None;
|
||||
while Instant::now() < deadline {
|
||||
if let Some(result) = browser.take_browse_result("e2e_browsee") {
|
||||
listing = Some(result);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
let directories = listing.expect("browser should receive the listing");
|
||||
assert!(
|
||||
directories
|
||||
.iter()
|
||||
.any(|d| { d.files.iter().any(|(name, _)| name == "track.flac") }),
|
||||
"the listing should include the shared file"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(share_dir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user