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:
michel
2026-07-19 18:56:19 +02:00
parent 9e050b59d9
commit f3218475dc
7 changed files with 175 additions and 4 deletions
+16 -1
View File
@@ -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,
+85 -1
View File
@@ -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,
+1
View File
@@ -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};
+2 -1
View File
@@ -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()
+50
View File
@@ -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);
}