Files
netbird/client/internal/filedrop/delivery.go
Zoltán Papp 73cffdb702 Add peer-to-peer file drop
Files move directly between peers over the overlay, with no server in the
path. The receiver listens on the WireGuard address only, so the port is
unreachable from outside the tunnel, and every offer is matched to a known
peer before anything is read.

Consent is the default: an offer carries metadata alone, and no payload
moves until the receiver accepts. Policy is per profile and device-local —
off, ask, or auto-accept, with per-sender exceptions on top.

Policy and history live in the profile's preferences, so removing a profile
takes its file drop state with it. Transfers interrupted by a restart are
settled on load; nothing survives to finish them, and left alone they would
sit in the log as permanently pending.

The Android bindings pull payload bytes through a chunk-returning stream:
gomobile copies a []byte argument into a fresh Java array and never copies
it back, so a fill-my-buffer method would hand back the right length with
no data.
2026-08-16 22:04:38 +02:00

101 lines
2.4 KiB
Go

package filedrop
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
)
func deliver(spool *Spool, offer Offer, destDir string) ([]string, error) {
if destDir == "" {
return nil, fmt.Errorf("no destination directory configured")
}
if err := os.MkdirAll(destDir, 0o755); err != nil {
return nil, fmt.Errorf("create destination dir: %w", err)
}
var delivered []string
for i, f := range offer.Files {
if f.Kind == KindText {
continue
}
dest, err := moveToUniqueName(spool.Path(offer.ID, i), destDir, sanitizeFileName(f.Name, i))
if err != nil {
return delivered, fmt.Errorf("deliver %s: %w", f.Name, err)
}
if err := chownToDirOwner(dest, destDir); err != nil {
log.Debugf("failed to adopt owner for %s: %v", dest, err)
}
delivered = append(delivered, dest)
}
spool.Remove(offer.ID)
return delivered, nil
}
func sanitizeFileName(name string, index int) string {
name = filepath.Base(filepath.Clean(strings.ReplaceAll(name, "\\", "/")))
if name == "" || name == "." || name == ".." || name == string(filepath.Separator) {
return fmt.Sprintf("file-%d", index)
}
return name
}
func moveToUniqueName(src, dir, name string) (string, error) {
ext := filepath.Ext(name)
stem := strings.TrimSuffix(name, ext)
for attempt := 0; attempt < 1000; attempt++ {
candidate := name
if attempt > 0 {
candidate = fmt.Sprintf("%s (%d)%s", stem, attempt, ext)
}
dest := filepath.Join(dir, candidate)
f, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
if err != nil {
if os.IsExist(err) {
continue
}
return "", fmt.Errorf("create destination: %w", err)
}
if err := moveInto(f, src); err != nil {
_ = f.Close()
_ = os.Remove(dest)
return "", err
}
if err := f.Close(); err != nil {
return "", fmt.Errorf("close destination: %w", err)
}
if err := os.Remove(src); err != nil {
log.Debugf("failed to remove spooled source %s: %v", src, err)
}
return dest, nil
}
return "", fmt.Errorf("no free name for %s in %s", name, dir)
}
func moveInto(dst *os.File, src string) error {
s, err := os.Open(src)
if err != nil {
return fmt.Errorf("open spooled file: %w", err)
}
defer func() {
if err := s.Close(); err != nil {
log.Debugf("close spooled file: %v", err)
}
}()
if _, err := io.Copy(dst, s); err != nil {
return fmt.Errorf("copy payload: %w", err)
}
return nil
}