feat(tsconnect): add outgoing file transfer progress notifications
- Export UpdateOutgoingFiles on taildrop.Extension so it can be called from outside the package (wasm bridge, package main). - Wrap sendFile's PUT body with progresstracking.NewReader so bytes-sent is sampled roughly once per second during transfer. - Create an OutgoingFile entry (with UUID, peer ID, name, declared size) before the PUT and call UpdateOutgoingFiles on each progress tick and on completion (setting Finished/Succeeded). This flows into the IPN notify stream as OutgoingFiles notifications. - Add jsOutgoingFile struct and wire n.OutgoingFiles into a new notifyOutgoingFiles callback in run(), mirroring notifyIncomingFiles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
})
|
||||
|
||||
@@ -413,6 +413,26 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
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() {
|
||||
@@ -949,6 +969,19 @@ type jsIncomingFile struct {
|
||||
Received int64 `json:"received"` // bytes received so far
|
||||
}
|
||||
|
||||
// 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"`
|
||||
|
||||
Reference in New Issue
Block a user