mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-13 02:09:08 +02:00
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.
206 lines
5.0 KiB
Go
206 lines
5.0 KiB
Go
package filedrop
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// The transfer directions.
|
|
const (
|
|
DirectionReceived Direction = iota
|
|
DirectionSent
|
|
)
|
|
|
|
// The failure reasons a transfer can end with; None accompanies every other state.
|
|
const (
|
|
ReasonNone FailureReason = iota
|
|
// ReasonUnreachable marks a transport-level failure: nothing listens on the
|
|
// peer's file drop port, so the client is old or receiving is off.
|
|
ReasonUnreachable
|
|
// ReasonInterrupted marks a transfer that was still moving when the process
|
|
// stopped; nothing survived to finish or resume it.
|
|
ReasonInterrupted
|
|
)
|
|
|
|
const historyCap = 30
|
|
|
|
// Direction tells whether a transfer was sent by this device or received on it.
|
|
type Direction uint8
|
|
|
|
// FailureReason classifies why a transfer failed, when it is known.
|
|
type FailureReason uint8
|
|
|
|
// String implements fmt.Stringer.
|
|
func (d Direction) String() string {
|
|
switch d {
|
|
case DirectionReceived:
|
|
return "received"
|
|
case DirectionSent:
|
|
return "sent"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", uint8(d))
|
|
}
|
|
}
|
|
|
|
// Transfer is one history entry: a sent or received offer with its outcome.
|
|
type Transfer struct {
|
|
ID OfferID `json:"id"`
|
|
Direction Direction `json:"direction"`
|
|
PeerKey PeerKey `json:"peerKey"`
|
|
PeerName string `json:"peerName"`
|
|
Files []FileMeta `json:"files"`
|
|
State State `json:"state"`
|
|
Transferred int64 `json:"transferred"`
|
|
TotalSize int64 `json:"totalSize"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
DeliveredPaths []string `json:"deliveredPaths,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Reason FailureReason `json:"reason,omitempty"`
|
|
}
|
|
|
|
// History is the persisted transfer log of one profile, newest first.
|
|
type History struct {
|
|
mu sync.RWMutex
|
|
store Store
|
|
entries []Transfer
|
|
}
|
|
|
|
// LoadHistory reads the stored transfer log, starting empty when unreadable.
|
|
func LoadHistory(store Store) *History {
|
|
h := &History{store: store}
|
|
|
|
var entries []Transfer
|
|
if err := loadSection(store, namespaceHistory, &entries); err != nil {
|
|
log.Warnf("failed to read file drop history, starting empty: %v", err)
|
|
return h
|
|
}
|
|
h.entries = entries
|
|
if h.settleInterrupted() {
|
|
h.persist()
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (t Transfer) terminal() bool {
|
|
switch t.State {
|
|
case StateCompleted, StateDeclined, StateExpired, StateCancelled, StateFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (t Transfer) clone() Transfer {
|
|
c := t
|
|
c.Files = slices.Clone(t.Files)
|
|
c.DeliveredPaths = slices.Clone(t.DeliveredPaths)
|
|
return c
|
|
}
|
|
|
|
// Upsert inserts or replaces the entry with the same ID and persists the log.
|
|
func (h *History) Upsert(t Transfer) {
|
|
t.UpdatedAt = time.Now()
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if i := h.indexOf(t.ID); i >= 0 {
|
|
h.entries[i] = t
|
|
} else {
|
|
h.entries = slices.Insert(h.entries, 0, t)
|
|
h.prune()
|
|
}
|
|
h.persist()
|
|
}
|
|
|
|
// SetProgress updates the transferred byte count in memory only.
|
|
func (h *History) SetProgress(id OfferID, transferred int64) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if i := h.indexOf(id); i >= 0 {
|
|
h.entries[i].Transferred = transferred
|
|
h.entries[i].UpdatedAt = time.Now()
|
|
if h.entries[i].State == StatePending {
|
|
h.entries[i].State = StateTransferring
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get returns the entry with the given ID.
|
|
func (h *History) Get(id OfferID) (Transfer, bool) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
if i := h.indexOf(id); i >= 0 {
|
|
return h.entries[i].clone(), true
|
|
}
|
|
return Transfer{}, false
|
|
}
|
|
|
|
// List returns every entry, newest first.
|
|
func (h *History) List() []Transfer {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
list := make([]Transfer, len(h.entries))
|
|
for i, e := range h.entries {
|
|
list[i] = e.clone()
|
|
}
|
|
return list
|
|
}
|
|
|
|
// Delete removes one entry and persists the log.
|
|
func (h *History) Delete(id OfferID) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if i := h.indexOf(id); i >= 0 {
|
|
h.entries = slices.Delete(h.entries, i, i+1)
|
|
h.persist()
|
|
}
|
|
}
|
|
|
|
func (h *History) indexOf(id OfferID) int {
|
|
return slices.IndexFunc(h.entries, func(t Transfer) bool { return t.ID == id })
|
|
}
|
|
|
|
func (h *History) prune() {
|
|
if len(h.entries) <= historyCap {
|
|
return
|
|
}
|
|
for i := len(h.entries) - 1; i >= 0 && len(h.entries) > historyCap; i-- {
|
|
if h.entries[i].terminal() {
|
|
h.entries = slices.Delete(h.entries, i, i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// settleInterrupted closes out transfers that were still moving when the
|
|
// process died. Nothing is left to finish them, so left alone they would sit in
|
|
// the log as permanently pending. Reports whether anything changed.
|
|
func (h *History) settleInterrupted() bool {
|
|
changed := false
|
|
for i, t := range h.entries {
|
|
if t.terminal() {
|
|
continue
|
|
}
|
|
h.entries[i].State = StateFailed
|
|
h.entries[i].Reason = ReasonInterrupted
|
|
h.entries[i].UpdatedAt = time.Now()
|
|
changed = true
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func (h *History) persist() {
|
|
if err := saveSection(h.store, namespaceHistory, h.entries); err != nil {
|
|
log.Warnf("failed to write file drop history: %v", err)
|
|
}
|
|
}
|