mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 11:05:45 +00:00
6ada33db77
It is possible that upon a cold-start, we enqueue a partial file for deletion that is resumed shortly after startup. If the file transfer happens to last longer than deleteDelay, we will delete the partial file, which is unfortunate. The client spent a long time uploading a file, only for it to be accidentally deleted. It's a very rare race, but also a frustrating one if it happens to manifest. Fix the code to only delete partial files that do not have an active puts against it. We also fix a minor bug in ResumeReader where we read b[:blockSize] instead of b[:cs.Size]. The former is the fixed size of 64KiB, while the latter is usually 64KiB, but may be less for the last block. Updates tailscale/corp#14772 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package taildrop
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"time"
|
|
|
|
"tailscale.com/client/tailscale/apitype"
|
|
"tailscale.com/logtail/backoff"
|
|
)
|
|
|
|
// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
|
|
// This always returns false when [Handler.DirectFileMode] is false.
|
|
func (m *Manager) HasFilesWaiting() (has bool) {
|
|
if m == nil || m.opts.Dir == "" || m.opts.DirectFileMode {
|
|
return false
|
|
}
|
|
|
|
// Optimization: this is usually empty, so avoid opening
|
|
// the directory and checking. We can't cache the actual
|
|
// has-files-or-not values as the macOS/iOS client might
|
|
// in the future use+delete the files directly. So only
|
|
// keep this negative cache.
|
|
totalReceived := m.totalReceived.Load()
|
|
if totalReceived == m.emptySince.Load() {
|
|
return false
|
|
}
|
|
|
|
// Check whether there is at least one one waiting file.
|
|
err := rangeDir(m.opts.Dir, func(de fs.DirEntry) bool {
|
|
name := de.Name()
|
|
if isPartialOrDeleted(name) || !de.Type().IsRegular() {
|
|
return true
|
|
}
|
|
_, err := os.Stat(filepath.Join(m.opts.Dir, name+deletedSuffix))
|
|
if os.IsNotExist(err) {
|
|
has = true
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
// If there are no more waiting files, record totalReceived as emptySince
|
|
// so that we can short-circuit the expensive directory traversal
|
|
// if no files have been received after the start of this call.
|
|
if err == nil && !has {
|
|
m.emptySince.Store(totalReceived)
|
|
}
|
|
return has
|
|
}
|
|
|
|
// WaitingFiles returns the list of files that have been sent by a
|
|
// peer that are waiting in [Handler.Dir].
|
|
// This always returns nil when [Handler.DirectFileMode] is false.
|
|
func (m *Manager) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
|
if m == nil || m.opts.Dir == "" {
|
|
return nil, ErrNoTaildrop
|
|
}
|
|
if m.opts.DirectFileMode {
|
|
return nil, nil
|
|
}
|
|
if err := rangeDir(m.opts.Dir, func(de fs.DirEntry) bool {
|
|
name := de.Name()
|
|
if isPartialOrDeleted(name) || !de.Type().IsRegular() {
|
|
return true
|
|
}
|
|
_, err := os.Stat(filepath.Join(m.opts.Dir, name+deletedSuffix))
|
|
if os.IsNotExist(err) {
|
|
fi, err := de.Info()
|
|
if err != nil {
|
|
return true
|
|
}
|
|
ret = append(ret, apitype.WaitingFile{
|
|
Name: filepath.Base(name),
|
|
Size: fi.Size(),
|
|
})
|
|
}
|
|
return true
|
|
}); err != nil {
|
|
return nil, redactError(err)
|
|
}
|
|
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
|
return ret, nil
|
|
}
|
|
|
|
// DeleteFile deletes a file of the given baseName from [Handler.Dir].
|
|
// This method is only allowed when [Handler.DirectFileMode] is false.
|
|
func (m *Manager) DeleteFile(baseName string) error {
|
|
if m == nil || m.opts.Dir == "" {
|
|
return ErrNoTaildrop
|
|
}
|
|
if m.opts.DirectFileMode {
|
|
return errors.New("deletes not allowed in direct mode")
|
|
}
|
|
path, err := joinDir(m.opts.Dir, baseName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var bo *backoff.Backoff
|
|
logf := m.opts.Logf
|
|
t0 := m.opts.Clock.Now()
|
|
for {
|
|
err := os.Remove(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
err = redactError(err)
|
|
// Put a retry loop around deletes on Windows.
|
|
//
|
|
// Windows file descriptor closes are effectively asynchronous,
|
|
// as a bunch of hooks run on/after close,
|
|
// and we can't necessarily delete the file for a while after close,
|
|
// as we need to wait for everybody to be done with it.
|
|
// On Windows, unlike Unix, a file can't be deleted if it's open anywhere.
|
|
// So try a few times but ultimately just leave a "foo.jpg.deleted"
|
|
// marker file to note that it's deleted and we clean it up later.
|
|
if runtime.GOOS == "windows" {
|
|
if bo == nil {
|
|
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
|
|
}
|
|
if m.opts.Clock.Since(t0) < 5*time.Second {
|
|
bo.BackOff(context.Background(), err)
|
|
continue
|
|
}
|
|
if err := touchFile(path + deletedSuffix); err != nil {
|
|
logf("peerapi: failed to leave deleted marker: %v", err)
|
|
}
|
|
m.deleter.Insert(baseName + deletedSuffix)
|
|
}
|
|
logf("peerapi: failed to DeleteFile: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func touchFile(path string) error {
|
|
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
|
if err != nil {
|
|
return redactError(err)
|
|
}
|
|
return f.Close()
|
|
}
|
|
|
|
// OpenFile opens a file of the given baseName from [Handler.Dir].
|
|
// This method is only allowed when [Handler.DirectFileMode] is false.
|
|
func (m *Manager) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
|
if m == nil || m.opts.Dir == "" {
|
|
return nil, 0, ErrNoTaildrop
|
|
}
|
|
if m.opts.DirectFileMode {
|
|
return nil, 0, errors.New("opens not allowed in direct mode")
|
|
}
|
|
path, err := joinDir(m.opts.Dir, baseName)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if _, err := os.Stat(path + deletedSuffix); err == nil {
|
|
return nil, 0, redactError(&fs.PathError{Op: "open", Path: path, Err: fs.ErrNotExist})
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, 0, redactError(err)
|
|
}
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, 0, redactError(err)
|
|
}
|
|
return f, fi.Size(), nil
|
|
}
|