From f5742b0647d61f9a33e5c3be4f9852f8910d24db Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 12 Apr 2021 14:05:44 -0700 Subject: [PATCH] ipn/ipnlocal: add LocalBackend.SetDirectFileRoot Signed-off-by: Brad Fitzpatrick --- ipn/backend.go | 7 +++++ ipn/ipnlocal/local.go | 63 +++++++++++++++++++++++++++++++---------- ipn/ipnlocal/peerapi.go | 35 +++++++++++++++++++++-- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/ipn/backend.go b/ipn/backend.go index 8b07d5953..8216d30b3 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -94,6 +94,13 @@ type PartialFile struct { Started time.Time // time transfer started DeclaredSize int64 // or -1 if unknown Received int64 // bytes copied thus far + + // FinalPath is non-empty when the final has been completely + // written and renamed into place. This is then the complete + // path to the file post-rename. This is only set in + // "direct" file mode where the peerapi isn't being used; see + // LocalBackend.SetDirectFileRoot. + FinalPath string `json:",omitempty"` } // StateKey is an opaque identifier for a set of LocalBackend state diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 568f5db08..0eb87442c 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -118,6 +118,16 @@ type LocalBackend struct { peerAPIServer *peerAPIServer // or nil peerAPIListeners []*peerAPIListener incomingFiles map[*incomingFile]bool + // directFileRoot, if non-empty, means to write received files + // directly to this directory, without staging them in an + // intermediate buffered directory for "pick-up" later. If + // empty, the files are received in a daemon-owned location + // and the localapi is used to enumerate, download, and delete + // them. This is used on macOS where the GUI lifetime is the + // same as the Network Extension lifetime and we can thus avoid + // double-copying files by writing them to the right location + // immediately. + directFileRoot string // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). @@ -179,6 +189,17 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge return b, nil } +// SetDirectFileRoot sets the directory to download files to directly, +// without buffering them through an intermediate daemon-owned +// tailcfg.UserID-specific directory. +// +// This must be called before the LocalBackend starts being used. +func (b *LocalBackend) SetDirectFileRoot(dir string) { + b.mu.Lock() + defer b.mu.Unlock() + b.directFileRoot = dir +} + // linkChange is our link monitor callback, called whenever the network changes. // major is whether ifst is different than earlier. func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) { @@ -1611,6 +1632,26 @@ func tailscaleVarRoot() string { return filepath.Dir(stateFile) } +func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string { + if v := b.directFileRoot; v != "" { + return v + } + varRoot := tailscaleVarRoot() + if varRoot == "" { + b.logf("peerapi disabled; no state directory") + return "" + } + baseDir := fmt.Sprintf("%s-uid-%d", + strings.ReplaceAll(b.activeLogin, "@", "-"), + uid) + dir := filepath.Join(varRoot, "files", baseDir) + if err := os.MkdirAll(dir, 0700); err != nil { + b.logf("peerapi disabled; error making directory: %v", err) + return "" + } + return dir +} + func (b *LocalBackend) initPeerAPIListener() { b.mu.Lock() defer b.mu.Unlock() @@ -1640,17 +1681,8 @@ func (b *LocalBackend) initPeerAPIListener() { return } - varRoot := tailscaleVarRoot() - if varRoot == "" { - b.logf("peerapi disabled; no state directory") - return - } - baseDir := fmt.Sprintf("%s-uid-%d", - strings.ReplaceAll(b.activeLogin, "@", "-"), - selfNode.User) - dir := filepath.Join(varRoot, "files", baseDir) - if err := os.MkdirAll(dir, 0700); err != nil { - b.logf("peerapi disabled; error making directory: %v", err) + fileRoot := b.fileRootLocked(selfNode.User) + if fileRoot == "" { return } @@ -1662,10 +1694,11 @@ func (b *LocalBackend) initPeerAPIListener() { } ps := &peerAPIServer{ - b: b, - rootDir: dir, - tunName: tunName, - selfNode: selfNode, + b: b, + rootDir: fileRoot, + tunName: tunName, + selfNode: selfNode, + directFileMode: b.directFileRoot != "", } b.peerAPIServer = ps diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 0d7319968..0f84cdb06 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -39,9 +39,15 @@ type peerAPIServer struct { tunName string selfNode *tailcfg.Node knownEmpty syncs.AtomicBool + + // directFileMode is whether we're writing files directly to a + // download directory (as *.partial files), rather than making + // the frontend retrieve it over localapi HTTP and write it + // somewhere itself. This is used on GUI macOS version. + directFileMode bool } -const partialSuffix = ".tspartial" +const partialSuffix = ".partial" func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) { clean := path.Clean(baseName) @@ -350,6 +356,15 @@ type incomingFile struct { mu sync.Mutex copied int64 lastNotify time.Time + finalPath string // non-empty in direct mode, when file is done +} + +func (f *incomingFile) markAndNotifyDone(finalPath string) { + f.mu.Lock() + f.finalPath = finalPath + f.mu.Unlock() + b := f.ph.ps.b + b.sendFileNotify() } func (f *incomingFile) Write(p []byte) (n int, err error) { @@ -383,6 +398,7 @@ func (f *incomingFile) PartialFile() ipn.PartialFile { Started: f.started, DeclaredSize: f.size, Received: f.copied, + FinalPath: f.finalPath, } } @@ -405,6 +421,9 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad filename", 400) return } + if h.ps.directFileMode { + dstFile += partialSuffix + } f, err := os.Create(dstFile) if err != nil { h.logf("put Create error: %v", err) @@ -418,8 +437,9 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) { } }() var finalSize int64 + var inFile *incomingFile if r.ContentLength != 0 { - inFile := &incomingFile{ + inFile = &incomingFile{ name: baseName, started: time.Now(), size: r.ContentLength, @@ -442,6 +462,17 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + if h.ps.directFileMode { + finalPath := strings.TrimSuffix(dstFile, partialSuffix) + if err := os.Rename(dstFile, finalPath); err != nil { + h.logf("Rename error: %v", err) + http.Error(w, "error renaming file", http.StatusInternalServerError) + return + } + if inFile != nil { // non-zero length; TODO: notify even for zero length + inFile.markAndNotifyDone(finalPath) + } + } h.logf("put of %s from %v/%v", baseName, approxSize(finalSize), h.remoteAddr.IP, h.peerNode.ComputedName)