feat: browse your own shares as a peer receives them

The window could show any peer's share tree but never this account's, so
"am I actually sharing that?" had no answer short of asking a friend to
browse you. The tree is already there; what was missing was feeding it
from our own index.

B opens it — the same collapsible tree, filter, and navigation the peer
browse offers, built from `Shares::directories()`, which is the listing
GetShareFileList answers with. So what it shows is what the network sees,
not what the config meant to share. r re-scans the disk rather than
re-asking a peer, o steps across to the folders that feed it, and nothing
here queues a download from ourselves.

Attached to a daemon the index lives on the daemon's host, so it comes
back over a new `shares.files` method rather than being scanned locally;
`shares files [--filter]` prints the same listing headless, through the
`emit_listing` that `browse` now shares with it — the two are only
comparable while they are built the same way.

Two allocation fixes came out of the review, both on paths this reaches:
the daemon hands its listing to the DTO instead of deep-copying one it
already owns, and `Shares::directories()` allocates its map key once per
folder rather than once per file, which the peer-facing serve path has
been paying for all along.
This commit is contained in:
Michel de Graaf
2026-09-10 11:44:57 +02:00
parent d6d870b39f
commit 26b551d997
24 changed files with 606 additions and 57 deletions
+22
View File
@@ -163,6 +163,28 @@ pub fn shares_status(ctx: &Ctx) -> CliResult {
Ok(())
}
/// List the share index file by file, in the same records `browse` emits for
/// a peer — so what the network sees of us can be read, and diffed, exactly
/// the way it is read of anyone else.
pub fn shares_files(ctx: &Ctx, filter: Option<&str>) -> CliResult {
let session = Session::open(ctx)?;
let needle = filter.map(str::to_lowercase);
let found = super::social::emit_listing(
ctx,
&session.client.username(),
&session.client.shared_listing(),
needle.as_deref(),
);
if found == 0 {
return Err(CliError::no_results(match filter {
Some(filter) => format!("no shared file matches '{filter}'"),
None => "no files are indexed".to_string(),
}));
}
Ok(())
}
/// Re-scan the configured folders in a live session, so files added since the
/// last run become visible without waiting for the next one.
pub fn shares_reindex(ctx: &Ctx) -> CliResult {
+37 -16
View File
@@ -48,6 +48,42 @@ pub fn fetch_listing(
})
}
/// Emit `user`'s listing one file per record, keeping only the paths that
/// contain `needle` (already lowercased; `None` keeps all). Returns how many
/// were emitted.
///
/// Shared with `shares files`, which prints this session's own index: the two
/// are only comparable while they are built the same way, and a field added
/// here has to reach both.
pub fn emit_listing(
ctx: &Ctx,
user: &str,
directories: &[SharedDirectory],
needle: Option<&str>,
) -> usize {
let mut emitted = 0usize;
for directory in directories {
for file in &directory.files {
let path = full_path(&directory.name, &file.name);
if needle
.is_some_and(|needle| !path.to_lowercase().contains(needle))
{
continue;
}
emitted += 1;
ctx.out.emit(&BrowseRecord {
user: user.to_string(),
directory: directory.name.clone(),
path,
size: file.size,
bitrate: file.attribute(super::transfer::ATTR_BITRATE),
duration: file.attribute(super::transfer::ATTR_DURATION),
});
}
}
emitted
}
pub fn browse(ctx: &Ctx, args: &BrowseArgs) -> CliResult {
let session = Session::open(ctx)?;
ctx.out
@@ -59,22 +95,7 @@ pub fn browse(ctx: &Ctx, args: &BrowseArgs) -> CliResult {
Duration::from_secs(args.timeout),
)?;
let mut files = 0usize;
for directory in &directories {
for file in &directory.files {
files += 1;
ctx.out.emit(&BrowseRecord {
user: args.user.clone(),
directory: directory.name.clone(),
path: full_path(&directory.name, &file.name),
size: file.size,
bitrate: file.attribute(super::transfer::ATTR_BITRATE),
duration: file.attribute(super::transfer::ATTR_DURATION),
});
}
}
if files == 0 {
if emit_listing(ctx, &args.user, &directories, None) == 0 {
return Err(CliError::no_results(format!(
"{} shares no files",
args.user