From 50197c73553e3b912d40d9d6ba79af64e1d4dec0 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 5 Sep 2026 19:12:43 +0200 Subject: [PATCH] [client] Fix file drop delivery and refine the files UI Seed the delivery directory from the IPC caller when the active profile carries no username, so the default profile gets a destination instead of failing every incoming transfer. Deliver text-only offers without requiring a destination directory at all. In the UI, drop the status label from the send picker rows, keep the dot, and give the transfer list per-kind icons: a quote for text, a file with an up or down arrow for files. --- client/internal/filedrop/delivery.go | 13 ++++ client/server/filedrop.go | 70 +++++++++++++------ client/server/server.go | 2 +- .../src/modules/main/advanced/files/Files.tsx | 37 +++++----- .../main/advanced/files/SendPeerPicker.tsx | 6 +- 5 files changed, 86 insertions(+), 42 deletions(-) diff --git a/client/internal/filedrop/delivery.go b/client/internal/filedrop/delivery.go index 19126b8d6..2b4355239 100644 --- a/client/internal/filedrop/delivery.go +++ b/client/internal/filedrop/delivery.go @@ -11,6 +11,19 @@ import ( ) func deliver(spool *Spool, offer Offer, destDir string) ([]string, error) { + files := false + for _, f := range offer.Files { + if f.Kind != KindText { + files = true + break + } + } + + if !files { + spool.Remove(offer.ID) + return nil, nil + } + if destDir == "" { return nil, fmt.Errorf("no destination directory configured") } diff --git a/client/server/filedrop.go b/client/server/filedrop.go index 5e1cd2df1..861975f61 100644 --- a/client/server/filedrop.go +++ b/client/server/filedrop.go @@ -10,6 +10,7 @@ import ( "os" "os/user" "path/filepath" + "strconv" log "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" @@ -20,7 +21,7 @@ import ( "github.com/netbirdio/netbird/client/proto" ) -func (s *Server) fileDropManager() (*filedrop.Manager, error) { +func (s *Server) fileDropManager(ctx context.Context) (*filedrop.Manager, error) { activeProf, err := s.profileManager.GetActiveProfileState() if err != nil { return nil, fmt.Errorf("get active profile: %w", err) @@ -33,6 +34,7 @@ func (s *Server) fileDropManager() (*filedrop.Manager, error) { if s.fileDrop != nil && s.fileDrop.Profile() == activeProf.ID { mgr := s.fileDrop s.mutex.Unlock() + seedFileDropDestination(ctx, mgr, activeProf.Username) return mgr, nil } old := s.fileDrop @@ -60,13 +62,14 @@ func (s *Server) fileDropManager() (*filedrop.Manager, error) { return nil, fmt.Errorf("create file drop manager: %w", err) } - seedFileDropDestination(mgr, activeProf.Username) + seedFileDropDestination(ctx, mgr, activeProf.Username) s.mutex.Lock() if s.fileDrop != nil && s.fileDrop.Profile() == activeProf.ID { winner := s.fileDrop s.mutex.Unlock() _ = mgr.Close() + seedFileDropDestination(ctx, winner, activeProf.Username) return winner, nil } s.fileDrop = mgr @@ -118,7 +121,7 @@ func (s *Server) publishFileDropEvent(kind filedrop.EventKind, transfer filedrop // FileDropSend starts an asynchronous transfer to a peer. func (s *Server) FileDropSend(ctx context.Context, req *proto.FileDropSendRequest) (*proto.FileDropSendResponse, error) { - mgr, err := s.fileDropManager() + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -182,8 +185,8 @@ func (s *Server) buildFileDropPayloads(ctx context.Context, req *proto.FileDropS } // FileDropDecide accepts or declines a pending incoming offer. -func (s *Server) FileDropDecide(_ context.Context, req *proto.FileDropDecideRequest) (*proto.FileDropDecideResponse, error) { - mgr, err := s.fileDropManager() +func (s *Server) FileDropDecide(ctx context.Context, req *proto.FileDropDecideRequest) (*proto.FileDropDecideResponse, error) { + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -201,8 +204,8 @@ func (s *Server) FileDropDecide(_ context.Context, req *proto.FileDropDecideRequ } // FileDropCancel aborts a transfer in either direction. -func (s *Server) FileDropCancel(_ context.Context, req *proto.FileDropCancelRequest) (*proto.FileDropCancelResponse, error) { - mgr, err := s.fileDropManager() +func (s *Server) FileDropCancel(ctx context.Context, req *proto.FileDropCancelRequest) (*proto.FileDropCancelResponse, error) { + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -211,8 +214,8 @@ func (s *Server) FileDropCancel(_ context.Context, req *proto.FileDropCancelRequ } // FileDropListTransfers returns the transfer history, newest first. -func (s *Server) FileDropListTransfers(context.Context, *proto.FileDropListTransfersRequest) (*proto.FileDropListTransfersResponse, error) { - mgr, err := s.fileDropManager() +func (s *Server) FileDropListTransfers(ctx context.Context, _ *proto.FileDropListTransfersRequest) (*proto.FileDropListTransfersResponse, error) { + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -228,8 +231,8 @@ func (s *Server) FileDropListTransfers(context.Context, *proto.FileDropListTrans } // FileDropDeleteTransfer removes one entry from the transfer history. -func (s *Server) FileDropDeleteTransfer(_ context.Context, req *proto.FileDropDeleteTransferRequest) (*proto.FileDropDeleteTransferResponse, error) { - mgr, err := s.fileDropManager() +func (s *Server) FileDropDeleteTransfer(ctx context.Context, req *proto.FileDropDeleteTransferRequest) (*proto.FileDropDeleteTransferResponse, error) { + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -238,8 +241,8 @@ func (s *Server) FileDropDeleteTransfer(_ context.Context, req *proto.FileDropDe } // FileDropGetSettings returns the active profile's receiving policy. -func (s *Server) FileDropGetSettings(context.Context, *proto.FileDropGetSettingsRequest) (*proto.FileDropGetSettingsResponse, error) { - mgr, err := s.fileDropManager() +func (s *Server) FileDropGetSettings(ctx context.Context, _ *proto.FileDropGetSettingsRequest) (*proto.FileDropGetSettingsResponse, error) { + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -259,7 +262,7 @@ func (s *Server) FileDropGetSettings(context.Context, *proto.FileDropGetSettings // FileDropSetSettings updates the active profile's receiving policy. func (s *Server) FileDropSetSettings(ctx context.Context, req *proto.FileDropSetSettingsRequest) (*proto.FileDropSetSettingsResponse, error) { - mgr, err := s.fileDropManager() + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -305,8 +308,8 @@ func (s *Server) validateFileDropDestination(ctx context.Context, dir string) er } // FileDropSetPeerRule sets or clears a per-sender exception. -func (s *Server) FileDropSetPeerRule(_ context.Context, req *proto.FileDropSetPeerRuleRequest) (*proto.FileDropSetPeerRuleResponse, error) { - mgr, err := s.fileDropManager() +func (s *Server) FileDropSetPeerRule(ctx context.Context, req *proto.FileDropSetPeerRuleRequest) (*proto.FileDropSetPeerRuleResponse, error) { + mgr, err := s.fileDropManager(ctx) if err != nil { return nil, err } @@ -390,18 +393,17 @@ func fileDropPayload(caller ipcauth.Identity, path string) (filedrop.Payload, er // a received file always has somewhere to land. Resolved from the profile's own // user rather than the process: the daemon runs as root, whose home is not where // the user would look for their downloads. -func seedFileDropDestination(mgr *filedrop.Manager, username string) { +func seedFileDropDestination(ctx context.Context, mgr *filedrop.Manager, username string) { if mgr.DestinationDir() != "" { return } - u, err := user.Lookup(username) - if err != nil { - log.Warnf("cannot resolve home of %s for the file drop destination: %v", username, err) + u, ok := fileDropDestinationUser(ctx, username) + if !ok { return } if u.HomeDir == "" { - log.Warnf("user %s has no home directory for the file drop destination", username) + log.Warnf("user %s has no home directory for the file drop destination", u.Username) return } @@ -410,3 +412,29 @@ func seedFileDropDestination(mgr *filedrop.Manager, username string) { log.Warnf("failed to set the default file drop destination: %v", err) } } + +func fileDropDestinationUser(ctx context.Context, username string) (*user.User, bool) { + if username != "" { + u, err := user.Lookup(username) + if err != nil { + log.Warnf("cannot resolve home of %s for the file drop destination: %v", username, err) + return nil, false + } + return u, true + } + + caller, ok := ipcauth.CallerIdentity(ctx) + if !ok || caller.IsPrivileged() { + return nil, false + } + id := caller.SID + if !caller.IsWindows() { + id = strconv.FormatUint(uint64(caller.UID), 10) + } + u, err := user.LookupId(id) + if err != nil { + log.Warnf("cannot resolve home of caller %s for the file drop destination: %v", id, err) + return nil, false + } + return u, true +} diff --git a/client/server/server.go b/client/server/server.go index 1f746beea..c0c44917d 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -2495,7 +2495,7 @@ func (s *Server) connect(ctx context.Context, config *profilemanager.Config, sta client.SetUpdateManager(s.updateManager) client.SetSyncResponsePersistence(s.persistSyncResponse) - if mgr, err := s.fileDropManager(); err != nil { + if mgr, err := s.fileDropManager(ctx); err != nil { log.Warnf("file drop is unavailable: %v", err) } else { client.SetFileDropManager(mgr) diff --git a/client/ui/frontend/src/modules/main/advanced/files/Files.tsx b/client/ui/frontend/src/modules/main/advanced/files/Files.tsx index 9e9e68d79..fbcbe5323 100644 --- a/client/ui/frontend/src/modules/main/advanced/files/Files.tsx +++ b/client/ui/frontend/src/modules/main/advanced/files/Files.tsx @@ -3,15 +3,16 @@ import { useTranslation } from "react-i18next"; import { Events } from "@wailsio/runtime"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { - ArrowDownIcon, - ArrowUpIcon, BanIcon, CheckIcon, ChevronDownIcon, CopyIcon, + FileDownIcon, + FileUpIcon, FolderDownIcon, FolderOpenIcon, MoreVerticalIcon, + QuoteIcon, SendIcon, ShieldCheckIcon, Trash2Icon, @@ -57,15 +58,21 @@ const transferTitle = (transfer: FileDropTransfer): string => { const isTextTransfer = (transfer: FileDropTransfer): boolean => (transfer.files ?? []).length === 1 && transfer.files[0].isText; +const kindIcon = (transfer: FileDropTransfer) => { + if (isTextTransfer(transfer)) return QuoteIcon; + return transfer.outgoing ? FileUpIcon : FileDownIcon; +}; + const timeLabel = (iso: string): string => { const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; -// Colours only what the eye should catch scanning the outcome column: a refusal -// or failure in red, a completed send in green. Everything else, a received file -// included, stays neutral so the exceptions stand out. +// The outcome column has its own legend, separate from the icon's: green is a +// transfer that arrived, either way round, red is one the far side refused or +// that broke, grey is everything else. A transfer in flight is grey too, the +// progress bar already says it is moving. const outcomeClass = (transfer: FileDropTransfer, inProgress: boolean): string => { if (inProgress) return "text-nb-gray-500"; switch (transfer.state) { @@ -73,7 +80,7 @@ const outcomeClass = (transfer: FileDropTransfer, inProgress: boolean): string = case TransferState.Failed: return "text-red-400"; case TransferState.Completed: - return transfer.outgoing ? "text-green-400" : "text-nb-gray-500"; + return "text-green-400"; default: return "text-nb-gray-500"; } @@ -273,6 +280,7 @@ type RowProps = { const PendingOfferRow = ({ transfer, onChanged }: RowProps) => { const { t } = useTranslation(); const [busy, setBusy] = useState(false); + const OfferIcon = kindIcon(transfer); const decide = async (accept: boolean) => { if (busy) return; @@ -297,7 +305,7 @@ const PendingOfferRow = ({ transfer, onChanged }: RowProps) => { )} >
- +
{ const { t } = useTranslation(); const [copied, setCopied] = useState(false); const isText = isTextTransfer(transfer); + const RowKindIcon = kindIcon(transfer); const live = !isTerminalState(transfer.state); const delivered = !transfer.outgoing && @@ -389,15 +398,11 @@ const TransferRow = ({ transfer, onChanged }: RowProps) => { "wails-no-draggable", )} > - {transfer.outgoing ? ( - - ) : ( - - )} +
{shortenDns(peer.fqdn) || peer.ip} - - {t(peerStatusLabelKey(peer.connStatus))} - ))}