diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index ca5a62e62..26ddd46df 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -142,7 +142,11 @@ type LocalBackend struct { // same as the Network Extension lifetime and we can thus avoid // double-copying files by writing them to the right location // immediately. - directFileRoot string + // It's also used on Synology, but in that case DoFinalRename is + // also set true, which moves the *.partial file to its final + // name on completion. + directFileRoot string + directFileDoFinalRename bool // false on macOS, true on Synology // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). @@ -234,6 +238,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) { b.directFileRoot = dir } +// SetDirectFileDoFinalRename sets whether the peerapi file server should rename +// a received "name.partial" file to "name" when the download is complete. +// +// This only applies when SetDirectFileRoot is non-empty. +// The default is false. +func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) { + b.mu.Lock() + defer b.mu.Unlock() + b.directFileDoFinalRename = v +} + // b.mu must be held. func (b *LocalBackend) maybePauseControlClientLocked() { if b.cc == nil { @@ -2199,10 +2214,11 @@ func (b *LocalBackend) initPeerAPIListener() { } ps := &peerAPIServer{ - b: b, - rootDir: fileRoot, - selfNode: selfNode, - directFileMode: b.directFileRoot != "", + b: b, + rootDir: fileRoot, + selfNode: selfNode, + directFileMode: b.directFileRoot != "", + directFileDoFinalRename: b.directFileDoFinalRename, } if re, ok := b.e.(wgengine.ResolvingEngine); ok { if r, ok := re.GetResolver(); ok { diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index cf9df33f7..1746a4d7e 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -56,10 +56,17 @@ type peerAPIServer struct { // 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. + // somewhere itself. This is used on the GUI macOS versions + // and on Synology. // In directFileMode, the peerapi doesn't do the final rename - // from "foo.jpg.partial" to "foo.jpg". + // from "foo.jpg.partial" to "foo.jpg" unless + // directFileDoFinalRename is set. directFileMode bool + + // directFileDoFinalRename is whether in directFileMode we + // additionally move the *.direct file to its final name after + // it's received. + directFileDoFinalRename bool } const ( @@ -697,7 +704,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - if h.ps.directFileMode { + if h.ps.directFileMode && !h.ps.directFileDoFinalRename { if inFile != nil { // non-zero length; TODO: notify even for zero length inFile.markAndNotifyDone() } diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index ae4088dc7..60bf7ee87 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -759,6 +759,18 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi b.SetDecompressor(func() (controlclient.Decompressor, error) { return smallzstd.NewDecoder(nil) }) + if distro.Get() == distro.Synology { + // See if they have a "Taildrop" share. + // See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319 + path, err := findSynologyTaildropDir() + if err != nil { + logf("Synology Taildrop support: %v", err) + } else { + logf("Synology Taildrop: using %v", path) + b.SetDirectFileRoot(path) + b.SetDirectFileDoFinalRename(true) + } + } if opts.AutostartStateKey == "" { autoStartKey, err := store.ReadState(ipn.ServerModeStartKey) @@ -1114,3 +1126,17 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) { } return ln.Listener.Accept() } + +// findSynologyTaildropDir looks for the first volume containing a +// "Taildrop" directory. We'd run "synoshare --get Taildrop" command +// but on DSM7 at least, we lack permissions to run that. +func findSynologyTaildropDir() (dir string, err error) { + const name = "Taildrop" + for i := 1; i <= 16; i++ { + dir = fmt.Sprintf("/volume%v/%s", i, name) + if fi, err := os.Stat(dir); err == nil && fi.IsDir() { + return dir, nil + } + } + return "", fmt.Errorf("shared folder %q not found", name) +}