mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
[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.
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
+49
-21
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
)}
|
||||
>
|
||||
<div className={"flex min-w-0 items-center gap-2.5"}>
|
||||
<ArrowDownIcon size={16} className={"shrink-0 text-netbird"} aria-hidden={"true"} />
|
||||
<OfferIcon size={16} className={"shrink-0 text-netbird"} aria-hidden={"true"} />
|
||||
<div className={"flex min-w-0 flex-1 flex-col leading-tight"}>
|
||||
<span
|
||||
className={
|
||||
@@ -344,6 +352,7 @@ const TransferRow = ({ 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 ? (
|
||||
<ArrowUpIcon size={15} className={"shrink-0 text-netbird"} aria-hidden={"true"} />
|
||||
) : (
|
||||
<ArrowDownIcon
|
||||
size={15}
|
||||
className={"shrink-0 text-green-400"}
|
||||
aria-hidden={"true"}
|
||||
/>
|
||||
)}
|
||||
<RowKindIcon
|
||||
size={15}
|
||||
className={cn("shrink-0", transfer.outgoing ? "text-netbird" : "text-green-400")}
|
||||
aria-hidden={"true"}
|
||||
/>
|
||||
<div className={"flex min-w-0 flex-1 flex-col gap-0.5 leading-tight"}>
|
||||
<TruncatedText
|
||||
text={transferTitle(transfer)}
|
||||
|
||||
@@ -242,7 +242,8 @@ const PeerStep = ({
|
||||
className={itemClass}
|
||||
>
|
||||
<span
|
||||
aria-hidden={"true"}
|
||||
role={"img"}
|
||||
aria-label={t(peerStatusLabelKey(peer.connStatus))}
|
||||
className={cn(
|
||||
"h-1.5 w-1.5 shrink-0 rounded-full",
|
||||
dotClass(peer.connStatus),
|
||||
@@ -251,9 +252,6 @@ const PeerStep = ({
|
||||
<span className={"min-w-0 flex-1 truncate font-medium"}>
|
||||
{shortenDns(peer.fqdn) || peer.ip}
|
||||
</span>
|
||||
<span className={"shrink-0 text-nb-gray-400"}>
|
||||
{t(peerStatusLabelKey(peer.connStatus))}
|
||||
</span>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.List>
|
||||
|
||||
Reference in New Issue
Block a user