Files
netbird/client/android/filedrop.go
T
Zoltán Papp 861e0d5609 [client] Stage incoming file drop payloads through a platform sink
Android cannot address the user's shared storage by path, so a received
file had to land in app-private storage and be copied out afterwards,
needing twice the space of the transfer.

Put the staging area behind a Sink interface the receiver writes every
payload through. The filesystem spool implements it unchanged and stays
the default; a platform that cannot be addressed by path implements the
gomobile-bound half instead and stages payloads wherever it can reach.

The writer reports its own total rather than returning a written count:
gomobile copies a []byte argument into a fresh Java array and carries no
count back out.

A failed delivery now drops the staged payloads. The filesystem spool
swept them up on its next pass, but a sink holding entries the engine
cannot address by path has no such fallback.
2026-08-25 18:45:38 +02:00

217 lines
6.3 KiB
Go

//go:build android
package android
import (
"errors"
"fmt"
"net/netip"
"path/filepath"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/filedrop"
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
const filedropDataSubdir = "filedrop"
// FileDrop is the platform-facing handle on one profile's file drop state. It
// outlives the engine: the manager keeps policy and history readable while the
// tunnel is down, and sending simply fails until it comes back up.
type FileDrop struct {
mu sync.Mutex
configDir string
profileID string
manager *filedrop.Manager
listener FileDropListener
}
// NewFileDrop opens the file drop state of the given profile. A nil sink leaves
// payloads staged and delivered on the filesystem, under the destination
// directory the policy names.
func NewFileDrop(configDir, profileID string, sink FileDropSink) (*FileDrop, error) {
if configDir == "" || profileID == "" {
return nil, errors.New("file drop requires a config dir and profile ID")
}
prefs, err := newProfilePrefs(configDir, profileID)
if err != nil {
return nil, err
}
var platformSink filedrop.Sink
if sink != nil {
platformSink, err = filedrop.NewPlatformSink(newPlatformSinkAdapter(sink))
if err != nil {
return nil, fmt.Errorf("wrap file drop sink: %w", err)
}
}
fd := &FileDrop{configDir: configDir, profileID: profileID}
manager, err := filedrop.NewManager(filedrop.ManagerConfig{
Profile: profilemanager.ID(profileID),
DataDir: filepath.Join(configDir, filedropDataSubdir, profileID),
Store: filedrop.NewProfileStore(prefs.prefs),
Events: fd.publish,
Sink: platformSink,
})
if err != nil {
return nil, fmt.Errorf("create file drop manager: %w", err)
}
fd.manager = manager
return fd, nil
}
// ProfileID returns the profile this handle belongs to.
func (f *FileDrop) ProfileID() string {
return f.profileID
}
// SetListener installs the event listener, replacing any previous one.
func (f *FileDrop) SetListener(listener FileDropListener) {
f.mu.Lock()
defer f.mu.Unlock()
f.listener = listener
}
// Listener returns the installed event listener, nil when there is none.
func (f *FileDrop) Listener() FileDropListener {
f.mu.Lock()
defer f.mu.Unlock()
return f.listener
}
// RemoveListener stops event delivery.
func (f *FileDrop) RemoveListener() {
f.mu.Lock()
defer f.mu.Unlock()
f.listener = nil
}
// Send starts an asynchronous transfer and returns its local transfer ID.
func (f *FileDrop) Send(peerKey, peerName, peerIP string, payloads *FileDropPayloads) (string, error) {
if payloads == nil || payloads.Length() == 0 {
return "", errors.New("nothing to send")
}
addr, err := netip.ParseAddr(peerIP)
if err != nil {
return "", fmt.Errorf("parse peer address %q: %w", peerIP, err)
}
id, err := f.manager.Send(filedrop.PeerKey(peerKey), peerName, addr.Unmap(), payloads.items)
if err != nil {
return "", err
}
return string(id), nil
}
// Accept releases a pending incoming offer for download.
func (f *FileDrop) Accept(transferID string) error {
return f.manager.Accept(filedrop.OfferID(transferID))
}
// Decline refuses a pending incoming offer.
func (f *FileDrop) Decline(transferID string) error {
return f.manager.Decline(filedrop.OfferID(transferID))
}
// Cancel aborts a transfer in either direction.
func (f *FileDrop) Cancel(transferID string) {
f.manager.Cancel(filedrop.OfferID(transferID))
}
// Transfers returns the history, newest first.
func (f *FileDrop) Transfers() *FileDropTransferArray {
transfers := f.manager.Transfers()
items := make([]*FileDropTransfer, 0, len(transfers))
for _, t := range transfers {
items = append(items, toFileDropTransfer(t))
}
return &FileDropTransferArray{items: items}
}
// Transfer returns one history entry, or nil when it is unknown.
func (f *FileDrop) Transfer(transferID string) *FileDropTransfer {
for _, t := range f.manager.Transfers() {
if string(t.ID) == transferID {
return toFileDropTransfer(t)
}
}
return nil
}
// DeleteTransfer removes one history entry, cancelling it when still live.
func (f *FileDrop) DeleteTransfer(transferID string) {
f.manager.DeleteTransfer(filedrop.OfferID(transferID))
}
// Mode returns the base receiving mode.
func (f *FileDrop) Mode() int {
return int(f.manager.Policy().Get().Mode)
}
// SetMode changes the base receiving mode.
func (f *FileDrop) SetMode(mode int) error {
return f.manager.Policy().SetMode(filedrop.Mode(mode))
}
// DestinationDir returns the directory received files are delivered to.
func (f *FileDrop) DestinationDir() string {
return f.manager.DestinationDir()
}
// SetDestinationDir persists the delivery directory. It must be a filesystem
// path the app can write; content URIs are not paths, so the platform layer
// moves files out of this directory afterwards.
func (f *FileDrop) SetDestinationDir(dir string) error {
return f.manager.SetDestinationDir(dir)
}
// PeerRule returns the rule stored for one sender.
func (f *FileDrop) PeerRule(peerKey string) int {
return int(f.manager.Policy().Get().Senders[filedrop.PeerKey(peerKey)])
}
// SetPeerRule sets or clears the exception for one sender.
func (f *FileDrop) SetPeerRule(peerKey string, rule int) error {
return f.manager.SetSenderRule(filedrop.PeerKey(peerKey), filedrop.SenderRule(rule))
}
// Close stops the receiver and aborts every outgoing transfer.
func (f *FileDrop) Close() error {
f.RemoveListener()
return f.manager.Close()
}
func (f *FileDrop) publish(kind filedrop.EventKind, transfer filedrop.Transfer) {
f.mu.Lock()
listener := f.listener
f.mu.Unlock()
if listener == nil {
return
}
listener.OnFileDropEvent(int(kind), toFileDropTransfer(transfer))
}
// defaultFileDropDir is the app-private landing directory used until the
// platform layer configures one.
func defaultFileDropDir(configDir, profileID string) string {
return filepath.Join(configDir, filedropDataSubdir, profileID, "incoming")
}
// ensureFileDropDestination seeds the delivery directory on first use, so a
// received file always has somewhere to land.
func ensureFileDropDestination(fd *FileDrop) {
if fd.DestinationDir() != "" {
return
}
dir := defaultFileDropDir(fd.configDir, fd.profileID)
if err := fd.SetDestinationDir(dir); err != nil {
log.Warnf("failed to set default file drop destination: %v", err)
}
}