2023-10-06 09:47:03 -05:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
package taildrop
|
|
|
|
|
|
|
|
import (
|
2025-05-20 15:30:19 -07:00
|
|
|
"fmt"
|
2023-10-06 09:47:03 -05:00
|
|
|
"io"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"tailscale.com/envknob"
|
2023-11-13 10:20:28 -08:00
|
|
|
"tailscale.com/ipn"
|
2023-10-06 09:47:03 -05:00
|
|
|
"tailscale.com/tstime"
|
|
|
|
"tailscale.com/version/distro"
|
|
|
|
)
|
|
|
|
|
2023-10-12 09:28:46 -07:00
|
|
|
type incomingFileKey struct {
|
2025-05-06 20:45:28 -07:00
|
|
|
id clientID
|
2023-10-12 09:28:46 -07:00
|
|
|
name string // e.g., "foo.jpeg"
|
|
|
|
}
|
|
|
|
|
2023-10-06 09:47:03 -05:00
|
|
|
type incomingFile struct {
|
2023-10-12 16:50:11 -07:00
|
|
|
clock tstime.DefaultClock
|
2023-10-06 09:47:03 -05:00
|
|
|
|
|
|
|
started time.Time
|
|
|
|
size int64 // or -1 if unknown; never 0
|
|
|
|
w io.Writer // underlying writer
|
|
|
|
sendFileNotify func() // called when done
|
|
|
|
partialPath string // non-empty in direct mode
|
2024-01-09 14:11:34 -06:00
|
|
|
finalPath string // not used in direct mode
|
2023-10-06 09:47:03 -05:00
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
copied int64
|
|
|
|
done bool
|
|
|
|
lastNotify time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *incomingFile) Write(p []byte) (n int, err error) {
|
|
|
|
n, err = f.w.Write(p)
|
|
|
|
|
|
|
|
var needNotify bool
|
|
|
|
defer func() {
|
|
|
|
if needNotify {
|
|
|
|
f.sendFileNotify()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if n > 0 {
|
|
|
|
f.mu.Lock()
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
f.copied += int64(n)
|
|
|
|
now := f.clock.Now()
|
|
|
|
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
|
|
|
|
f.lastNotify = now
|
|
|
|
needNotify = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return n, err
|
|
|
|
}
|
|
|
|
|
2025-05-06 20:45:28 -07:00
|
|
|
// PutFile stores a file into [manager.Dir] from a given client id.
|
2023-10-12 09:28:46 -07:00
|
|
|
// The baseName must be a base filename without any slashes.
|
|
|
|
// The length is the expected length of content to read from r,
|
|
|
|
// it may be negative to indicate that it is unknown.
|
2023-10-12 16:50:11 -07:00
|
|
|
// It returns the length of the entire file.
|
2023-10-12 09:28:46 -07:00
|
|
|
//
|
|
|
|
// If there is a failure reading from r, then the partial file is not deleted
|
2025-05-06 20:45:28 -07:00
|
|
|
// for some period of time. The [manager.PartialFiles] and [manager.HashPartialFile]
|
2023-10-12 09:28:46 -07:00
|
|
|
// methods may be used to list all partial files and to compute the hash for a
|
|
|
|
// specific partial file. This allows the client to determine whether to resume
|
|
|
|
// a partial file. While resuming, PutFile may be called again with a non-zero
|
|
|
|
// offset to specify where to resume receiving data at.
|
2025-08-01 15:10:00 -07:00
|
|
|
func (m *manager) PutFile(id clientID, baseName string, r io.Reader, offset, length int64) (fileLength int64, err error) {
|
|
|
|
|
2023-10-12 09:28:46 -07:00
|
|
|
switch {
|
2025-08-01 15:10:00 -07:00
|
|
|
case m == nil || m.opts.fileOps == nil:
|
2023-10-12 09:28:46 -07:00
|
|
|
return 0, ErrNoTaildrop
|
|
|
|
case !envknob.CanTaildrop():
|
|
|
|
return 0, ErrNoTaildrop
|
2023-10-17 13:46:05 -07:00
|
|
|
case distro.Get() == distro.Unraid && !m.opts.DirectFileMode:
|
2023-10-12 09:28:46 -07:00
|
|
|
return 0, ErrNotAccessible
|
|
|
|
}
|
|
|
|
|
2025-08-01 15:10:00 -07:00
|
|
|
if err := validateBaseName(baseName); err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// and make sure we don't delete it while uploading:
|
|
|
|
m.deleter.Remove(baseName)
|
|
|
|
|
|
|
|
// Create (if not already) the partial file with read-write permissions.
|
|
|
|
partialName := baseName + id.partialSuffix()
|
|
|
|
wc, partialPath, err := m.opts.fileOps.OpenWriter(partialName, offset, 0o666)
|
|
|
|
if err != nil {
|
|
|
|
return 0, m.redactAndLogError("Create", err)
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
wc.Close()
|
2025-05-20 15:30:19 -07:00
|
|
|
if err != nil {
|
2025-08-01 15:10:00 -07:00
|
|
|
m.deleter.Insert(partialName) // mark partial file for eventual deletion
|
2025-05-20 15:30:19 -07:00
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
}()
|
2023-10-12 09:28:46 -07:00
|
|
|
|
|
|
|
// Check whether there is an in-progress transfer for the file.
|
2025-08-01 15:10:00 -07:00
|
|
|
inFileKey := incomingFileKey{id, baseName}
|
|
|
|
inFile, loaded := m.incomingFiles.LoadOrInit(inFileKey, func() *incomingFile {
|
|
|
|
inFile := &incomingFile{
|
2023-10-17 13:46:05 -07:00
|
|
|
clock: m.opts.Clock,
|
|
|
|
started: m.opts.Clock.Now(),
|
2023-10-12 09:28:46 -07:00
|
|
|
size: length,
|
2023-10-17 13:46:05 -07:00
|
|
|
sendFileNotify: m.opts.SendFileNotify,
|
2023-10-12 09:28:46 -07:00
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
if m.opts.DirectFileMode {
|
|
|
|
inFile.partialPath = partialPath
|
|
|
|
}
|
|
|
|
return inFile
|
2023-10-12 09:28:46 -07:00
|
|
|
})
|
2025-08-01 15:10:00 -07:00
|
|
|
|
|
|
|
inFile.w = wc
|
|
|
|
|
2023-10-12 09:28:46 -07:00
|
|
|
if loaded {
|
|
|
|
return 0, ErrFileExists
|
2023-10-06 09:47:03 -05:00
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
defer m.incomingFiles.Delete(inFileKey)
|
2023-10-12 09:28:46 -07:00
|
|
|
|
2023-11-13 10:20:28 -08:00
|
|
|
// Record that we have started to receive at least one file.
|
|
|
|
// This is used by the deleter upon a cold-start to scan the directory
|
|
|
|
// for any files that need to be deleted.
|
2025-05-20 15:30:19 -07:00
|
|
|
if st := m.opts.State; st != nil {
|
|
|
|
if b, _ := st.ReadState(ipn.TaildropReceivedKey); len(b) == 0 {
|
|
|
|
if werr := st.WriteState(ipn.TaildropReceivedKey, []byte{1}); werr != nil {
|
|
|
|
m.opts.Logf("WriteState error: %v", werr) // non-fatal error
|
2023-11-13 10:20:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-20 15:30:19 -07:00
|
|
|
// Copy the contents of the file to the writer.
|
|
|
|
copyLength, err := io.Copy(wc, r)
|
2023-10-12 09:28:46 -07:00
|
|
|
if err != nil {
|
2025-05-20 15:30:19 -07:00
|
|
|
return 0, m.redactAndLogError("Copy", err)
|
2023-10-06 09:47:03 -05:00
|
|
|
}
|
2023-10-12 09:28:46 -07:00
|
|
|
if length >= 0 && copyLength != length {
|
2025-05-20 15:30:19 -07:00
|
|
|
return 0, m.redactAndLogError("Copy", fmt.Errorf("copied %d bytes; expected %d", copyLength, length))
|
2023-10-12 09:28:46 -07:00
|
|
|
}
|
2025-05-20 15:30:19 -07:00
|
|
|
if err := wc.Close(); err != nil {
|
|
|
|
return 0, m.redactAndLogError("Close", err)
|
2023-10-12 09:28:46 -07:00
|
|
|
}
|
2025-05-20 15:30:19 -07:00
|
|
|
|
2025-08-01 15:10:00 -07:00
|
|
|
fileLength = offset + copyLength
|
2023-10-12 09:28:46 -07:00
|
|
|
|
2024-01-09 14:11:34 -06:00
|
|
|
inFile.mu.Lock()
|
|
|
|
inFile.done = true
|
|
|
|
inFile.mu.Unlock()
|
2023-10-12 09:28:46 -07:00
|
|
|
|
2025-08-01 15:10:00 -07:00
|
|
|
// 6) Finalize (rename/move) the partial into place via FileOps.Rename
|
|
|
|
finalPath, err := m.opts.fileOps.Rename(partialPath, baseName)
|
|
|
|
if err != nil {
|
|
|
|
return 0, m.redactAndLogError("Rename", err)
|
2025-05-20 15:30:19 -07:00
|
|
|
}
|
2025-08-01 15:10:00 -07:00
|
|
|
inFile.finalPath = finalPath
|
2025-05-20 15:30:19 -07:00
|
|
|
|
|
|
|
m.totalReceived.Add(1)
|
|
|
|
m.opts.SendFileNotify()
|
|
|
|
return fileLength, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *manager) redactAndLogError(stage string, err error) error {
|
|
|
|
err = redactError(err)
|
|
|
|
m.opts.Logf("put %s error: %v", stage, err)
|
|
|
|
return err
|
2023-10-12 09:28:46 -07:00
|
|
|
}
|