feat: update shared directories at runtime
Client::set_shared_directories rescans the given roots into a fresh in-memory index, swaps it for peers, and re-announces the folder/file counts to the server. The current directory list moves into the client context so the running set is queryable. e2e-verified: a browser sees a directory that was added after login.
This commit is contained in:
@@ -2,7 +2,7 @@ use super::{
|
|||||||
Arc, Client, ClientContext, ClientOperation, ConnectionType, DownloadPeer,
|
Arc, Client, ClientContext, ClientOperation, ConnectionType, DownloadPeer,
|
||||||
DownloadStatus, Listen, Peer, PeerRegistry, Receiver, Result, RwLock,
|
DownloadStatus, Listen, Peer, PeerRegistry, Receiver, Result, RwLock,
|
||||||
RwLockExt, Sender, ServerActor, ServerMessage, Shares, SoulseekRs,
|
RwLockExt, Sender, ServerActor, ServerMessage, Shares, SoulseekRs,
|
||||||
TcpStream, debug, error, info, mpsc, thread, trace, warn,
|
TcpStream, debug, error, info, mpsc, thread, trace,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
@@ -46,6 +46,7 @@ impl Client {
|
|||||||
let shared_folder_count = shares.folder_count();
|
let shared_folder_count = shares.folder_count();
|
||||||
let shared_file_count = shares.file_count();
|
let shared_file_count = shares.file_count();
|
||||||
ctx.shares = shares;
|
ctx.shares = shares;
|
||||||
|
ctx.shared_directories = self.shared_directories.clone();
|
||||||
|
|
||||||
let server_actor = ServerActor::new(
|
let server_actor = ServerActor::new(
|
||||||
self.address.clone(),
|
self.address.clone(),
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ pub struct ClientContext {
|
|||||||
pending_connect_tokens: HashMap<u32, String>,
|
pending_connect_tokens: HashMap<u32, String>,
|
||||||
/// Files we share with peers (read-only after connect).
|
/// Files we share with peers (read-only after connect).
|
||||||
pub shares: Arc<Shares>,
|
pub shares: Arc<Shares>,
|
||||||
|
/// The directories the current share index was built from.
|
||||||
|
pub shared_directories: Vec<String>,
|
||||||
/// Peer listen addresses learned from GetPeerAddress responses.
|
/// Peer listen addresses learned from GetPeerAddress responses.
|
||||||
peer_addresses: HashMap<String, (String, u32)>,
|
peer_addresses: HashMap<String, (String, u32)>,
|
||||||
/// Peer messages waiting for a control connection to that peer.
|
/// Peer messages waiting for a control connection to that peer.
|
||||||
@@ -501,6 +503,7 @@ impl ClientContext {
|
|||||||
private_messages: Vec::new(),
|
private_messages: Vec::new(),
|
||||||
pending_connect_tokens: HashMap::new(),
|
pending_connect_tokens: HashMap::new(),
|
||||||
shares: Arc::new(Shares::empty()),
|
shares: Arc::new(Shares::empty()),
|
||||||
|
shared_directories: Vec::new(),
|
||||||
peer_addresses: HashMap::new(),
|
peer_addresses: HashMap::new(),
|
||||||
pending_peer_messages: HashMap::new(),
|
pending_peer_messages: HashMap::new(),
|
||||||
uploads: HashMap::new(),
|
uploads: HashMap::new(),
|
||||||
@@ -645,10 +648,51 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The directories whose files are shared with other peers.
|
/// The directories whose files are currently shared with other peers.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn shared_directories(&self) -> &[String] {
|
pub fn shared_directories(&self) -> Vec<String> {
|
||||||
&self.shared_directories
|
self.context
|
||||||
|
.read_safe()
|
||||||
|
.map(|ctx| ctx.shared_directories.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the shared directories at runtime: rescan into a fresh
|
||||||
|
/// index (served to peers from then on) and re-announce the new
|
||||||
|
/// folder/file counts to the server.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
|
||||||
|
pub fn set_shared_directories(&self, dirs: Vec<String>) -> Result<()> {
|
||||||
|
let roots: Vec<std::path::PathBuf> = dirs
|
||||||
|
.iter()
|
||||||
|
.filter(|dir| !dir.trim().is_empty())
|
||||||
|
.map(std::path::PathBuf::from)
|
||||||
|
.collect();
|
||||||
|
let shares = if roots.is_empty() {
|
||||||
|
Shares::empty()
|
||||||
|
} else {
|
||||||
|
Shares::scan_many(&roots)
|
||||||
|
};
|
||||||
|
info!(
|
||||||
|
"Now sharing {} files in {} folders from {} directories",
|
||||||
|
shares.file_count(),
|
||||||
|
shares.folder_count(),
|
||||||
|
roots.len()
|
||||||
|
);
|
||||||
|
let folder_count = shares.folder_count();
|
||||||
|
let file_count = shares.file_count();
|
||||||
|
{
|
||||||
|
let mut ctx = self.context.write_safe()?;
|
||||||
|
ctx.shares = Arc::new(shares);
|
||||||
|
ctx.shared_directories = dirs;
|
||||||
|
}
|
||||||
|
self.send_server_message(
|
||||||
|
crate::message::server::MessageFactory::build_shared_folders_message(
|
||||||
|
folder_count,
|
||||||
|
file_count,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl Client {
|
|||||||
|
|
||||||
/// Send a raw server message via the server actor, mapping a dead channel
|
/// Send a raw server message via the server actor, mapping a dead channel
|
||||||
/// to [`SoulseekRs::NotConnected`].
|
/// to [`SoulseekRs::NotConnected`].
|
||||||
fn send_server_message(
|
pub(super) fn send_server_message(
|
||||||
&self,
|
&self,
|
||||||
message: crate::message::Message,
|
message: crate::message::Message,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ impl Shares {
|
|||||||
|
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
let mut folder_count = 0;
|
let mut folder_count = 0;
|
||||||
scan_root(root, &root_display_name(root), &mut files, &mut folder_count);
|
scan_root(
|
||||||
|
root,
|
||||||
|
&root_display_name(root),
|
||||||
|
&mut files,
|
||||||
|
&mut folder_count,
|
||||||
|
);
|
||||||
Ok(Self::from_files(files, folder_count))
|
Ok(Self::from_files(files, folder_count))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,8 +283,7 @@ mod tests {
|
|||||||
fn scan_many_merges_roots() {
|
fn scan_many_merges_roots() {
|
||||||
let root_a = temp_tree();
|
let root_a = temp_tree();
|
||||||
let root_b = temp_tree();
|
let root_b = temp_tree();
|
||||||
let shares =
|
let shares = Shares::scan_many(&[root_a.clone(), root_b.clone()]);
|
||||||
Shares::scan_many(&[root_a.clone(), root_b.clone()]);
|
|
||||||
assert_eq!(shares.file_count(), 6);
|
assert_eq!(shares.file_count(), 6);
|
||||||
assert_eq!(shares.folder_count(), 4);
|
assert_eq!(shares.folder_count(), 4);
|
||||||
// Files from both roots are reachable under their root's name.
|
// Files from both roots are reachable under their root's name.
|
||||||
@@ -303,8 +307,7 @@ mod tests {
|
|||||||
std::fs::write(music_a.join("a.mp3"), b"a").unwrap();
|
std::fs::write(music_a.join("a.mp3"), b"a").unwrap();
|
||||||
std::fs::write(music_b.join("b.mp3"), b"b").unwrap();
|
std::fs::write(music_b.join("b.mp3"), b"b").unwrap();
|
||||||
|
|
||||||
let shares =
|
let shares = Shares::scan_many(&[music_a.clone(), music_b.clone()]);
|
||||||
Shares::scan_many(&[music_a.clone(), music_b.clone()]);
|
|
||||||
assert!(shares.get("music\\a.mp3").is_some());
|
assert!(shares.get("music\\a.mp3").is_some());
|
||||||
assert!(
|
assert!(
|
||||||
shares.get("music (2)\\b.mp3").is_some(),
|
shares.get("music (2)\\b.mp3").is_some(),
|
||||||
|
|||||||
@@ -1249,6 +1249,62 @@ fn two_real_clients_search_and_download() {
|
|||||||
let _ = std::fs::remove_dir_all(download_dir);
|
let _ = std::fs::remove_dir_all(download_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_runtime_share_update_is_visible_to_browsers() {
|
||||||
|
let server = server_or_skip!();
|
||||||
|
|
||||||
|
// The sharer starts sharing NOTHING, then adds a directory at runtime
|
||||||
|
// (what the TUI settings screen does).
|
||||||
|
let sharer_port = free_port().expect("sharer port");
|
||||||
|
let mut sharer = Client::with_settings(server.listening_settings(
|
||||||
|
"e2e_reshare",
|
||||||
|
"pw",
|
||||||
|
sharer_port,
|
||||||
|
));
|
||||||
|
sharer.connect().expect("sharer connect");
|
||||||
|
assert!(sharer.login().expect("sharer login"));
|
||||||
|
assert!(sharer.shared_directories().is_empty());
|
||||||
|
|
||||||
|
let share_dir = unique_download_dir();
|
||||||
|
std::fs::create_dir_all(share_dir.join("new")).unwrap();
|
||||||
|
std::fs::write(share_dir.join("new").join("late.mp3"), b"yyyy").unwrap();
|
||||||
|
sharer
|
||||||
|
.set_shared_directories(vec![share_dir.display().to_string()])
|
||||||
|
.expect("runtime share update");
|
||||||
|
assert_eq!(sharer.shared_directories().len(), 1);
|
||||||
|
|
||||||
|
let browser_port = free_port().expect("browser port");
|
||||||
|
let mut browser = Client::with_settings(server.listening_settings(
|
||||||
|
"e2e_reshare_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_reshare").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_reshare") {
|
||||||
|
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 == "late.mp3")),
|
||||||
|
"the listing should include the file shared at runtime"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(share_dir);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn browse_a_peers_shared_files() {
|
fn browse_a_peers_shared_files() {
|
||||||
let server = server_or_skip!();
|
let server = server_or_skip!();
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ impl MainTui {
|
|||||||
let shortcuts_line = format_shortcuts_styled(&shortcuts);
|
let shortcuts_line = format_shortcuts_styled(&shortcuts);
|
||||||
// Surface our own sharing status in the block title.
|
// Surface our own sharing status in the block title.
|
||||||
let shared = self.client.shared_directories();
|
let shared = self.client.shared_directories();
|
||||||
let sharing = match shared {
|
let sharing = match shared.as_slice() {
|
||||||
[] => "off".to_string(),
|
[] => "off".to_string(),
|
||||||
[only] => only.clone(),
|
[only] => only.clone(),
|
||||||
more => format!("{} folders", more.len()),
|
more => format!("{} folders", more.len()),
|
||||||
|
|||||||
Reference in New Issue
Block a user