feat(lib): answer FolderContentsRequest with the folder's listing

"Download folder" in SoulseekQt and Nicotine+ asks the sharer for that
one folder (peer code 36) instead of the whole share, and a sharer that
never answers leaves the folder download hanging. The requested folder
now comes back as a FolderContentsResponse (code 37), which carries a
directory listing in the same form a browse does, so the two share one
codec. The browse and folder replies also share one reply helper.
This commit is contained in:
Michel de Graaf
2026-09-07 15:21:42 +02:00
parent 53b32e0201
commit 4839e36be8
7 changed files with 266 additions and 55 deletions
+59 -28
View File
@@ -756,35 +756,36 @@ 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),
Self::reply_to_peer(
&client_context,
&requester_key,
|ctx| {
crate::message::peer::build_shared_file_list(
&shared_directories(ctx),
)
}
Err(_) => continue,
};
if let Some(registry) = registry {
let _ = registry.send_to_peer(
&requester_key,
PeerMessage::SendMessage(message),
);
}
},
);
}
ClientOperation::FolderContentsRequested {
requester_key,
token,
folder,
} => {
// ponytail: walks the whole share for one folder, as a
// browse does; index by directory if it ever shows up.
Self::reply_to_peer(
&client_context,
&requester_key,
|ctx| {
let dirs: Vec<_> = shared_directories(ctx)
.into_iter()
.filter(|dir| dir.name == folder)
.collect();
crate::message::peer::build_folder_contents(
token, &folder, &dirs,
)
},
);
}
ClientOperation::BrowseResult {
username,
@@ -845,6 +846,22 @@ impl Client {
});
}
/// Answer a peer with the message `build` derives from the client state.
fn reply_to_peer(
client_context: &Arc<RwLock<ClientContext>>,
requester_key: &str,
build: impl FnOnce(&ClientContext) -> crate::message::Message,
) {
let (registry, message) = match client_context.read_safe() {
Ok(ctx) => (ctx.peer_registry.clone(), build(&ctx)),
Err(_) => return,
};
if let Some(registry) = registry {
let _ = registry
.send_to_peer(requester_key, PeerMessage::SendMessage(message));
}
}
fn sweep_expired_connects(client_context: &Arc<RwLock<ClientContext>>) {
let expired = match client_context.write_safe() {
Ok(mut ctx) => ctx.take_expired_connects(Instant::now()),
@@ -855,3 +872,17 @@ impl Client {
}
}
}
/// Our shares in the wire form a browse or folder reply carries.
fn shared_directories(
ctx: &ClientContext,
) -> Vec<crate::message::peer::SharedDirectory> {
ctx.shares
.directories()
.into_iter()
.map(|(name, files)| crate::message::peer::SharedDirectory {
name,
files,
})
.collect()
}