feat(tsconnect/wasm): add hasShares filter to listDrivePeers
PeerCapabilityTaildriveSharer says a peer may share with us, not that it
does, so listDrivePeers is a superset of the peers actually exposing
shares. listDrivePeers now takes an options object; with
{hasShares: true} each candidate's taildrive root is probed with a
Depth-1 PROPFIND and only peers listing at least one share are kept.
The probe and its multistatus parsing live in cmd/tsconnect/driveprobe
so they can be tested without syscall/js. Probes run in parallel with a
bounded worker count, a per-probe timeout and a bounded response read;
a probe that fails drops that peer and is logged rather than failing
the call, so the filter is positive-only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,14 +6,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
|
||||
"tailscale.com/cmd/tsconnect/driveprobe"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
@@ -228,11 +231,37 @@ func wireDriveJS(i *jsIPN, driveFS *jsFileSystemForRemote, m map[string]any) {
|
||||
return nil
|
||||
})
|
||||
|
||||
m["listDrivePeers"] = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
||||
return i.listDrivePeers()
|
||||
m["listDrivePeers"] = js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
var hasShares bool
|
||||
if len(args) > 0 && !args[0].IsUndefined() && !args[0].IsNull() {
|
||||
if v := args[0].Get("hasShares"); v.Type() == js.TypeBoolean {
|
||||
hasShares = v.Bool()
|
||||
}
|
||||
}
|
||||
return i.listDrivePeers(hasShares)
|
||||
})
|
||||
}
|
||||
|
||||
// filterPeersWithShares keeps only the peers whose Taildrive endpoint lists at
|
||||
// least one share for us, preserving the order of the input.
|
||||
func (i *jsIPN) filterPeersWithShares(peers []jsDrivePeer) []jsDrivePeer {
|
||||
urls := make([]string, len(peers))
|
||||
for n, p := range peers {
|
||||
urls[n] = p.PeerAPIURL
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: i.lb.Dialer().PeerAPITransport()}
|
||||
results := driveprobe.HasSharesMulti(context.Background(), client, urls, log.Printf)
|
||||
|
||||
kept := make([]jsDrivePeer, 0, len(peers))
|
||||
for n, p := range peers {
|
||||
if results[n] {
|
||||
kept = append(kept, p)
|
||||
}
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
||||
type jsDrivePeer struct {
|
||||
Name string `json:"name"`
|
||||
PeerAPIURL string `json:"peerAPIURL"`
|
||||
@@ -248,7 +277,12 @@ type jsDrivePeer struct {
|
||||
//
|
||||
// The cap means a peer is allowed to share with us, not that it currently
|
||||
// exposes any share, so the result is a superset of the peers with shares.
|
||||
func (i *jsIPN) listDrivePeers() js.Value {
|
||||
//
|
||||
// hasShares narrows it to peers we confirmed are exposing at least one share,
|
||||
// at the cost of one peerAPI round-trip per candidate (run in parallel). It is
|
||||
// a positive filter: a peer we could not reach is left out, which is not the
|
||||
// same as knowing it has no shares.
|
||||
func (i *jsIPN) listDrivePeers(hasShares bool) js.Value {
|
||||
return makePromise(func() (any, error) {
|
||||
if !i.lb.DriveAccessEnabled() {
|
||||
return "[]", nil
|
||||
@@ -301,6 +335,10 @@ func (i *jsIPN) listDrivePeers() js.Value {
|
||||
})
|
||||
}
|
||||
|
||||
if hasShares && len(peers) > 0 {
|
||||
peers = i.filterPeersWithShares(peers)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(peers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listDrivePeers: marshal: %w", err)
|
||||
|
||||
@@ -20,7 +20,7 @@ func initDriveForRemote(_ *tsd.System) *jsFileSystemForRemote { return nil }
|
||||
func wireDriveJS(_ *jsIPN, _ *jsFileSystemForRemote, _ map[string]any) {}
|
||||
|
||||
// listDrivePeers returns an empty list when the drive feature is omitted.
|
||||
func (i *jsIPN) listDrivePeers() js.Value {
|
||||
func (i *jsIPN) listDrivePeers(_ bool) js.Value {
|
||||
return makePromise(func() (any, error) {
|
||||
return "[]", nil
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user