ipn/ipnlocal: add LocalBackend.SetDirectFileRoot

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/1693/head
Brad Fitzpatrick 3 years ago committed by Brad Fitzpatrick
parent 64c80129f1
commit f5742b0647

@ -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

@ -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

@ -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)

Loading…
Cancel
Save