[client] Report file drop progress, and stop a withdrawn transfer

The receiver reported progress once per file, after the whole body had
been staged: spool.Write drains its reader before returning, so a large
file sat at nothing until it jumped to done. It now stages through a
reader that reports as the bytes land, the same way the sender already
did, and both sides thin their reports to one per percent and one per
200ms so a fast transfer cannot flood the history or the UI.

Progress also becomes an event of its own. It was left out because a
report per byte would have been unusable; thinned, it costs a handful of
events a second and spares every UI a poll. The daemon's event bridge
ignores the new kind, so nothing is published where a notification would
be noise.

Withdrawing consent mid-transfer did nothing. Cancel routed through
Decide, which only moves an offer out of Pending, so an accepted offer
kept its decision, kept its spool, and kept being uploaded into: the
receiver's list went quiet while the sender ran to completion. Revoke
takes an offer back whatever it has already answered, the staging reader
gives up as soon as consent is gone, and the sender stops rather than
retrying a refusal three times over. A transfer withdrawn this way reads
as declined on both ends, which is what it is, rather than as a failure.
This commit is contained in:
Zoltan Papp
2026-08-18 14:34:44 +02:00
parent a94424d563
commit b165e0126e
7 changed files with 262 additions and 20 deletions
+10 -3
View File
@@ -18,12 +18,14 @@ import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
// The event kinds. Progress is not an event: live transfers are polled.
// The event kinds. Progress is one of them, thinned to a few reports a second
// by the reader that raises it; see progressInterval.
const (
EventOffer EventKind = iota
EventCompleted
EventFailed
EventWithdrawn
EventProgress
)
// portSignalGrace bounds how long a failed attempt waits for one signal message
@@ -304,7 +306,7 @@ func (m *Manager) Cancel(id OfferID) {
}
if server != nil {
if offer, ok := server.Offers().Decide(id, DecisionDeclined); ok {
if offer, ok := server.Offers().Revoke(id); ok {
server.Spool().Remove(offer.ID)
}
}
@@ -409,6 +411,7 @@ func (m *Manager) runSend(ctx context.Context, client *Client, handle *sendHandl
total += n
}
m.history.SetProgress(transfer.ID, total)
m.emit(EventProgress, m.transferOf(transfer.ID))
}
if err := client.Upload(ctx, addr, remoteID, payloads, progress); err != nil {
@@ -484,7 +487,10 @@ func (m *Manager) failSend(ctx context.Context, id OfferID, err error) {
state := StateFailed
switch {
case errors.Is(err, ErrDeclined):
// Withdrawn mid-transfer reads the same way as refused up front: the
// receiver said no, which is an answer rather than a failure.
case errors.Is(err, ErrDeclined), errors.Is(err, ErrNotAccepted),
errors.Is(err, ErrOfferNotFound):
state = StateDeclined
case errors.Is(err, ErrExpired):
state = StateExpired
@@ -554,6 +560,7 @@ func (m *Manager) OnProgress(offer Offer, index int, received int64) {
total += n
}
m.history.SetProgress(offer.ID, total)
m.emit(EventProgress, m.transferOf(offer.ID))
}
// OnCompleted implements Notifier.