Help us shape the future of Go! Take the 2026 Go Developer Survey

xfer

package
v1.3.98 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package xfer pkg/skychat/xfer/manager.go c4-app-chat

Package xfer pkg/skychat/xfer/xfer.go c4-app-chat

xfer is skychat's file-transfer primitive: it moves one file over a single skywire stream (a dmsg stream or a skynet route — the caller supplies the DialFunc/listener, exactly like pkg/skychat/call). Files are conversation messages, Telegram-style: the Offer is announced into chat/group history and the bytes stream out-of-band on skyenv.SkychatFilePort so a large transfer never blocks the chat-message conn.

Wire protocol on one transfer stream (all frames are pkg/skychat/message length-prefixed frames; the DATA between them is raw):

  1. Offer frame (sender → receiver: id, name, size, mime, from, group)
  2. Accept frame (receiver → sender: ok, err) — the receiver's AcceptFunc decides here, BEFORE any bytes move, so a declining or over-quota peer never receives the body
  3. exactly offer.Size raw bytes (only when Accept.ok)
  4. Trailer frame (sender → receiver: sha256 of the streamed bytes)
  5. Receipt frame (receiver → sender: id, ok, err) — final verify verdict

The digest is a trailer, not part of the offer, so the sender streams in a single pass (works for non-seekable sources such as stdin); the receiver hashes what it reads and compares against the trailer before accepting the file as complete. All bytes ride the encrypted skywire transport — nothing touches the clearnet.

Index

Constants

View Source
const (
	// IdleWindow bounds how long a transfer may make NO progress before it is
	// abandoned. It is deliberately an idle timeout and not a total one: a
	// total deadline is a file-size limit in disguise (a 35 MB file over a
	// slow route legitimately takes minutes), whereas a dead peer still fails
	// within one window.
	IdleWindow = 60 * time.Second
)
View Source
const MaxOfferSize = message.MaxFrameSize

MaxOfferSize caps an Offer/Trailer/Receipt control frame. The DATA is not bounded by this — only offer.Size bounds it, and the AcceptFunc is free to reject an over-large Size before a single byte is read.

Variables

View Source
var (

	// ErrNoReceipt reports that the body AND its digest trailer were written in
	// full but the receiver's verdict never came back. The bytes are on the
	// peer's side — only the confirmation is missing — so a caller should treat
	// this as delivered-but-unconfirmed rather than as a failed transfer.
	ErrNoReceipt = errors.New("xfer: no receipt")
)

Functions

func IsDeclined

func IsDeclined(err error) bool

IsDeclined reports whether err is the sentinel returned by Receive when the AcceptFunc declined the transfer (so callers can treat it as non-fatal).

func IsNoReceipt

func IsNoReceipt(err error) bool

IsNoReceipt reports whether err means "fully delivered, verdict unknown" — the body and its digest went out in full but the receiver's receipt never came back. Callers should treat this as sent, not failed.

Types

type AcceptFunc

type AcceptFunc func(from cipher.PubKey, offer Offer) (dst io.WriteCloser, ok bool)

AcceptFunc decides an inbound transfer. It receives the peer that opened the stream and the Offer, and returns the destination to stream into plus whether to accept. The app implements "auto-accept when already paired 1:1 or sharing a group" here, and points dst at a file under the downloads dir. Returning ok false declines the transfer (the sender gets a Receipt with OK=false).

type Config

type Config struct {
	// LocalPK is this node's public key (stamped into outgoing offers).
	LocalPK cipher.PubKey
	// Dial opens a transfer stream to a peer (dmsg or skynet, same port).
	Dial DialFunc
	// Port is the file-transfer port (skyenv.SkychatFilePort) — the port this
	// manager listens on and dials out to.
	Port uint16
	// Accept decides inbound transfers (auto-accept paired/group peers). Required
	// to receive; if nil, every inbound transfer is declined.
	Accept AcceptFunc
	// OnDone, if set, is called after each transfer completes (either direction)
	// with the final error (nil on success) — the app uses it to drop a message
	// into chat/group history and update progress.
	OnDone func(dir Direction, peer cipher.PubKey, offer Offer, err error)
	// Logger is optional.
	Logger *logging.Logger
}

