feat(wasm): expose taildrive WebDAV server and listDrivePeers via JS bridge

Add drive.go (build tag !ts_omit_drive): implements drive.FileSystemForRemote
with a JS-backed handler. Streams request bodies chunk-by-chunk via
readBodyChunk() and response bodies via write()/end() callbacks so no
full-body buffering occurs regardless of file size. The handler is nil-safe:
returns 404 until setDriveHandler() is called from JS.

Add drive_stub.go (build tag ts_omit_drive): no-op stubs for stripped builds.

Add peer.go: extract buildPeerAPIURL helper (previously inline in run()).

Modify wasm_js.go: call initDriveForRemote before NewLocalBackend (SubSystem
is set-once), expose setDriveHandler and listDrivePeers via wireDriveJS,
and refactor the inline peerAPI URL logic to use buildPeerAPIURL.

listDrivePeers mirrors native driveRemotesFromPeers: returns empty if
DriveAccessEnabled() is false, then filters peers by PeerCapabilityTaildriveSharer
using lb.PeerCaps(addr).HasCapability() (the live ACL-derived cap map).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:57:01 +00:00
parent 0df765eb60
commit 7c5ecfe50f
4 changed files with 377 additions and 27 deletions
+8 -27
View File
@@ -158,6 +158,10 @@ func newIPN(jsConfig js.Value) map[string]any {
sys.Tun.Get().Start()
logid := lpc.PublicID
// initDriveForRemote must be called before NewLocalBackend (SubSystem is set-once).
driveFS := initDriveForRemote(sys)
srv := ipnserver.New(logf, logid, sys.Bus.Get(), sys.NetMon.Get())
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
if err != nil {
@@ -182,7 +186,7 @@ func newIPN(jsConfig js.Value) map[string]any {
}
lb.SetTCPHandlerForFunnelFlow(jsIPN.handleFunnelTCP)
return map[string]any{
m := map[string]any{
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
log.Fatal(`Usage: run({
@@ -373,6 +377,8 @@ func newIPN(jsConfig js.Value) map[string]any {
return jsIPN.localAPI(args[0].String(), args[1].String(), body)
}),
}
wireDriveJS(jsIPN, driveFS, m)
return m
}
type jsIPN struct {
@@ -482,32 +488,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
}
// Peer peerAPI URL from the peer's advertised Services.
peerURL := ""
var pp4, pp6 uint16
for _, s := range p.Hostinfo().Services().All() {
switch s.Proto {
case tailcfg.PeerAPI4:
pp4 = s.Port
case tailcfg.PeerAPI6:
pp6 = s.Port
}
}
if selfHave4 && pp4 != 0 {
for _, a := range p.Addresses().All() {
if a.IsSingleIP() && a.Addr().Is4() {
peerURL = fmt.Sprintf("http://%v", netip.AddrPortFrom(a.Addr(), pp4))
break
}
}
}
if peerURL == "" && selfHave6 && pp6 != 0 {
for _, a := range p.Addresses().All() {
if a.IsSingleIP() && a.Addr().Is6() {
peerURL = fmt.Sprintf("http://%v", netip.AddrPortFrom(a.Addr(), pp6))
break
}
}
}
peerURL := buildPeerAPIURL(p, selfHave4, selfHave6)
return jsNetMapPeerNode{
jsNetMapNode: jsNetMapNode{