mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-24 18:21:00 +00:00
183 lines
4.6 KiB
Go
183 lines
4.6 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 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.RootDir == "" {
|
||
|
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", 400)
|
||
|
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.DirectFileDoFinalRename {
|
||
|
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
|
||
|
}
|