2023-10-05 18:05:45 -05:00
|
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package taildrop
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"os"
|
|
|
|
|
"runtime"
|
|
|
|
|
"sort"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"tailscale.com/client/tailscale/apitype"
|
|
|
|
|
"tailscale.com/logtail/backoff"
|
2025-08-01 15:10:00 -07:00
|
|
|
|
"tailscale.com/util/set"
|
2023-10-05 18:05:45 -05:00
|
|
|
|
)
|
|
|
|
|
|
2023-10-06 15:41:14 -07:00
|
|
|
|
// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
|
|
|
|
|
// This always returns false when [Handler.DirectFileMode] is false.
|
2025-08-01 15:10:00 -07:00
|
|
|
|
func (m *manager) HasFilesWaiting() bool {
|
|
|
|
|
if m == nil || m.opts.fileOps == nil || m.opts.DirectFileMode {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
return false
|
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
|
|
|
|
|
// 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.
|
2025-08-01 15:10:00 -07:00
|
|
|
|
total := m.totalReceived.Load()
|
|
|
|
|
if total == m.emptySince.Load() {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
return false
|
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
|
2025-08-01 15:10:00 -07:00
|
|
|
|
files, err := m.opts.fileOps.ListFiles()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build a set of filenames present in Dir
|
|
|
|
|
fileSet := set.Of(files...)
|
|
|
|
|
|
|
|
|
|
for _, filename := range files {
|
|
|
|
|
if isPartialOrDeleted(filename) {
|
|
|
|
|
continue
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
if fileSet.Contains(filename + deletedSuffix) {
|
|
|
|
|
continue // already handled
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
// Found at least one downloadable file
|
2023-10-17 13:46:05 -07:00
|
|
|
|
return true
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
|
|
|
|
|
// No waiting files → update negative‑result cache
|
|
|
|
|
m.emptySince.Store(total)
|
|
|
|
|
return false
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WaitingFiles returns the list of files that have been sent by a
|
2023-10-06 15:41:14 -07:00
|
|
|
|
// peer that are waiting in [Handler.Dir].
|
|
|
|
|
// This always returns nil when [Handler.DirectFileMode] is false.
|
2025-08-01 15:10:00 -07:00
|
|
|
|
func (m *manager) WaitingFiles() ([]apitype.WaitingFile, error) {
|
|
|
|
|
if m == nil || m.opts.fileOps == nil {
|
2023-10-12 09:28:46 -07:00
|
|
|
|
return nil, ErrNoTaildrop
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
if m.opts.DirectFileMode {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
names, err := m.opts.fileOps.ListFiles()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, redactError(err)
|
|
|
|
|
}
|
|
|
|
|
var ret []apitype.WaitingFile
|
|
|
|
|
for _, name := range names {
|
|
|
|
|
if isPartialOrDeleted(name) {
|
|
|
|
|
continue
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
// A corresponding .deleted marker means the file was already handled.
|
|
|
|
|
if _, err := m.opts.fileOps.Stat(name + deletedSuffix); err == nil {
|
|
|
|
|
continue
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
fi, err := m.opts.fileOps.Stat(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ret = append(ret, apitype.WaitingFile{
|
|
|
|
|
Name: name,
|
|
|
|
|
Size: fi.Size(),
|
|
|
|
|
})
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
|
|
|
|
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
|
|
|
|
return ret, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-06 15:41:14 -07:00
|
|
|
|
// DeleteFile deletes a file of the given baseName from [Handler.Dir].
|
|
|
|
|
// This method is only allowed when [Handler.DirectFileMode] is false.
|
2025-05-06 20:45:28 -07:00
|
|
|
|
func (m *manager) DeleteFile(baseName string) error {
|
2025-08-01 15:10:00 -07:00
|
|
|
|
if m == nil || m.opts.fileOps == nil {
|
2023-10-12 09:28:46 -07:00
|
|
|
|
return ErrNoTaildrop
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
if m.opts.DirectFileMode {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
return errors.New("deletes not allowed in direct mode")
|
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
|
2023-10-05 18:05:45 -05:00
|
|
|
|
var bo *backoff.Backoff
|
2023-10-17 13:46:05 -07:00
|
|
|
|
logf := m.opts.Logf
|
|
|
|
|
t0 := m.opts.Clock.Now()
|
2023-10-05 18:05:45 -05:00
|
|
|
|
for {
|
2025-08-01 15:10:00 -07:00
|
|
|
|
err := m.opts.fileOps.Remove(baseName)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
2023-10-17 13:46:05 -07:00
|
|
|
|
err = redactError(err)
|
2023-10-19 13:26:55 -07:00
|
|
|
|
// 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.
|
2023-10-05 18:05:45 -05:00
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
if bo == nil {
|
|
|
|
|
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
|
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
if m.opts.Clock.Since(t0) < 5*time.Second {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
bo.BackOff(context.Background(), err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
if err := m.touchFile(baseName + deletedSuffix); err != nil {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
logf("peerapi: failed to leave deleted marker: %v", err)
|
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
m.deleter.Insert(baseName + deletedSuffix)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
|
|
|
|
logf("peerapi: failed to DeleteFile: %v", err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-01 15:10:00 -07:00
|
|
|
|
func (m *manager) touchFile(name string) error {
|
|
|
|
|
wc, _, err := m.opts.fileOps.OpenWriter(name /* offset= */, 0, 0666)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
if err != nil {
|
2023-10-17 13:46:05 -07:00
|
|
|
|
return redactError(err)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
return wc.Close()
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-06 15:41:14 -07:00
|
|
|
|
// OpenFile opens a file of the given baseName from [Handler.Dir].
|
|
|
|
|
// This method is only allowed when [Handler.DirectFileMode] is false.
|
2025-05-06 20:45:28 -07:00
|
|
|
|
func (m *manager) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
2025-08-01 15:10:00 -07:00
|
|
|
|
if m == nil || m.opts.fileOps == nil {
|
2023-10-12 09:28:46 -07:00
|
|
|
|
return nil, 0, ErrNoTaildrop
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2023-10-17 13:46:05 -07:00
|
|
|
|
if m.opts.DirectFileMode {
|
2023-10-05 18:05:45 -05:00
|
|
|
|
return nil, 0, errors.New("opens not allowed in direct mode")
|
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
if _, err := m.opts.fileOps.Stat(baseName + deletedSuffix); err == nil {
|
|
|
|
|
return nil, 0, redactError(&fs.PathError{Op: "open", Path: baseName, Err: fs.ErrNotExist})
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
f, err := m.opts.fileOps.OpenReader(baseName)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
if err != nil {
|
2023-10-17 13:46:05 -07:00
|
|
|
|
return nil, 0, redactError(err)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
|
fi, err := m.opts.fileOps.Stat(baseName)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
if err != nil {
|
|
|
|
|
f.Close()
|
2023-10-17 13:46:05 -07:00
|
|
|
|
return nil, 0, redactError(err)
|
2023-10-05 18:05:45 -05:00
|
|
|
|
}
|
|
|
|
|
return f, fi.Size(), nil
|
|
|
|
|
}
|