ipn/localapi: add support for multipart POST to file-put

This allows sending multiple files via Taildrop in one request.
Progress is tracked via ipn.Notify.

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
jonathan/taildrop_path
Percy Wegmann 8 months ago committed by Percy Wegmann
parent bed818a978
commit 66e4d843c1

@ -102,7 +102,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/google/nftables/expr from github.com/google/nftables+ L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+ L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+ L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/clientupdate github.com/google/uuid from tailscale.com/clientupdate+
github.com/gorilla/csrf from tailscale.com/client/web github.com/gorilla/csrf from tailscale.com/client/web
github.com/gorilla/securecookie from github.com/gorilla/csrf github.com/gorilla/securecookie from github.com/gorilla/csrf
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+ github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
@ -376,6 +376,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+ tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
tailscale.com/util/osuser from tailscale.com/ipn/localapi+ tailscale.com/util/osuser from tailscale.com/ipn/localapi+
tailscale.com/util/progresstracking from tailscale.com/ipn/localapi
tailscale.com/util/race from tailscale.com/net/dns/resolver tailscale.com/util/race from tailscale.com/net/dns/resolver
tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+ tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
@ -522,7 +523,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
math/rand from github.com/mdlayher/netlink+ math/rand from github.com/mdlayher/netlink+
math/rand/v2 from tailscale.com/util/rands math/rand/v2 from tailscale.com/util/rands
mime from github.com/tailscale/xnet/webdav+ mime from github.com/tailscale/xnet/webdav+
mime/multipart from net/http mime/multipart from net/http+
mime/quotedprintable from mime/multipart mime/quotedprintable from mime/multipart
net from crypto/tls+ net from crypto/tls+
net/http from expvar+ net/http from expvar+

@ -202,8 +202,8 @@ type PartialFile struct {
// OutgoingFile represents an in-progress outgoing file transfer. // OutgoingFile represents an in-progress outgoing file transfer.
type OutgoingFile struct { type OutgoingFile struct {
ID string `json:"-"` // unique identifier for this transfer (a type 4 UUID) ID string `json:",omitempty"` // unique identifier for this transfer (a type 4 UUID)
PeerID tailcfg.StableNodeID // identifier for the peer to which this is being transferred PeerID tailcfg.StableNodeID `json:",omitempty"` // identifier for the peer to which this is being transferred
Name string `json:",omitempty"` // e.g. "foo.jpg" Name string `json:",omitempty"` // e.g. "foo.jpg"
Started time.Time // time transfer started Started time.Time // time transfer started
DeclaredSize int64 // or -1 if unknown DeclaredSize int64 // or -1 if unknown

@ -320,7 +320,7 @@ type LocalBackend struct {
// notified about. // notified about.
lastNotifiedTailFSShares atomic.Pointer[views.SliceView[*tailfs.Share, tailfs.ShareView]] lastNotifiedTailFSShares atomic.Pointer[views.SliceView[*tailfs.Share, tailfs.ShareView]]
// outgoingFiles keeps track of Taildrop outgoing files // outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
outgoingFiles map[string]*ipn.OutgoingFile outgoingFiles map[string]*ipn.OutgoingFile
} }

@ -4,20 +4,21 @@
package ipnlocal package ipnlocal
import ( import (
"maps"
"slices" "slices"
"strings" "strings"
"tailscale.com/ipn" "tailscale.com/ipn"
) )
func (b *LocalBackend) UpdateOutgoingFiles(updates map[string]ipn.OutgoingFile) { // UpdateOutgoingFiles updates b.outgoingFiles to reflect the given updates and
// sends an ipn.Notify with the full list of outgoingFiles.
func (b *LocalBackend) UpdateOutgoingFiles(updates map[string]*ipn.OutgoingFile) {
b.mu.Lock() b.mu.Lock()
if b.outgoingFiles == nil { if b.outgoingFiles == nil {
b.outgoingFiles = make(map[string]*ipn.OutgoingFile, len(updates)) b.outgoingFiles = make(map[string]*ipn.OutgoingFile, len(updates))
} }
for id, file := range updates { maps.Copy(b.outgoingFiles, updates)
b.outgoingFiles[id] = &file
}
outgoingFiles := make([]*ipn.OutgoingFile, 0, len(b.outgoingFiles)) outgoingFiles := make([]*ipn.OutgoingFile, 0, len(b.outgoingFiles))
for _, file := range b.outgoingFiles { for _, file := range b.outgoingFiles {
outgoingFiles = append(outgoingFiles, file) outgoingFiles = append(outgoingFiles, file)

@ -1535,11 +1535,10 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
// directly, as the Windows GUI always runs in tun mode anyway. // directly, as the Windows GUI always runs in tun mode anyway.
// //
// In addition to single file PUTs, this endpoint accepts multipart file // In addition to single file PUTs, this endpoint accepts multipart file
// POSTS encoded as multipart/form-data. Each part must include a // POSTS encoded as multipart/form-data.The first part should be an
// "Content-Length" in the MIME header indicating the size of the file. // application/json file that contains a manifest consisting of a JSON array of
// The first part should be an application/json file that contains a JSON map // OutgoingFiles which wecan use for tracking progress even before reading the
// of filename -> length, which we can use for tracking progress even before // file parts.
// reading the file parts.
// //
// URL format: // URL format:
// //
@ -1599,9 +1598,9 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
return return
} }
// Report progress on outgoing files every 5 seconds // Periodically report progress of outgoing files.
outgoingFiles := make(map[string]ipn.OutgoingFile) outgoingFiles := make(map[string]*ipn.OutgoingFile)
t := time.NewTicker(5 * time.Second) t := time.NewTicker(1 * time.Second)
progressUpdates := make(chan ipn.OutgoingFile) progressUpdates := make(chan ipn.OutgoingFile)
defer close(progressUpdates) defer close(progressUpdates)
@ -1614,7 +1613,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
outgoingFiles[u.ID] = u outgoingFiles[u.ID] = &u
case <-t.C: case <-t.C:
h.b.UpdateOutgoingFiles(outgoingFiles) h.b.UpdateOutgoingFiles(outgoingFiles)
} }
@ -1672,21 +1671,15 @@ func (h *Handler) multiFilePost(progressUpdates chan (ipn.OutgoingFile), w http.
return return
} }
var manifest map[string]int64 var manifest []ipn.OutgoingFile
err := json.NewDecoder(part).Decode(&manifest) err := json.NewDecoder(part).Decode(&manifest)
if err != nil { if err != nil {
http.Error(ww, fmt.Sprintf("invalid manifest: %s", err), http.StatusBadRequest) http.Error(ww, fmt.Sprintf("invalid manifest: %s", err), http.StatusBadRequest)
return return
} }
for filename, size := range manifest { for _, file := range manifest {
file := ipn.OutgoingFile{ outgoingFilesByName[file.Name] = file
ID: uuid.Must(uuid.NewRandom()).String(),
Name: filename,
PeerID: peerID,
DeclaredSize: size,
}
outgoingFilesByName[filename] = file
progressUpdates <- file progressUpdates <- file
} }

Loading…
Cancel
Save