Config configures a Manager.

type DialFunc

type DialFunc func(ctx context.Context, peer cipher.PubKey, port uint16) (net.Conn, error)

DialFunc opens a transfer stream to peer:port over a skywire network (a dmsg stream or a skynet route). Same shape and contract as voice.DialFunc — the manager supplies one that tries whichever network is up, always on the SAME port (skyenv.SkychatFilePort) across dmsg and skynet.

type Direction

type Direction string

Direction labels a completed transfer for the OnDone hook.

const (
	// Incoming is a transfer this node received.
	Incoming Direction = "incoming"
	// Outgoing is a transfer this node sent.
	Outgoing Direction = "outgoing"
)

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager accepts inbound file transfers on one port over every skywire network it's given a listener for (dmsg + skynet, same port — like voice), and dials out to send. It is safe for concurrent use.

func NewManager

func NewManager(cfg Config) *Manager

NewManager builds a Manager. A nil Accept means inbound transfers are declined.

func (*Manager) AddListener

func (m *Manager) AddListener(ctx context.Context, lis net.Listener)

AddListener starts accepting on one more listener after Serve is already running (e.g. the skynet listener that comes up only once the skynet networker registers). It returns immediately; accepting runs until ctx is done.

func (*Manager) SendFile

func (m *Manager) SendFile(ctx context.Context, peer cipher.PubKey, offer Offer, r io.Reader) (Receipt, error)

SendFile dials peer and streams the file described by offer from r. It stamps offer.From with the local PK. The returned Receipt carries the receiver's verdict; a non-nil error is a transport/protocol failure before a verdict.

func (*Manager) Serve

func (m *Manager) Serve(ctx context.Context, listeners ...net.Listener)

Serve accepts transfer streams on ALL provided listeners concurrently — pass the dmsg listener AND the skynet listener (both bound to cfg.Port). Returns when ctx is done or every listener has failed.

type Offer

type Offer struct {
	ID    string        `json:"id"`              // unique per transfer
	Name  string        `json:"name"`            // suggested basename (no path)
	Size  int64         `json:"size"`            // byte count
	MIME  string        `json:"mime,omitempty"`  // best-effort content type
	From  cipher.PubKey `json:"from"`            // sender PK
	Group string        `json:"group,omitempty"` // group id when shared to a group (else 1:1)
}

Offer describes a file being sent. It is the first frame on a transfer stream and doubles as the metadata rendered as a message in the conversation.

func Receive

func Receive(ctx context.Context, from cipher.PubKey, conn net.Conn, accept AcceptFunc) (Offer, error)

Receive reads an Offer from conn, asks accept where to put it (and whether to take it at all), streams offer.Size bytes into the destination while hashing, verifies the digest against the sender's trailer, and returns a Receipt to the sender. It returns the Offer and a nil error on a verified, accepted transfer; errDeclined when accept says no; a non-nil error on any wire/verify failure (the receiver still tries to send a negative Receipt so the sender learns). conn is NOT closed here.

type Receipt

type Receipt struct {
	ID  string `json:"id"`
	OK  bool   `json:"ok"`
	Err string `json:"err,omitempty"`
}

Receipt is the receiver's verdict, returned to the sender at the end.

func Send

func Send(ctx context.Context, conn net.Conn, offer Offer, r io.Reader) (Receipt, error)

Send streams offer + the first offer.Size bytes of r over conn, then reads the receiver's Receipt. The digest trailer is computed over exactly the bytes streamed, so r need not be seekable. conn is NOT closed here — the caller owns it (it may be a reused/pooled stream).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL