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::dispatcher::MessageDispatcher;
|
||||||
use crate::message::peer::{
|
use crate::message::peer::{
|
||||||
FileSearchResponse, GetShareFileList, PeerInit, PlaceInQueueResponse,
|
FileSearchResponse, GetShareFileList, PeerInit, PlaceInQueueResponse,
|
||||||
QueueUploadHandler, TransferRequest, TransferResponse, UploadFailedHandler,
|
QueueUploadHandler, SharedDirectory, SharedFileListResponseHandler,
|
||||||
|
TransferRequest, TransferResponse, UploadFailedHandler,
|
||||||
};
|
};
|
||||||
use crate::message::server::MessageFactory;
|
use crate::message::server::MessageFactory;
|
||||||
use crate::message::{Handlers, Message, MessageReader, MessageType};
|
use crate::message::{Handlers, Message, MessageReader, MessageType};
|
||||||
@@ -40,6 +41,8 @@ pub enum PeerMessage {
|
|||||||
IncomingQueueUpload(String),
|
IncomingQueueUpload(String),
|
||||||
/// A peer asked to browse our shared files (they sent us code 4).
|
/// A peer asked to browse our shared files (they sent us code 4).
|
||||||
ShareListRequested,
|
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.
|
/// Offer the queued file to that peer: send an upload TransferRequest.
|
||||||
ServeUpload {
|
ServeUpload {
|
||||||
token: u32,
|
token: u32,
|
||||||
@@ -153,6 +156,7 @@ impl PeerActor {
|
|||||||
handlers.register_handler(UploadFailedHandler);
|
handlers.register_handler(UploadFailedHandler);
|
||||||
handlers.register_handler(PlaceInQueueResponse);
|
handlers.register_handler(PlaceInQueueResponse);
|
||||||
handlers.register_handler(QueueUploadHandler);
|
handlers.register_handler(QueueUploadHandler);
|
||||||
|
handlers.register_handler(SharedFileListResponseHandler);
|
||||||
handlers.register_handler(PeerInit);
|
handlers.register_handler(PeerInit);
|
||||||
|
|
||||||
self.dispatcher = Some(MessageDispatcher::new(
|
self.dispatcher = Some(MessageDispatcher::new(
|
||||||
@@ -358,6 +362,17 @@ impl PeerActor {
|
|||||||
error!("[peer_actor] forward ShareListRequested: {}", e);
|
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) => {
|
PeerMessage::RequestTransfer(download) => {
|
||||||
let message = MessageFactory::build_transfer_request_message(
|
let message = MessageFactory::build_transfer_request_message(
|
||||||
&download.filename,
|
&download.filename,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
Transfer,
|
Transfer,
|
||||||
actor::{ActorSystem, peer_registry::PeerRegistry},
|
actor::{ActorSystem, peer_registry::PeerRegistry},
|
||||||
error::{Result, SoulseekRs},
|
error::{Result, SoulseekRs},
|
||||||
message::peer::{FileEntry, build_file_search_response},
|
message::peer::{FileEntry, SharedDirectory, build_file_search_response},
|
||||||
peer::{
|
peer::{
|
||||||
ConnectionType, DownloadPeer, NewPeer, Peer, PeerMessage,
|
ConnectionType, DownloadPeer, NewPeer, Peer, PeerMessage,
|
||||||
listen::Listen,
|
listen::Listen,
|
||||||
@@ -177,6 +177,11 @@ pub enum ClientOperation {
|
|||||||
ShareListRequested {
|
ShareListRequested {
|
||||||
requester_key: String,
|
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
|
/// A direct outbound connection to this peer failed before it was
|
||||||
/// established — the peer is likely firewalled, so fall back to asking the
|
/// established — the peer is likely firewalled, so fall back to asking the
|
||||||
/// server to broker the connection. Carries the reporting actor's id.
|
/// server to broker the connection. Carries the reporting actor's id.
|
||||||
@@ -202,6 +207,8 @@ pub struct ClientContext {
|
|||||||
uploads: HashMap<u32, UploadJob>,
|
uploads: HashMap<u32, UploadJob>,
|
||||||
/// Upload tokens waiting for the downloader's address to be resolved.
|
/// Upload tokens waiting for the downloader's address to be resolved.
|
||||||
pending_serves: HashMap<String, Vec<u32>>,
|
pending_serves: HashMap<String, Vec<u32>>,
|
||||||
|
/// Shared-file listings received from peers we browsed.
|
||||||
|
browse_results: HashMap<String, Vec<SharedDirectory>>,
|
||||||
actor_system: Arc<ActorSystem>,
|
actor_system: Arc<ActorSystem>,
|
||||||
}
|
}
|
||||||
impl Default for ClientContext {
|
impl Default for ClientContext {
|
||||||
@@ -491,6 +498,7 @@ impl ClientContext {
|
|||||||
pending_peer_messages: HashMap::new(),
|
pending_peer_messages: HashMap::new(),
|
||||||
uploads: HashMap::new(),
|
uploads: HashMap::new(),
|
||||||
pending_serves: HashMap::new(),
|
pending_serves: HashMap::new(),
|
||||||
|
browse_results: HashMap::new(),
|
||||||
downloads: DownloadStore::new(),
|
downloads: DownloadStore::new(),
|
||||||
actor_system,
|
actor_system,
|
||||||
}
|
}
|
||||||
@@ -535,6 +543,23 @@ impl ClientContext {
|
|||||||
.unwrap_or_default()
|
.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
|
/// Remember that a server-brokered connection to `username` is pending under
|
||||||
/// `token`; the peer will quote it back in a PierceFirewall.
|
/// `token`; the peer will quote it back in a PierceFirewall.
|
||||||
pub fn add_pending_connect(&mut self, token: u32, username: String) {
|
pub fn add_pending_connect(&mut self, token: u32, username: String) {
|
||||||
@@ -721,6 +746,53 @@ impl Client {
|
|||||||
Ok(())
|
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
|
/// Ask the server for a peer's address and open a direct control
|
||||||
/// connection to it. Downloads queued for that peer are sent automatically
|
/// connection to it. Downloads queued for that peer are sent automatically
|
||||||
/// once the connection is established.
|
/// 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(
|
ClientOperation::PeerConnectFailed(
|
||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ pub mod prelude {
|
|||||||
pub use actor::server_actor::{PeerAddress, UserMessage};
|
pub use actor::server_actor::{PeerAddress, UserMessage};
|
||||||
pub use client::{Client, ClientSettings};
|
pub use client::{Client, ClientSettings};
|
||||||
pub use error::{Result, SoulseekRs};
|
pub use error::{Result, SoulseekRs};
|
||||||
|
pub use message::peer::SharedDirectory;
|
||||||
pub use types::{DownloadStatus, File, Search, SearchResult, Transfer};
|
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 place_in_queue_response::PlaceInQueueResponse;
|
||||||
pub use queue_upload::QueueUploadHandler;
|
pub use queue_upload::QueueUploadHandler;
|
||||||
pub use shared_file_list::{
|
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_request::TransferRequest;
|
||||||
pub use transfer_response::TransferResponse;
|
pub use transfer_response::TransferResponse;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
//! sent in reply to `GetShareFileList` (code 4). The payload is zlib-compressed
|
//! sent in reply to `GetShareFileList` (code 4). The payload is zlib-compressed
|
||||||
//! and groups files by their virtual directory.
|
//! 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 crate::utils::zlib::{compress_stored, deflate};
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
/// One shared directory and the files directly in it (basename + size).
|
/// One shared directory and the files directly in it (basename + size).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -12,6 +14,18 @@ pub struct SharedDirectory {
|
|||||||
pub files: Vec<(String, u64)>,
|
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.
|
/// Build a `SharedFileListResponse` (peer code 5) from the directory listing.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build_shared_file_list(dirs: &[SharedDirectory]) -> Message {
|
pub fn build_shared_file_list(dirs: &[SharedDirectory]) -> Message {
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ impl MessageFactory {
|
|||||||
.clone()
|
.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]
|
#[must_use]
|
||||||
pub fn build_queue_upload_message(filename: &str) -> Message {
|
pub fn build_queue_upload_message(filename: &str) -> Message {
|
||||||
Message::new()
|
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(share_dir);
|
||||||
let _ = std::fs::remove_dir_all(download_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