mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-01 04:21:29 +02:00
Merge remote-tracking branch 'origin/file-share' into android-integration
# Conflicts: # client/android/client.go # client/internal/connect.go # client/internal/engine.go
This commit is contained in:
@@ -114,6 +114,11 @@ type Client struct {
|
||||
|
||||
extendMu sync.Mutex
|
||||
extendCancel context.CancelFunc
|
||||
|
||||
// The file drop handle survives engine restarts so the UI keeps one listener
|
||||
// registration and one history view across reconnects. See fileDropFor.
|
||||
fileDropMu sync.Mutex
|
||||
fileDrop *FileDrop
|
||||
}
|
||||
|
||||
func (c *Client) setState(cfg *profilemanager.Config, cacheDir string, cfgPath string, cc *internal.ConnectClient) {
|
||||
@@ -204,6 +209,7 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
|
||||
ctx = internal.CtxInitState(ctx)
|
||||
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder,
|
||||
internal.WithNetworkState(c.netState), internal.WithSweeper(c.sweeper))
|
||||
c.attachFileDrop(connectClient, cfgFile)
|
||||
c.setState(cfg, cacheDir, cfgFile, connectClient)
|
||||
// This path runs the interactive SSO flow, so reaching here means the peer
|
||||
// is authenticated again — release the latch Status() reports from. Clear
|
||||
@@ -246,6 +252,7 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
|
||||
ctx = internal.CtxInitState(ctx)
|
||||
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder,
|
||||
internal.WithNetworkState(c.netState), internal.WithSweeper(c.sweeper))
|
||||
c.attachFileDrop(connectClient, cfgFile)
|
||||
c.setState(cfg, cacheDir, cfgFile, connectClient)
|
||||
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
|
||||
}
|
||||
|
||||
78
client/android/client_filedrop.go
Normal file
78
client/android/client_filedrop.go
Normal file
@@ -0,0 +1,78 @@
|
||||
//go:build android
|
||||
|
||||
package android
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
)
|
||||
|
||||
// FileDrop returns the handle of the active profile, creating it on first use.
|
||||
// The UI calls this to list transfers and change settings while disconnected.
|
||||
func (c *Client) FileDrop(configDir string) (*FileDrop, error) {
|
||||
profile, err := NewProfileManager(configDir).GetActiveProfile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get active profile: %w", err)
|
||||
}
|
||||
return c.fileDropFor(configDir, profile.ID)
|
||||
}
|
||||
|
||||
// fileDropFor returns the handle of one profile, replacing the cached one when
|
||||
// the profile changed. The listener is carried over so a profile switch does not
|
||||
// silence the UI.
|
||||
func (c *Client) fileDropFor(configDir, profileID string) (*FileDrop, error) {
|
||||
c.fileDropMu.Lock()
|
||||
|
||||
if c.fileDrop != nil && c.fileDrop.ProfileID() == profileID {
|
||||
fd := c.fileDrop
|
||||
c.fileDropMu.Unlock()
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
fd, err := NewFileDrop(configDir, profileID)
|
||||
if err != nil {
|
||||
c.fileDropMu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
ensureFileDropDestination(fd)
|
||||
|
||||
old := c.fileDrop
|
||||
if old != nil {
|
||||
fd.SetListener(old.Listener())
|
||||
}
|
||||
c.fileDrop = fd
|
||||
c.fileDropMu.Unlock()
|
||||
|
||||
// Closing waits out the in-flight uploads of the profile being left, which is
|
||||
// far too long to hold the lock every caller of this goes through.
|
||||
if old != nil {
|
||||
if err := old.Close(); err != nil {
|
||||
log.Warnf("failed to close previous file drop manager: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// attachFileDrop hands the connect client the file drop manager of the profile
|
||||
// the engine is starting for. The profile is derived from the config path rather
|
||||
// than read from the active profile state, so a switch racing the startup cannot
|
||||
// pair one profile's engine with another's transfers. A failure is not fatal:
|
||||
// the tunnel is worth more than the feature, so the engine runs on without it.
|
||||
func (c *Client) attachFileDrop(cc *internal.ConnectClient, cfgFile string) {
|
||||
configDir, profileID, err := profileLocationFor(cfgFile)
|
||||
if err != nil {
|
||||
log.Warnf("file drop is unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fd, err := c.fileDropFor(configDir, profileID)
|
||||
if err != nil {
|
||||
log.Warnf("file drop is unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
cc.SetFileDropManager(fd.manager)
|
||||
}
|
||||
205
client/android/filedrop.go
Normal file
205
client/android/filedrop.go
Normal file
@@ -0,0 +1,205 @@
|
||||
//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.
|
||||
func NewFileDrop(configDir, profileID string) (*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
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
122
client/android/filedrop_payload.go
Normal file
122
client/android/filedrop_payload.go
Normal file
@@ -0,0 +1,122 @@
|
||||
//go:build android
|
||||
|
||||
package android
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/filedrop"
|
||||
)
|
||||
|
||||
// FileSource opens the bytes of one outgoing item. Android hands out content URIs
|
||||
// rather than paths, so the platform layer owns opening and seeking.
|
||||
type FileSource interface {
|
||||
// Open returns a stream positioned at offset. It is called once per attempt,
|
||||
// and again from the start when a transfer resumes.
|
||||
Open(offset int64) (SourceStream, error)
|
||||
}
|
||||
|
||||
// SourceStream is the readable half of a FileSource.
|
||||
//
|
||||
// It returns each chunk instead of filling a caller-supplied buffer: 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. Only
|
||||
// the return value crosses the bridge intact.
|
||||
type SourceStream interface {
|
||||
// NextChunk returns up to max bytes. An empty result means end of stream.
|
||||
NextChunk(max int) ([]byte, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type sourceStreamReader struct {
|
||||
stream SourceStream
|
||||
buf []byte
|
||||
eof bool
|
||||
}
|
||||
|
||||
// FileDropPayloads collects the items of one outgoing transfer.
|
||||
type FileDropPayloads struct {
|
||||
items []filedrop.Payload
|
||||
}
|
||||
|
||||
// NewFileDropPayloads returns an empty payload list to fill before sending.
|
||||
func NewFileDropPayloads() *FileDropPayloads {
|
||||
return &FileDropPayloads{}
|
||||
}
|
||||
|
||||
// AddFile appends a file item backed by a platform-provided source.
|
||||
func (p *FileDropPayloads) AddFile(name string, size int64, contentType string, source FileSource) error {
|
||||
if name == "" {
|
||||
return errors.New("file name is required")
|
||||
}
|
||||
if source == nil {
|
||||
return fmt.Errorf("file %s has no source", name)
|
||||
}
|
||||
|
||||
p.items = append(p.items, filedrop.Payload{
|
||||
Meta: filedrop.FileMeta{
|
||||
Name: name,
|
||||
Size: size,
|
||||
ContentType: contentType,
|
||||
},
|
||||
Open: func(offset int64) (io.ReadCloser, error) {
|
||||
stream, err := source.Open(offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stream == nil {
|
||||
return nil, fmt.Errorf("no stream for %s", name)
|
||||
}
|
||||
return &sourceStreamReader{stream: stream}, nil
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddText appends an inline text item.
|
||||
func (p *FileDropPayloads) AddText(name, text string) error {
|
||||
if len(text) > filedrop.MaxInlineTextSize {
|
||||
return fmt.Errorf("text exceeds %d bytes", filedrop.MaxInlineTextSize)
|
||||
}
|
||||
if name == "" {
|
||||
name = "text"
|
||||
}
|
||||
p.items = append(p.items, filedrop.TextPayload(name, text))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Length returns the number of items.
|
||||
func (p *FileDropPayloads) Length() int {
|
||||
return len(p.items)
|
||||
}
|
||||
|
||||
func (r *sourceStreamReader) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
for len(r.buf) == 0 {
|
||||
if r.eof {
|
||||
return 0, io.EOF
|
||||
}
|
||||
chunk, err := r.stream.NextChunk(len(p))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(chunk) == 0 {
|
||||
r.eof = true
|
||||
return 0, io.EOF
|
||||
}
|
||||
r.buf = chunk
|
||||
}
|
||||
|
||||
n := copy(p, r.buf)
|
||||
r.buf = r.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *sourceStreamReader) Close() error {
|
||||
return r.stream.Close()
|
||||
}
|
||||
261
client/android/filedrop_test.go
Normal file
261
client/android/filedrop_test.go
Normal file
@@ -0,0 +1,261 @@
|
||||
//go:build android
|
||||
|
||||
package android
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/filedrop"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
)
|
||||
|
||||
type stubStream struct {
|
||||
reader io.Reader
|
||||
closed bool
|
||||
// chunk caps what one call returns, so the reader's buffering is exercised
|
||||
// rather than every read landing in a single hop.
|
||||
chunk int
|
||||
}
|
||||
|
||||
type stubSource struct {
|
||||
content string
|
||||
offsets []int64
|
||||
chunk int
|
||||
}
|
||||
|
||||
func (s *stubStream) NextChunk(max int) ([]byte, error) {
|
||||
if s.chunk > 0 && s.chunk < max {
|
||||
max = s.chunk
|
||||
}
|
||||
buf := make([]byte, max)
|
||||
|
||||
n, err := s.reader.Read(buf)
|
||||
if errors.Is(err, io.EOF) || n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
func (s *stubStream) Close() error {
|
||||
s.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubSource) Open(offset int64) (SourceStream, error) {
|
||||
s.offsets = append(s.offsets, offset)
|
||||
return &stubStream{reader: strings.NewReader(s.content[offset:]), chunk: s.chunk}, nil
|
||||
}
|
||||
|
||||
func TestPayloadSourceReassemblesChunks(t *testing.T) {
|
||||
for name, chunk := range map[string]int{
|
||||
"one hop": 0,
|
||||
"three bytes": 3,
|
||||
"one byte": 1,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
source := &stubSource{content: "hello world", chunk: chunk}
|
||||
|
||||
payloads := NewFileDropPayloads()
|
||||
if err := payloads.AddFile("greeting.txt", 11, "text/plain", source); err != nil {
|
||||
t.Fatalf("AddFile: %v", err)
|
||||
}
|
||||
if payloads.Length() != 1 {
|
||||
t.Fatalf("expected 1 payload, got %d", payloads.Length())
|
||||
}
|
||||
|
||||
stream, err := payloads.items[0].Open(0)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
got, err := io.ReadAll(stream)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll: %v", err)
|
||||
}
|
||||
if string(got) != "hello world" {
|
||||
t.Fatalf("got %q, want %q", got, "hello world")
|
||||
}
|
||||
if err := stream.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayloadSourceHonoursOffset(t *testing.T) {
|
||||
source := &stubSource{content: "hello world"}
|
||||
|
||||
payloads := NewFileDropPayloads()
|
||||
if err := payloads.AddFile("greeting.txt", 11, "", source); err != nil {
|
||||
t.Fatalf("AddFile: %v", err)
|
||||
}
|
||||
|
||||
stream, err := payloads.items[0].Open(6)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
got, err := io.ReadAll(stream)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll: %v", err)
|
||||
}
|
||||
if string(got) != "world" {
|
||||
t.Fatalf("got %q, want %q", got, "world")
|
||||
}
|
||||
if len(source.offsets) != 1 || source.offsets[0] != 6 {
|
||||
t.Fatalf("expected one open at offset 6, got %v", source.offsets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayloadRejectsMissingSourceAndOversizedText(t *testing.T) {
|
||||
payloads := NewFileDropPayloads()
|
||||
|
||||
if err := payloads.AddFile("no-source.bin", 1, "", nil); err == nil {
|
||||
t.Fatal("expected an error for a file without a source")
|
||||
}
|
||||
if err := payloads.AddFile("", 1, "", &stubSource{}); err == nil {
|
||||
t.Fatal("expected an error for an empty file name")
|
||||
}
|
||||
if err := payloads.AddText("big", strings.Repeat("x", filedrop.MaxInlineTextSize+1)); err == nil {
|
||||
t.Fatal("expected an error for oversized text")
|
||||
}
|
||||
if payloads.Length() != 0 {
|
||||
t.Fatalf("expected no payloads, got %d", payloads.Length())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDropPersistsSettingsPerProfile(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
writeTestProfile(t, configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
writeTestProfile(t, configDir, "11111111222222223333333344444444")
|
||||
|
||||
first, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileDrop: %v", err)
|
||||
}
|
||||
defer first.Close()
|
||||
|
||||
if err := first.SetMode(FileDropModeAutoAccept); err != nil {
|
||||
t.Fatalf("SetMode: %v", err)
|
||||
}
|
||||
if err := first.SetPeerRule("peer-key", FileDropRuleBlock); err != nil {
|
||||
t.Fatalf("SetPeerRule: %v", err)
|
||||
}
|
||||
|
||||
second, err := NewFileDrop(configDir, "11111111222222223333333344444444")
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileDrop: %v", err)
|
||||
}
|
||||
defer second.Close()
|
||||
|
||||
if got := second.Mode(); got != FileDropModeAsk {
|
||||
t.Fatalf("second profile mode = %d, want the default %d", got, FileDropModeAsk)
|
||||
}
|
||||
if got := second.PeerRule("peer-key"); got != FileDropRuleDefault {
|
||||
t.Fatalf("second profile rule = %d, want %d", got, FileDropRuleDefault)
|
||||
}
|
||||
|
||||
reopened, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileDrop: %v", err)
|
||||
}
|
||||
defer reopened.Close()
|
||||
|
||||
if got := reopened.Mode(); got != FileDropModeAutoAccept {
|
||||
t.Fatalf("reopened mode = %d, want %d", got, FileDropModeAutoAccept)
|
||||
}
|
||||
if got := reopened.PeerRule("peer-key"); got != FileDropRuleBlock {
|
||||
t.Fatalf("reopened rule = %d, want %d", got, FileDropRuleBlock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDropSeedsDefaultDestination(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
writeTestProfile(t, configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
|
||||
fd, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileDrop: %v", err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if fd.DestinationDir() != "" {
|
||||
t.Fatalf("expected no destination before seeding, got %q", fd.DestinationDir())
|
||||
}
|
||||
|
||||
ensureFileDropDestination(fd)
|
||||
|
||||
want := filepath.Join(configDir, filedropDataSubdir, "aaaaaaaabbbbbbbbccccccccdddddddd", "incoming")
|
||||
if got := fd.DestinationDir(); got != want {
|
||||
t.Fatalf("destination = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDropSendWithoutTunnelFails(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
writeTestProfile(t, configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
|
||||
fd, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileDrop: %v", err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
payloads := NewFileDropPayloads()
|
||||
if err := payloads.AddText("note", "hi"); err != nil {
|
||||
t.Fatalf("AddText: %v", err)
|
||||
}
|
||||
|
||||
if _, err := fd.Send("peer-key", "peer", "100.64.0.2", payloads); !errors.Is(err, filedrop.ErrNotConnected) {
|
||||
t.Fatalf("Send error = %v, want %v", err, filedrop.ErrNotConnected)
|
||||
}
|
||||
if _, err := fd.Send("peer-key", "peer", "100.64.0.2", NewFileDropPayloads()); err == nil {
|
||||
t.Fatal("expected an error when there is nothing to send")
|
||||
}
|
||||
if _, err := fd.Send("peer-key", "peer", "not-an-ip", payloads); err == nil {
|
||||
t.Fatal("expected an error for an unparseable peer address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileLocationForSplitsConfigPath(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
dir, id, err := profileLocationFor(filepath.Join(root, defaultConfigFilename))
|
||||
if err != nil {
|
||||
t.Fatalf("default profile: %v", err)
|
||||
}
|
||||
if dir != root || id != profilemanager.DefaultProfileName {
|
||||
t.Fatalf("default profile = (%q, %q), want (%q, %q)", dir, id, root, profilemanager.DefaultProfileName)
|
||||
}
|
||||
|
||||
named := filepath.Join(root, profilesSubdir, "aaaaaaaabbbbbbbbccccccccdddddddd.json")
|
||||
dir, id, err = profileLocationFor(named)
|
||||
if err != nil {
|
||||
t.Fatalf("named profile: %v", err)
|
||||
}
|
||||
if dir != root || id != "aaaaaaaabbbbbbbbccccccccdddddddd" {
|
||||
t.Fatalf("named profile = (%q, %q), want (%q, %q)", dir, id, root, "aaaaaaaabbbbbbbbccccccccdddddddd")
|
||||
}
|
||||
|
||||
for _, path := range []string{"", filepath.Join(root, "stray.json"), filepath.Join(root, profilesSubdir, "not-an-id!.json")} {
|
||||
if _, _, err := profileLocationFor(path); err == nil {
|
||||
t.Fatalf("expected an error for %q", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestProfile(t *testing.T, configDir, id string) {
|
||||
t.Helper()
|
||||
|
||||
pm := NewProfileManager(configDir)
|
||||
if _, err := pm.serviceMgr.ProfilePrefs(profilemanager.ID(id), androidUsername); err != nil {
|
||||
t.Fatalf("resolve prefs for %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
163
client/android/filedrop_transfer.go
Normal file
163
client/android/filedrop_transfer.go
Normal file
@@ -0,0 +1,163 @@
|
||||
//go:build android
|
||||
|
||||
package android
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/filedrop"
|
||||
)
|
||||
|
||||
// The file drop receiving modes exported via gomobile.
|
||||
const (
|
||||
FileDropModeOff = int(filedrop.ModeOff)
|
||||
FileDropModeAsk = int(filedrop.ModeAsk)
|
||||
FileDropModeAutoAccept = int(filedrop.ModeAutoAccept)
|
||||
)
|
||||
|
||||
// The per-sender rules exported via gomobile.
|
||||
const (
|
||||
FileDropRuleDefault = int(filedrop.SenderRuleDefault)
|
||||
FileDropRuleAlwaysAccept = int(filedrop.SenderRuleAlwaysAccept)
|
||||
FileDropRuleBlock = int(filedrop.SenderRuleBlock)
|
||||
)
|
||||
|
||||
// The transfer states exported via gomobile.
|
||||
const (
|
||||
FileDropStatePending = int(filedrop.StatePending)
|
||||
FileDropStateTransferring = int(filedrop.StateTransferring)
|
||||
FileDropStateCompleted = int(filedrop.StateCompleted)
|
||||
FileDropStateDeclined = int(filedrop.StateDeclined)
|
||||
FileDropStateExpired = int(filedrop.StateExpired)
|
||||
FileDropStateCancelled = int(filedrop.StateCancelled)
|
||||
FileDropStateFailed = int(filedrop.StateFailed)
|
||||
)
|
||||
|
||||
// The failure reasons exported via gomobile.
|
||||
const (
|
||||
FileDropReasonNone = int(filedrop.ReasonNone)
|
||||
FileDropReasonUnreachable = int(filedrop.ReasonUnreachable)
|
||||
)
|
||||
|
||||
// The event kinds delivered to a FileDropListener.
|
||||
const (
|
||||
FileDropEventOffer = int(filedrop.EventOffer)
|
||||
FileDropEventCompleted = int(filedrop.EventCompleted)
|
||||
FileDropEventFailed = int(filedrop.EventFailed)
|
||||
FileDropEventWithdrawn = int(filedrop.EventWithdrawn)
|
||||
)
|
||||
|
||||
// FileDropListener receives transfer events. Calls arrive on background
|
||||
// goroutines, so implementations must post to the UI thread themselves.
|
||||
type FileDropListener interface {
|
||||
OnFileDropEvent(kind int, transfer *FileDropTransfer)
|
||||
}
|
||||
|
||||
// FileDropFile is one item of a transfer.
|
||||
type FileDropFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
ContentType string
|
||||
IsText bool
|
||||
Text string
|
||||
}
|
||||
|
||||
// FileDropTransfer is one history entry.
|
||||
type FileDropTransfer struct {
|
||||
ID string
|
||||
Outgoing bool
|
||||
PeerKey string
|
||||
PeerName string
|
||||
State int
|
||||
Transferred int64
|
||||
TotalSize int64
|
||||
// Unix milliseconds, so the platform layer can render the time in the
|
||||
// user's own locale and zone rather than parsing a preformatted string.
|
||||
CreatedAtMillis int64
|
||||
UpdatedAtMillis int64
|
||||
// IsText marks a transfer that is a single inline snippet rather than
|
||||
// files, so the UI can drop the size and offer a copy action instead.
|
||||
IsText bool
|
||||
Error string
|
||||
Reason int
|
||||
|
||||
files []*FileDropFile
|
||||
deliveredPaths []string
|
||||
}
|
||||
|
||||
// FileDropTransferArray wraps transfers for gomobile compatibility.
|
||||
type FileDropTransferArray struct {
|
||||
items []*FileDropTransfer
|
||||
}
|
||||
|
||||
// FileCount returns the number of items in the transfer.
|
||||
func (t *FileDropTransfer) FileCount() int {
|
||||
return len(t.files)
|
||||
}
|
||||
|
||||
// GetFile returns the item at index i, or nil when out of range.
|
||||
func (t *FileDropTransfer) GetFile(i int) *FileDropFile {
|
||||
if i < 0 || i >= len(t.files) {
|
||||
return nil
|
||||
}
|
||||
return t.files[i]
|
||||
}
|
||||
|
||||
// DeliveredPaths returns the delivered file paths joined by newlines, so the
|
||||
// platform layer can move them into user-visible storage.
|
||||
func (t *FileDropTransfer) DeliveredPaths() string {
|
||||
return strings.Join(t.deliveredPaths, "\n")
|
||||
}
|
||||
|
||||
// Length returns the number of transfers.
|
||||
func (a *FileDropTransferArray) Length() int {
|
||||
return len(a.items)
|
||||
}
|
||||
|
||||
// Get returns the transfer at index i, or nil when out of range.
|
||||
func (a *FileDropTransferArray) Get(i int) *FileDropTransfer {
|
||||
if i < 0 || i >= len(a.items) {
|
||||
return nil
|
||||
}
|
||||
return a.items[i]
|
||||
}
|
||||
|
||||
func toFileDropTransfer(t filedrop.Transfer) *FileDropTransfer {
|
||||
files := make([]*FileDropFile, 0, len(t.Files))
|
||||
for _, f := range t.Files {
|
||||
files = append(files, &FileDropFile{
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
ContentType: f.ContentType,
|
||||
IsText: f.Kind == filedrop.KindText,
|
||||
Text: f.Text,
|
||||
})
|
||||
}
|
||||
|
||||
return &FileDropTransfer{
|
||||
ID: string(t.ID),
|
||||
Outgoing: t.Direction == filedrop.DirectionSent,
|
||||
PeerKey: string(t.PeerKey),
|
||||
PeerName: t.PeerName,
|
||||
State: int(t.State),
|
||||
Transferred: t.Transferred,
|
||||
TotalSize: t.TotalSize,
|
||||
CreatedAtMillis: unixMillis(t.CreatedAt),
|
||||
UpdatedAtMillis: unixMillis(t.UpdatedAt),
|
||||
IsText: len(t.Files) == 1 && t.Files[0].Kind == filedrop.KindText,
|
||||
Error: t.Error,
|
||||
Reason: int(t.Reason),
|
||||
files: files,
|
||||
deliveredPaths: t.DeliveredPaths,
|
||||
}
|
||||
}
|
||||
|
||||
// unixMillis renders a timestamp for the platform layer, mapping the zero time
|
||||
// to 0 so it reads as "unknown" rather than as 1970.
|
||||
func unixMillis(t time.Time) int64 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return t.UnixMilli()
|
||||
}
|
||||
@@ -48,6 +48,33 @@ func profileAccountPathFor(configPath string) (string, error) {
|
||||
return filepath.Join(filepath.Dir(configPath), stem+profileAccountSuffix), nil
|
||||
}
|
||||
|
||||
// profileLocationFor splits a profile's config path back into the config dir and
|
||||
// the profile ID: <dir>/netbird.cfg is the default profile, while
|
||||
// <dir>/profiles/<id>.json is a named one.
|
||||
func profileLocationFor(configPath string) (string, string, error) {
|
||||
if configPath == "" {
|
||||
return "", "", fmt.Errorf("empty config path")
|
||||
}
|
||||
|
||||
base := filepath.Base(configPath)
|
||||
dir := filepath.Dir(configPath)
|
||||
|
||||
if base == defaultConfigFilename {
|
||||
return dir, profilemanager.DefaultProfileName, nil
|
||||
}
|
||||
|
||||
if filepath.Base(dir) != profilesSubdir {
|
||||
return "", "", fmt.Errorf("config path %q is outside the profiles directory", configPath)
|
||||
}
|
||||
|
||||
id := strings.TrimSuffix(base, filepath.Ext(base))
|
||||
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
||||
return "", "", fmt.Errorf("config path %q has no valid profile ID", configPath)
|
||||
}
|
||||
|
||||
return filepath.Dir(dir), id, nil
|
||||
}
|
||||
|
||||
// readProfileEmail returns the account email stored for the profile whose config
|
||||
// lives at configPath. A missing or unreadable file yields "", which leaves the
|
||||
// account choice to the IdP.
|
||||
|
||||
Reference in New Issue
Block a user