feat/tsconnect-file-notify #1

Merged
codinget merged 4 commits from feat/tsconnect-file-notify into webnet 19 hours ago
  1. 38
      cmd/tsconnect/wasm/taildrop.go
  2. 65
      cmd/tsconnect/wasm/wasm_js.go
  3. 7
      feature/taildrop/ext.go
  4. 5
      feature/taildrop/send.go

@ -23,8 +23,11 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/feature/taildrop"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/util/progresstracking"
"tailscale.com/util/rands"
)
// Compile-time check that jsFileOps implements taildrop.FileOps.
@ -78,7 +81,8 @@ func (i *jsIPN) listFileTargets() js.Value {
})
}
// sendFile sends data as filename to the peer identified by stableNodeID.
// sendFile sends data as filename to the peer identified by stableNodeID,
// reporting progress via notifyOutgoingFiles callbacks roughly once per second.
func (i *jsIPN) sendFile(stableNodeID, filename string, data js.Value) js.Value {
return makePromise(func() (any, error) {
ext, err := i.taildropExt()
@ -105,20 +109,46 @@ func (i *jsIPN) sendFile(stableNodeID, filename string, data js.Value) js.Value
}
b := make([]byte, data.Get("byteLength").Int())
js.CopyBytesToGo(b, data)
req, err := http.NewRequest("PUT", dstURL.String()+"/v0/put/"+url.PathEscape(filename), bytes.NewReader(b))
outgoing := &ipn.OutgoingFile{
ID: rands.HexString(30),
PeerID: tailcfg.StableNodeID(stableNodeID),
Name: filename,
DeclaredSize: int64(len(b)),
Started: time.Now(),
}
updates := map[string]*ipn.OutgoingFile{outgoing.ID: outgoing}
// Report final state (success or failure) when the function returns.
var sendErr error
defer func() {
outgoing.Finished = true
outgoing.Succeeded = sendErr == nil
ext.UpdateOutgoingFiles(updates)
}()
body := progresstracking.NewReader(bytes.NewReader(b), time.Second, func(n int, _ error) {
outgoing.Sent = int64(n)
ext.UpdateOutgoingFiles(updates)
})
req, err := http.NewRequest("PUT", dstURL.String()+"/v0/put/"+url.PathEscape(filename), body)
if err != nil {
sendErr = err
return nil, err
}
req.ContentLength = int64(len(b))
client := &http.Client{Transport: i.lb.Dialer().PeerAPITransport()}
resp, err := client.Do(req)
if err != nil {
sendErr = err
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("send file: %s: %s", resp.Status, bytes.TrimSpace(body))
respBody, _ := io.ReadAll(resp.Body)
sendErr = fmt.Errorf("send file: %s: %s", resp.Status, bytes.TrimSpace(respBody))
return nil, sendErr
}
return nil, nil
})

@ -388,12 +388,52 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
log.Printf("Could not generate JSON netmap: %v", err)
}
}
if n.Prefs.Valid() {
if n.Prefs != nil && n.Prefs.Valid() {
jsCallbacks.Call("notifyExitNode", string(n.Prefs.ExitNodeID()))
}
if n.BrowseToURL != nil {
jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
}
if n.FilesWaiting != nil {
jsCallbacks.Call("notifyFilesWaiting")
}
if n.IncomingFiles != nil {
files := make([]jsIncomingFile, len(n.IncomingFiles))
for i, f := range n.IncomingFiles {
files[i] = jsIncomingFile{
Name: f.Name,
Started: f.Started.UnixMilli(),
DeclaredSize: f.DeclaredSize,
Received: f.Received,
Done: f.Done,
}
}
if b, err := json.Marshal(files); err == nil {
jsCallbacks.Call("notifyIncomingFiles", string(b))
} else {
log.Printf("could not marshal IncomingFiles: %v", err)
}
}
if n.OutgoingFiles != nil {
files := make([]jsOutgoingFile, len(n.OutgoingFiles))
for i, f := range n.OutgoingFiles {
files[i] = jsOutgoingFile{
ID: f.ID,
PeerID: string(f.PeerID),
Name: f.Name,
Started: f.Started.UnixMilli(),
DeclaredSize: f.DeclaredSize,
Sent: f.Sent,
Finished: f.Finished,
Succeeded: f.Succeeded,
}
}
if b, err := json.Marshal(files); err == nil {
jsCallbacks.Call("notifyOutgoingFiles", string(b))
} else {
log.Printf("could not marshal OutgoingFiles: %v", err)
}
}
})
go func() {
@ -921,6 +961,29 @@ func (w termWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
// jsIncomingFile is the JSON representation of an in-progress inbound file
// transfer sent to the notifyIncomingFiles callback.
type jsIncomingFile struct {
Name string `json:"name"`
Started int64 `json:"started"` // Unix milliseconds; use new Date(started) in JS
DeclaredSize int64 `json:"declaredSize"` // -1 if unknown
Received int64 `json:"received"` // bytes received so far
Done bool `json:"done"` // true once the file has been fully received
}
// jsOutgoingFile is the JSON representation of an outgoing file transfer
// sent to the notifyOutgoingFiles callback.
type jsOutgoingFile struct {
ID string `json:"id"`
PeerID string `json:"peerID"`
Name string `json:"name"`
Started int64 `json:"started"` // Unix milliseconds
DeclaredSize int64 `json:"declaredSize"` // -1 if unknown
Sent int64 `json:"sent"` // bytes sent so far
Finished bool `json:"finished"`
Succeeded bool `json:"succeeded"` // only meaningful when finished
}
type jsNetMap struct {
Self jsNetMapSelfNode `json:"self"`
Peers []jsNetMapPeerNode `json:"peers"`

@ -418,6 +418,13 @@ func (e *Extension) taildropTargetStatus(p tailcfg.NodeView, nb ipnext.NodeBacke
return ipnstate.TaildropTargetAvailable
}
// UpdateOutgoingFiles updates the tracked set of outgoing file transfers and
// sends an ipn.Notify with the full merged list. The updates map is keyed by
// OutgoingFile.ID; existing entries not present in updates are preserved.
func (e *Extension) UpdateOutgoingFiles(updates map[string]*ipn.OutgoingFile) {
e.updateOutgoingFiles(updates)
}
// updateOutgoingFiles updates b.outgoingFiles to reflect the given updates and
// sends an ipn.Notify with the full list of outgoingFiles.
func (e *Extension) updateOutgoingFiles(updates map[string]*ipn.OutgoingFile) {

@ -134,8 +134,9 @@ func (m *manager) PutFile(id clientID, baseName string, r io.Reader, offset, len
}
}
// Copy the contents of the file to the writer.
copyLength, err := io.Copy(wc, r)
// Copy via inFile (which wraps wc) so [incomingFile.Write] can track
// progress and fire periodic sendFileNotify callbacks.
copyLength, err := io.Copy(inFile, r)
if err != nil {
return 0, m.redactAndLogError("Copy", err)
}

Loading…
Cancel
Save