185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package taildrop
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/tstime"
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
type incomingFile struct {
|
|
clock tstime.Clock
|
|
|
|
name string // "foo.jpg"
|
|
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
|
|
|
|
mu sync.Mutex
|
|
copied int64
|
|
done bool
|
|
lastNotify time.Time
|
|
}
|
|
|
|
func (f *incomingFile) markAndNotifyDone() {
|
|
f.mu.Lock()
|
|
f.done = true
|
|
f.mu.Unlock()
|
|
f.sendFileNotify()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// HandlePut receives a file.
|
|
// It handles an HTTP PUT request to the "/v0/put/{filename}" endpoint,
|
|
// where {filename} is a base filename.
|
|
// It returns the number of bytes received and whether it was received successfully.
|
|
func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize int64, success bool) {
|
|
if !envknob.CanTaildrop() {
|
|
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
|
|
return finalSize, success
|
|
}
|
|
if r.Method != "PUT" {
|
|
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
|
return finalSize, success
|
|
}
|
|
if h == nil || h.Dir == "" {
|
|
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
if distro.Get() == distro.Unraid && !h.DirectFileMode {
|
|
http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
rawPath := r.URL.EscapedPath()
|
|
suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
|
|
if !ok {
|
|
http.Error(w, "misconfigured internals", http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
if suffix == "" {
|
|
http.Error(w, "empty filename", http.StatusBadRequest)
|
|
return finalSize, success
|
|
}
|
|
if strings.Contains(suffix, "/") {
|
|
http.Error(w, "directories not supported", http.StatusBadRequest)
|
|
return finalSize, success
|
|
}
|
|
baseName, err := url.PathUnescape(suffix)
|
|
if err != nil {
|
|
http.Error(w, "bad path encoding", http.StatusBadRequest)
|
|
return finalSize, success
|
|
}
|
|
dstFile, ok := h.diskPath(baseName)
|
|
if !ok {
|
|
http.Error(w, "bad filename", http.StatusBadRequest)
|
|
return finalSize, success
|
|
}
|
|
// TODO(bradfitz): prevent same filename being sent by two peers at once
|
|
|
|
// prevent same filename being sent twice
|
|
if _, err := os.Stat(dstFile); err == nil {
|
|
http.Error(w, "file exists", http.StatusConflict)
|
|
return finalSize, success
|
|
}
|
|
|
|
partialFile := dstFile + partialSuffix
|
|
f, err := os.Create(partialFile)
|
|
if err != nil {
|
|
h.Logf("put Create error: %v", redactErr(err))
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
defer func() {
|
|
if !success {
|
|
os.Remove(partialFile)
|
|
}
|
|
}()
|
|
var inFile *incomingFile
|
|
sendFileNotify := h.SendFileNotify
|
|
if sendFileNotify == nil {
|
|
sendFileNotify = func() {} // avoid nil panics below
|
|
}
|
|
if r.ContentLength != 0 {
|
|
inFile = &incomingFile{
|
|
clock: h.Clock,
|
|
name: baseName,
|
|
started: h.Clock.Now(),
|
|
size: r.ContentLength,
|
|
w: f,
|
|
sendFileNotify: sendFileNotify,
|
|
}
|
|
if h.DirectFileMode {
|
|
inFile.partialPath = partialFile
|
|
}
|
|
h.incomingFiles.Store(inFile, struct{}{})
|
|
defer h.incomingFiles.Delete(inFile)
|
|
n, err := io.Copy(inFile, r.Body)
|
|
if err != nil {
|
|
err = redactErr(err)
|
|
f.Close()
|
|
h.Logf("put Copy error: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
finalSize = n
|
|
}
|
|
if err := redactErr(f.Close()); err != nil {
|
|
h.Logf("put Close error: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
if h.DirectFileMode && h.AvoidFinalRename {
|
|
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
|
inFile.markAndNotifyDone()
|
|
}
|
|
} else {
|
|
if err := os.Rename(partialFile, dstFile); err != nil {
|
|
err = redactErr(err)
|
|
h.Logf("put final rename: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return finalSize, success
|
|
}
|
|
}
|
|
|
|
// TODO: set modtime
|
|
// TODO: some real response
|
|
success = true
|
|
io.WriteString(w, "{}\n")
|
|
h.knownEmpty.Store(false)
|
|
sendFileNotify()
|
|
return finalSize, success
|
|
}
|