mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
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.
217 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|