Early-stage software. Shurli is experimental and built with AI assistance. It will have bugs. Not recommended for production or safety-critical use. Read the disclaimer.
File Transfer

File Transfer

Shurli includes a built-in file transfer plugin that sends files directly between peers over the P2P network. Files are chunked with FastCDC, compressed with zstd, and verified with a BLAKE3 Merkle tree. Relay is blocked for file transfer by default (drives own-relay adoption).

Sending a File

# By peer name (fire-and-forget - exits immediately)
shurli send photo.jpg home-server

# With live progress
shurli send photo.jpg home-server --follow

# With priority (jumps the queue)
shurli send photo.jpg home-server --priority

# Send a directory
shurli send ./folder home-server

# JSON output (for scripting)
shurli send photo.jpg home-server --json

shurli send is fire-and-forget by default. The daemon handles the transfer in the background. Use --follow to watch progress inline.

Receiving Files

Receive behavior is controlled by the receive mode (AirDrop-style):

ModeBehavior
offReject all incoming transfers
contactsAuto-accept from authorized peers (default)
askQueue for manual approval via shurli accept/shurli reject
openAccept from any authorized peer without prompting
timedTemporarily open, reverts to previous mode after duration

Set the receive mode:

shurli config set transfer.receive_mode ask

# Timed mode: open for 10 minutes then revert
shurli config set transfer.receive_mode timed --duration 10m

Default receive directory: ~/Downloads/shurli/

Change it with:

shurli config set transfer.receive_dir /path/to/your/dir

If a file with the same name already exists, Shurli creates photo (1).jpg, photo (2).jpg, etc.

Managing Transfers

# View transfer inbox (pending + active)
shurli transfers

# Watch live (auto-refresh)
shurli transfers --watch

# View completed transfers
shurli transfers --history

# Accept a pending transfer
shurli accept <transfer-id>

# Accept only specific files (1-indexed)
shurli accept <transfer-id> --files 1,3,5

# Accept all except specific files
shurli accept <transfer-id> --exclude 2,4

# Accept with index ranges
shurli accept <transfer-id> --files 1-5,10,15-20

# Accept all pending
shurli accept --all

# Reject a pending transfer
shurli reject <transfer-id>

# View single transfer details (including file list)
shurli transfers <transfer-id>

# Cancel an outbound transfer
shurli cancel <transfer-id>

Sharing Files

Share files for other peers to browse and download on demand:

# Share a file with all authorized peers
shurli share add /path/to/file.pdf

# Share a directory (hidden files like .env, .git/ are excluded from downloads)
shurli share add /path/to/photos

# Share with a specific peer only
shurli share add /path/to/file.pdf --to home-server

# List your shares
shurli share list

# Remove a share
shurli share remove /path/to/file.pdf

# Browse a peer's shared files
shurli browse home-server

# Download a specific file from a peer's shares
shurli download home-server:shareID/document.pdf

# Download an entire shared directory
shurli download home-server:shareID

# Download a subdirectory
shurli download home-server:shareID/photos/2024

# List files in a shared directory (preview before downloading)
shurli download home-server:shareID --list

# Download only specific files from a shared directory (1-indexed)
shurli download home-server:shareID --files 1,3,10-20

# Download all except specific files
shurli download home-server:shareID --exclude 2,4

Directory downloads use the same SHFT streaming protocol as sends. The server walks the directory, builds a file table, and streams all files in a single transfer. Hidden files (dot-prefixed: .env, .git/, .ssh/) are automatically excluded from directory shares to prevent accidental data leakage. Single-file shares of hidden files (e.g., shurli share add .env) are served normally since the share was explicit.

Selective file rejection (--files/--exclude) is not supported with erasure-coded transfers (WAN/relay) or multi-peer downloads. Accept all files or reject the entire transfer in those cases.

Shares persist across daemon restarts (stored in ~/.shurli/shares.json).

Multi-Source Download

Download a file from multiple peers simultaneously using RaptorQ fountain codes:

shurli download large-file.zip home-server --multi-peer --peers home-server,laptop

Each peer contributes RaptorQ symbols. Any sufficient subset of symbols reconstructs the file. Faster than single-source for large files across multiple peers.

Requirements

  • Both peers must be running the daemon (shurli daemon)
  • Peers must be paired (via shurli invite / shurli join)
  • Works over LAN (mDNS) or direct connections. Relay is blocked by default.

How It Works

Chunking: FastCDC content-defined chunking with 5 adaptive tiers (64KB-4MB based on file size). Single-pass with BLAKE3 hash per chunk.

Integrity: BLAKE3 Merkle tree over all chunk hashes. Root hash verified after all chunks received. Each chunk verified before writing to disk.

Compression: zstd compression on by default. Auto-detects incompressible data and skips re-compression. Bomb protection: decompression aborted if output exceeds 10x compressed size. Opt-out via shurli config set transfer.compress false.

Erasure Coding: Reed-Solomon erasure coding, auto-enabled on Direct WAN connections only. Recovers from lost chunks without retransmission. Wire overhead matches the configured transfer.erasure_overhead (default 10%); transfer.bandwidth_budget and per-peer bandwidth_budget ACL attributes are enforced on TOTAL wire bytes (data + parity), so a 100 MB file with 10% erasure consumes ~110 MB of budget. Memory footprint per erasure-coded transfer is bounded to roughly one stripe (≤~400 MB sustained, ≤~880 MB momentary during encode) via the incremental per-stripe encoder; LAN transfers skip erasure entirely and avoid this cost.

Parallel Streams: Adaptive parallel QUIC streams per transfer. Defaults: 8 on LAN (max 32), 4 on WAN (max 20). Minimum 4 chunks per stream to justify parallelism.

Resume: Checkpoint files (.shurli-ckpt-<hash>) store a bitfield of received chunks. Interrupted transfers resume from the last checkpoint. Checkpoints cleaned up on successful completion.

macOS LAN Send Throughput: When macOS is the sender over a LAN, QUIC throughput is ~80-90 MB/s for large files instead of the ~107 MB/s that TCP (scp) achieves. This is a platform limitation: macOS lacks UDP GSO (Generic Segmentation Offload), forcing each QUIC packet through a separate system call. The reverse direction (Linux sending to macOS) and WAN transfers are unaffected. See the QUIC Transport Performance section in the SDK documentation for the full technical analysis.

Security

  • Integrity: BLAKE3 Merkle tree verification. Corrupted chunks are rejected before writing to disk.
  • Path traversal: Filenames like ../../../etc/passwd are sanitized. Only the base filename is used. Receive directory is a jail.
  • Transport encryption: All data travels over libp2p’s encrypted transport (TLS 1.3 or Noise).
  • Authorization: Only paired peers can send files. Unauthorized peers are silently rejected at the connection gating layer.
  • Resource limits: Per-peer queue depth 10 (configurable), per-peer concurrent 5 (configurable), global concurrent 20 (configurable), 1M chunk limit, 40MB manifest limit, 1h timeout. Ask-mode pending transfers consume zero capacity slots.
  • Disk space: Re-checked before each chunk write, not just at accept time.
  • Transfer IDs: Random hex (xfer-<12hex>), not sequential (prevents enumeration).
  • Compression bombs: zstd decompression capped at 10x ratio per chunk.
  • No symlink following in share paths. Regular files only.

Configuration

KeyDefaultDescription
transfer.receive_modecontactsReceive mode: off, contacts, ask, open, timed
transfer.receive_dir~/Downloads/shurli/Directory for received files
transfer.compresstrueEnable zstd compression
transfer.erasure_overhead0.1Reed-Solomon parity ratio (0.0-0.5)
transfer.max_concurrent5Max concurrent outbound transfers
transfer.max_inbound_transfers20Max concurrent inbound transfers (global)
transfer.max_per_peer_transfers5Max concurrent inbound transfers per peer
transfer.max_file_size0 (unlimited)Max file size to accept (bytes)
transfer.timed_duration10mDefault duration for timed receive mode
transfer.notifynoneNotification mode: none, desktop, command
transfer.notify_command""Command template with {from}, {file}, {size}
transfer.log_path~/.shurli/logs/transfers.logTransfer event log path
transfer.multi_peer_enabledtrueEnable multi-peer swarming downloads
transfer.multi_peer_max_peers4Max peers for multi-source download

Daemon API

For programmatic use (SDK consumers, scripts, other applications):

Send a file

curl -X POST --unix-socket ~/.shurli/shurli.sock \
  http://localhost/v1/send \
  -H "Cookie: auth=$(cat ~/.shurli/.daemon-cookie)" \
  -H "Content-Type: application/json" \
  -d '{"file_path": "/absolute/path/to/file.pdf", "peer": "home-server"}'

Response:

{
  "transfer_id": "xfer-a1b2c3d4e5f6"
}

Check transfer progress

curl --unix-socket ~/.shurli/shurli.sock \
  "http://localhost/v1/transfers/xfer-a1b2c3d4e5f6" \
  -H "Cookie: auth=$(cat ~/.shurli/.daemon-cookie)"

List all transfers

curl --unix-socket ~/.shurli/shurli.sock \
  http://localhost/v1/transfers \
  -H "Cookie: auth=$(cat ~/.shurli/.daemon-cookie)"

See the Daemon API reference for the full list of 15 file transfer endpoints.


Go API Reference

The file transfer engine lives in plugins/filetransfer/. Import path:

import "github.com/shurlinet/shurli/plugins/filetransfer"

Generic SDK utilities (MerkleRoot, transport classification, relay grant interface) are imported from pkg/sdk/. See the Go SDK reference for those types. See the Plugin System for the plugin framework (Plugin interface, PluginContext, lifecycle, registration).

type FileTransferPlugin

type FileTransferPlugin struct {
    // unexported fields
}

Implements the Plugin interface. Owns the TransferService and ShareRegistry.

func New() *FileTransferPlugin

Creates a new FileTransferPlugin instance. Called by the plugin registry at startup.

type PluginConfig

type PluginConfig struct {
    ReceiveDir        string   `yaml:"receive_dir"`
    MaxFileSize       int64    `yaml:"max_file_size"`
    ReceiveMode       string   `yaml:"receive_mode"`
    TimedDuration     string   `yaml:"timed_duration"`
    Compress          *bool    `yaml:"compress"`
    Notify            string   `yaml:"notify"`
    NotifyCommand     string   `yaml:"notify_command"`
    LogPath           string   `yaml:"log_path"`
    MaxConcurrent     int      `yaml:"max_concurrent"`
    RateLimit         int      `yaml:"rate_limit"`
    BrowseRateLimit   int      `yaml:"browse_rate_limit"`
    QueueFile         string   `yaml:"queue_file"`
    MultiPeerEnabled  *bool    `yaml:"multi_peer_enabled"`
    MultiPeerMaxPeers int      `yaml:"multi_peer_max_peers"`
    MultiPeerMinSize  int64    `yaml:"multi_peer_min_size"`
    ErasureOverhead     *float64 `yaml:"erasure_overhead"`
    MaxInboundTransfers int      `yaml:"max_inbound_transfers"`
    MaxPerPeerTransfers int      `yaml:"max_per_peer_transfers"`
    GlobalRateLimit     int      `yaml:"global_rate_limit"`
    MaxQueuedPerPeer  int      `yaml:"max_queued_per_peer"`
    MinSpeedBytes     int      `yaml:"min_speed_bytes"`
    MinSpeedSeconds   int      `yaml:"min_speed_seconds"`
    MaxTempSize       int64    `yaml:"max_temp_size"`
    TempFileExpiry    string   `yaml:"temp_file_expiry"`
    BandwidthBudget   string   `yaml:"bandwidth_budget"`
    DefaultPersistent *bool    `yaml:"default_persistent"`
}

YAML configuration loaded from the plugin’s config directory. Parsed by loadConfig(), hot-reloaded by reloadConfig().


Protocol Constants

const (
    TransferProtocol  = "/shurli/file-transfer/2.0.0"
    BrowseProtocol    = "/shurli/file-browse/1.0.0"
    DownloadProtocol  = "/shurli/file-download/1.0.0"
    MultiPeerProtocol = "/shurli/file-multi-peer/1.0.0"
    CancelProtocol    = "/shurli/transfer-cancel/1.0.0"
)

Download Protocol Request Types

The download protocol multiplexes three request types on a single stream:

ByteNameDescription
0x01requestTypeDownloadFull file/directory download via SHFT streaming
0x02requestTypeProbeHash probe for multi-peer coordination (single files only)
0x03requestTypeListFile listing without transfer (directory contents preview)

Wire format (client to server): pathLen(2) + path + requestType(1).

requestTypeList returns a lightweight file table response: 'L'(1) + fileCount(2) + totalSize(8) + [fileCount x (pathLen(2) + path + size(8))]. Used by shurli download --list to preview directory contents with indices before downloading. No goroutine launched, no progress tracking. Rate-limited alongside all other request types.

requestTypeProbe rejects directory paths. Probing requires single-file chunking to compute the MerkleRoot; directory transfers use cross-file CDC with different chunk boundaries.

Download Serving Concurrency

HandleDownload limits concurrent download-serving goroutines to prevent resource exhaustion:

  • Per-peer limit: 3 concurrent downloads per peer (maxPerPeerServing)
  • Global limit: 10 concurrent downloads total (maxGlobalServing)

Slots are acquired non-blocking before SendFile. On rejection: "server busy, try later". The serving slot tracks the actual SendFile goroutine lifetime via the OnComplete callback (not the handler return), ensuring slots are held for the full transfer duration.

Cancel Protocol

func RegisterCancelHandler(h host.Host, ts *TransferService)

Registers the multi-path cancel protocol handler on the host. Called from plugin.Start(). Handles inbound cancel messages: reads a 32-byte transferID, verifies the sender matches the active transfer peer, and cancels the matching send or receive session. Rate-limited to 10 messages per peer per minute with a 5-second stream deadline.

func UnregisterCancelHandler(h host.Host)

Removes the cancel protocol handler from the host. Called from plugin.Stop() to prevent handler access after TransferService is closed.

Reject Reasons

const (
    RejectReasonNone  byte = 0x00 // no reason disclosed
    RejectReasonSpace byte = 0x01 // insufficient disk space
    RejectReasonBusy  byte = 0x02 // receiver busy
    RejectReasonSize  byte = 0x03 // file too large
)

type ReceiveMode

type ReceiveMode string

const (
    ReceiveModeOff      ReceiveMode = "off"      // reject all
    ReceiveModeContacts ReceiveMode = "contacts"  // auto-accept from authorized peers (default)
    ReceiveModeAsk      ReceiveMode = "ask"       // queue for manual approval
    ReceiveModeOpen     ReceiveMode = "open"      // accept from any authorized peer
    ReceiveModeTimed    ReceiveMode = "timed"     // temporarily open, reverts after duration
)

type TransferPriority

type TransferPriority int

const (
    PriorityLow    TransferPriority = 0
    PriorityNormal TransferPriority = 1
    PriorityHigh   TransferPriority = 2
)

type TransferConfig

type TransferConfig struct {
    ReceiveDir        string              // directory for received files
    MaxSize           int64               // max file size (0 = unlimited)
    ReceiveMode       ReceiveMode         // default: contacts
    Compress          bool                // enable zstd compression (default: true)
    ErasureOverhead   float64             // RS parity overhead (0.10 = 10%, 0 = disabled)
    LogPath           string              // transfer event log path (empty = disabled)
    Notify            string              // "none" (default), "desktop", "command"
    NotifyCommand     string              // command template for "command" mode
    MaxConcurrent     int                 // max concurrent outbound transfers (default: 5)
    MultiPeerEnabled  bool                // enable multi-peer downloads (default: true)
    MultiPeerMaxPeers int                 // max peers for multi-peer (default: 4)
    MultiPeerMinSize  int64               // min file size for multi-peer (default: 10 MB)
    RateLimit         int                 // max requests per peer per minute (default: 600)
    GlobalRateLimit   int                 // max total inbound requests per minute (default: 600)
    MaxQueuedPerPeer  int                 // max pending+active per peer (default: 10)
    MinSpeedBytes     int                 // min transfer speed bytes/sec (default: 1024)
    MinSpeedSeconds   int                 // speed check window seconds (default: 30)
    MaxTempSize       int64               // max total .tmp size (default: 1GB)
    TempFileExpiry    time.Duration       // auto-expire old .tmp files (default: 1h)
    BandwidthBudget   int64               // max bytes per peer per hour (default: 100MB)
    PeerBudgetFunc    func(string) int64  // per-peer budget override (-1=unlimited, 0=global)
    FailureBackoffThreshold int           // fails to trigger block (default: 3)
    FailureBackoffWindow    time.Duration // failure counting window (default: 5m)
    FailureBackoffBlock     time.Duration // block duration (default: 60s)
    QueueFile         string              // persisted queue path (empty = disabled)
    QueueHMACKey      []byte              // 32-byte HMAC key for queue integrity
    GrantChecker      sdk.RelayGrantChecker // relay grant checker for budget/time checks
    ConnsToPeer       func(peer.ID) []network.Conn // returns connections to a peer
    HasVerifiedLANConn func(peer.ID) bool // true if peer has live mDNS-verified LAN connection
}

TransferConfig configures the transfer service. Passed to NewTransferService.

func NewTransferService

func NewTransferService(cfg TransferConfig, metrics *sdk.Metrics, events *sdk.EventBus) (*TransferService, error)

Creates a new chunked transfer service. Returns error if receive directory cannot be created.


type TransferService

type TransferService struct {
    // unexported fields
}

Manages chunked file transfers over libp2p streams. Thread-safe.

Sending

func (ts *TransferService) SendFile(s network.Stream, filePath string, opts ...SendOptions) (*TransferProgress, error)

Sends a file over a libp2p stream. Runs transfer in background goroutine.

func (ts *TransferService) SendDirectory(ctx context.Context, dirPath string, openStream func() (network.Stream, error), opts SendOptions) ([]*TransferProgress, error)

Sends all files in a directory. Opens one stream per file.

func (ts *TransferService) SubmitSend(filePath, peerID string, priority TransferPriority, openStream streamOpener, opts SendOptions) (*TransferProgress, error)

Enqueues outbound transfer to the queue processor.

Receiving

func (ts *TransferService) HandleInbound() sdk.StreamHandler

Returns handler for inbound transfer protocol. Register with libp2p.

func (ts *TransferService) ReceiveFrom(s network.Stream, remotePath, destDir string, sel ...*FileSelection) (*TransferProgress, error)

Initiates receiver-side download from a peer’s shared file or directory. Supports directory downloads (the server walks and streams the directory as a multi-file SHFT transfer). Optional FileSelection enables --files/--exclude selective download.

func (ts *TransferService) ProbeRootHash(openStream func() (network.Stream, error), remotePath string) ([32]byte, error)

Sends hash probe request and reads 45-byte response. Used by multi-peer download. Rejects directory paths (probe requires single-file chunking).

func RequestList(s network.Stream, remotePath string) ([]fileEntry, int64, error)

Sends a list request (requestTypeList = 0x03) and reads the file listing response. Returns the sorted file table matching the order a download would use (same walkDirectoryForTransfer + sortFileTable as SendFile). Used by shurli download --list.

Transfer Management

func (ts *TransferService) GetTransfer(id string) (*TransferProgress, bool)
func (ts *TransferService) ListTransfers() []TransferSnapshot
func (ts *TransferService) CancelTransfer(id string) error
func (ts *TransferService) CleanTempFiles() (int, int64)
func (ts *TransferService) Close() error

Configuration

func (ts *TransferService) SetReceiveMode(mode ReceiveMode)
func (ts *TransferService) SetTimedMode(duration time.Duration) error
func (ts *TransferService) TimedModeRemaining() time.Duration
func (ts *TransferService) GetReceiveMode() ReceiveMode
func (ts *TransferService) SetReceiveDir(dir string)
func (ts *TransferService) SetCompress(enabled bool)
func (ts *TransferService) SetMaxSize(maxBytes int64)
func (ts *TransferService) SetNotifyMode(mode string)
func (ts *TransferService) SetNotifyCommand(cmd string)
func (ts *TransferService) ReceiveDir() string

Multi-Peer Download

type MultiPeerStreamOpener

type MultiPeerStreamOpener func(peerID peer.ID) (network.Stream, error)

Opens a stream to a specific peer for the multi-peer download protocol.

func (ts *TransferService) MultiPeerEnabled() bool
func (ts *TransferService) MultiPeerMaxPeers() int
func (ts *TransferService) MultiPeerMinSize() int64
func (ts *TransferService) HandleMultiPeerRequest() sdk.StreamHandler
func (ts *TransferService) DownloadMultiPeer(ctx context.Context, rootHash [32]byte, peers []peer.ID, openStream MultiPeerStreamOpener, destDir string) (*TransferProgress, error)

Hash Registry

func (ts *TransferService) RegisterHash(rootHash [32]byte, localPath string)
func (ts *TransferService) LookupHash(rootHash [32]byte) (string, bool)

Register hash-to-path mappings for multi-peer serving.

Queue

func (ts *TransferService) LogPath() string
func (ts *TransferService) FlushQueue()
func (ts *TransferService) RequeuePersisted(streamFactory func(peerID string) func() (network.Stream, error))

Ask Mode

func (ts *TransferService) ListPending() []PendingTransfer
func (ts *TransferService) AcceptTransfer(id, dest string, acceptedFiles []int) error
func (ts *TransferService) RejectTransfer(id string, reason byte) error

type TransferProgress

type TransferProgress struct {
    ID              string       `json:"id"`
    Filename        string       `json:"filename"`
    Size            int64        `json:"size"`
    Transferred     int64        `json:"transferred"`
    ChunksTotal     int          `json:"chunks_total"`
    ChunksDone      int          `json:"chunks_done"`
    Compressed      bool         `json:"compressed"`
    CompressedSize  int64        `json:"compressed_size,omitempty"`
    ErasureParity   int          `json:"erasure_parity,omitempty"`
    ErasureOverhead float64      `json:"erasure_overhead,omitempty"`
    StreamProgress  []StreamInfo `json:"stream_progress,omitempty"`
    PeerID          string       `json:"peer_id"`
    Direction       string       `json:"direction"`
    Status          string       `json:"status"`
    StartTime       time.Time    `json:"start_time"`
    Done            bool         `json:"done"`
    Error           string       `json:"error,omitempty"`
}

Tracks progress of an active transfer.

func (p *TransferProgress) Snapshot() TransferSnapshot
func (p *TransferProgress) Sent() int64

type TransferSnapshot

Same fields as TransferProgress plus:

PendingFiles []PendingFileInfo `json:"pending_files,omitempty"`

Populated for transfers with status awaiting_approval. Contains the per-file list for selective rejection. Mutex-free copy safe for JSON serialization and API responses.

type StreamInfo

type StreamInfo struct {
    ChunksDone int   `json:"chunks_done"`
    BytesDone  int64 `json:"bytes_done"`
}

Per-stream progress for parallel transfers.

type PendingTransfer

type PendingTransfer struct {
    ID       string    `json:"id"`
    Filename string    `json:"filename"`
    Size     int64     `json:"size"`
    PeerID   string    `json:"peer_id"`
    Time     time.Time `json:"time"`

    // unexported: files []fileEntry, hasErasure bool, decision chan
}

Inbound transfer awaiting approval in ask mode. Internal fields store the per-file table from the wire header (used by AcceptTransfer to build selective accept bitfields) and whether the sender uses erasure coding (gates selective rejection).

type FileSelection

type FileSelection struct {
    Include []int // accept ONLY these file indices (0-indexed)
    Exclude []int // accept all EXCEPT these file indices (0-indexed)
}

Specifies which files to include or exclude in a download. Nil means all files. Include and Exclude are mutually exclusive. Passed as optional variadic to ReceiveFrom.

func (s *FileSelection) resolve(fileCount int) ([]int, error)

Converts Include/Exclude into accepted 0-indexed file indices. Returns nil for full accept. Returns error for out-of-range indices.

type SendOptions

type SendOptions struct {
    NoCompress           bool         // disable compression for this transfer
    Streams              int          // parallel stream count (0 = adaptive default based on transport)
    StreamOpener         streamOpener // opens additional streams for parallel transfer
    RelativeName         string       // override manifest filename (for directory transfer)
    RateLimitBytesPerSec int64        // per-transfer send rate limit (0 = use service default)
    SkipHidden           bool         // skip dot-prefixed files/dirs in directory walks (#41)
    OnComplete           func()       // called when send goroutine exits (serving concurrency tracking)
}

SkipHidden causes directory walks to skip hidden files (.env, .gitignore) and hidden directories (.git/, .ssh/) entirely via filepath.SkipDir. Used by the download serving path (HandleDownload) to prevent accidental data leakage from shared directories. Does not affect single-file shares.

OnComplete is called in a defer when the SendFile goroutine exits. Used by HandleDownload to track active download-serving goroutine lifetime for concurrency limiting.


type ShareRegistry

type ShareRegistry struct {
    // unexported fields
}

Manages shared paths with per-peer ACLs. Thread-safe. Persistent shares survive restarts.

func NewShareRegistry

func NewShareRegistry() *ShareRegistry

Creates an empty share registry.

func LoadShareRegistry

func LoadShareRegistry(path string) (*ShareRegistry, error)

Loads persistent shares from JSON file with HMAC verification.

Configuration

func (r *ShareRegistry) SetPersistPath(path string)
func (r *ShareRegistry) SetHMACKey(key []byte)
func (r *ShareRegistry) SetBrowseRateLimit(maxPerMin int)

Share Management

func (r *ShareRegistry) Share(path string, peers []peer.ID, persistent bool) error
func (r *ShareRegistry) Unshare(path string) error
func (r *ShareRegistry) DenyPeer(path string, peerID peer.ID) error
func (r *ShareRegistry) ListShares(forPeer *peer.ID) []*ShareEntry
func (r *ShareRegistry) LookupShare(path string) (*ShareEntry, bool)
func (r *ShareRegistry) LookupShareByID(shareID string, peerID peer.ID) (*ShareEntry, bool)
func (r *ShareRegistry) IsPathShared(path string, peerID peer.ID) bool
func (r *ShareRegistry) SavePersistent(path string) error

Protocol Handlers

func (r *ShareRegistry) BrowseForPeer(peerID peer.ID) []BrowseEntry
func (r *ShareRegistry) HandleBrowse() sdk.StreamHandler
func (r *ShareRegistry) HandleDownload(ts *TransferService) sdk.StreamHandler

type ShareEntry

type ShareEntry struct {
    ID         string           `json:"id"`
    Path       string           `json:"path"`
    Name       string           `json:"name"`
    Peers      map[peer.ID]bool `json:"-"`
    PeerIDs    []string         `json:"peers"`
    Persistent bool             `json:"persistent"`
    SharedAt   time.Time        `json:"shared_at"`
    IsDir      bool             `json:"is_dir"`
}

Single shared path with its ACL. Path is never sent to peers. ID is opaque.

type BrowseEntry

type BrowseEntry struct {
    Name    string `json:"name"`
    Path    string `json:"path"`
    ShareID string `json:"share_id"`
    Size    int64  `json:"size"`
    IsDir   bool   `json:"is_dir"`
    ModTime int64  `json:"mod_time"`
}

Item in browse results. Path is always relative to share root.

type BrowseResult

type BrowseResult struct {
    Entries []BrowseEntry `json:"entries"`
    Error   string        `json:"error,omitempty"`
}

type HashProbeResult

type HashProbeResult struct {
    RootHash   [32]byte
    TotalSize  int64
    ChunkCount uint32
}

Response from hash probe request. Used by multi-peer download to discover Merkle root hash.

Client Functions

func BrowsePeer(s network.Stream, subPath string) (*BrowseResult, error)
func RequestDownload(s network.Stream, remotePath string) (io.Reader, error)
func RequestProbe(s network.Stream, remotePath string) (*HashProbeResult, error)

type TransferQueue

type TransferQueue struct {
    // unexported fields
}

Manages ordered transfer execution with priority queue.

func NewTransferQueue(maxActive int) *TransferQueue
func (q *TransferQueue) Enqueue(filePath, peerID, direction string, priority TransferPriority) (string, error)
func (q *TransferQueue) Dequeue() *QueuedTransfer
func (q *TransferQueue) Complete(id string)
func (q *TransferQueue) Requeue(id, filePath, peerID, direction string, priority TransferPriority)
func (q *TransferQueue) Cancel(id string) bool
func (q *TransferQueue) Pending() []*QueuedTransfer
func (q *TransferQueue) ActiveCount() int

type QueuedTransfer

type QueuedTransfer struct {
    ID        string           `json:"id"`
    FilePath  string           `json:"file_path"`
    PeerID    string           `json:"peer_id"`
    Priority  TransferPriority `json:"priority"`
    Direction string           `json:"direction"`
    QueuedAt  time.Time        `json:"queued_at"`
}

type DirectoryTransfer

type DirectoryTransfer struct {
    RootDir   string
    Files     []dirFileEntry
    TotalSize int64
}
func WalkDirectory(dirPath string) (*DirectoryTransfer, error)
func (d *DirectoryTransfer) RegularFiles() []dirFileEntry

type TransferEvent

type TransferEvent struct {
    Timestamp     time.Time `json:"timestamp"`
    EventType     string    `json:"event_type"`
    Direction     string    `json:"direction"`
    PeerID        string    `json:"peer_id"`
    FileName      string    `json:"file_name"`
    FileSize      int64     `json:"file_size,omitempty"`
    BytesDone     int64     `json:"bytes_done,omitempty"`
    Error         string    `json:"error,omitempty"`
    Duration      string    `json:"duration,omitempty"`
    AcceptedFiles int       `json:"accepted_files,omitempty"` // selective rejection: accepted file count
    TotalFiles    int       `json:"total_files,omitempty"`    // selective rejection: total file count
}

Structured log entry for file transfer event.

Event Type Constants

const (
    EventLogRequestReceived   = "request_received"
    EventLogAccepted          = "accepted"
    EventLogRejected          = "rejected"
    EventLogStarted           = "started"
    EventLogProgress25        = "progress_25"
    EventLogProgress50        = "progress_50"
    EventLogProgress75        = "progress_75"
    EventLogCompleted         = "completed"
    EventLogFailed            = "failed"
    EventLogResumed           = "resumed"
    EventLogCancelled         = "cancelled"
    EventLogSpamBlocked       = "spam_blocked"
    EventLogDiskSpaceRejected = "disk_space_rejected"
    EventLogMultiPeerRejected = "multi_peer_rejected"
    EventLogPathFailover      = "path_failover"
)

type TransferLogger

type TransferLogger struct {
    // unexported fields
}

JSON-lines logger for transfer events. Rotation: 10 MB per file, 3 rotated files.

func NewTransferLogger(path string) (*TransferLogger, error)
func (l *TransferLogger) Log(event TransferEvent)
func (l *TransferLogger) Close() error
func ReadTransferEvents(path string, max int) ([]TransferEvent, error)

type TransferNotifier

type TransferNotifier struct {
    // unexported fields
}

Sends notifications on incoming file transfers. Modes: “none” (default), “desktop” (OS-native), “command” (user template).

func NewTransferNotifier(mode, command string) *TransferNotifier
func (n *TransferNotifier) SetMode(mode string)
func (n *TransferNotifier) SetCommand(cmd string)
func (n *TransferNotifier) Notify(from, fileName string, fileSize int64) error

type Chunk

type Chunk struct {
    Data   []byte
    Hash   [32]byte
    Offset int64
}

Single content-defined chunk with BLAKE3 hash.

func ChunkTarget(fileSize int64) (minSize, avgSize, maxSize int)

Selects adaptive chunk sizes (min/avg/max) based on file size across 5 tiers: 64K/128K/256K for <64MB, 128K/256K/512K for <512MB, 256K/512K/1M for <2GB, 512K/1M/2M for <8GB, 1M/2M/4M for >=8GB.

func ChunkReader(r io.Reader, fileSize int64, cb func(Chunk) error) error

Reads from reader and produces content-defined chunks using FastCDC with BLAKE3. Single-pass: each byte is hashed as the chunk boundary is found.


Utility Functions

func RejectReasonString(reason byte) string

Returns human-readable string for reject reason byte.

func SanitizeDisplayName(name string) string

Sanitizes filename for safe terminal display, stripping ANSI escapes, BiDi overrides, and zero-width characters.


HTTP API Types

Request and response types used by the daemon HTTP API for file transfer endpoints.

type SendRequest struct {
    Path       string `json:"path"`
    Peer       string `json:"peer"`
    NoCompress bool   `json:"no_compress"`
    Streams    int    `json:"streams"`
    Priority   string `json:"priority"`
    RateLimit  string `json:"rate_limit"` // send rate limit e.g. "100M" (empty = service default)
}

type SendResponse struct {
    TransferID string `json:"transfer_id"`
    Filename   string `json:"filename"`
    Size       int64  `json:"size"`
    PeerID     string `json:"peer_id"`
}

type TransferAcceptRequest struct {
    Dest    string `json:"dest,omitempty"`    // override receive directory
    Files   []int  `json:"files,omitempty"`   // 0-indexed: accept ONLY these files (nil = all)
    Exclude []int  `json:"exclude,omitempty"` // 0-indexed: accept all EXCEPT these (nil = none)
}

type TransferRejectRequest struct {
    Reason string `json:"reason,omitempty"`
}

type PendingTransferInfo struct {
    ID         string            `json:"id"`
    Filename   string            `json:"filename"`
    Size       int64             `json:"size"`
    PeerID     string            `json:"peer_id"`
    Time       string            `json:"time"`
    FileCount  int               `json:"file_count"`            // total files in transfer
    Files      []PendingFileInfo `json:"files,omitempty"`       // per-file info for selective rejection
    HasErasure bool              `json:"has_erasure,omitempty"` // sender uses erasure coding (gates selective rejection)
}

type PendingFileInfo struct {
    Index int    `json:"index"` // 0-indexed position in file table
    Path  string `json:"path"`  // relative path (sanitized)
    Size  int64  `json:"size"`  // file size in bytes
}

type ShareRequest struct {
    Path       string   `json:"path"`
    Peers      []string `json:"peers,omitempty"`
    Persistent *bool    `json:"persistent,omitempty"`
}

type UnshareRequest struct {
    Path string `json:"path"`
}

type ShareDenyRequest struct {
    Path string `json:"path"` // shared path
    Peer string `json:"peer"` // peer name or ID to remove
}

type ShareInfo struct {
    Path       string   `json:"path"`
    Peers      []string `json:"peers,omitempty"`
    Persistent bool     `json:"persistent"`
    IsDir      bool     `json:"is_dir"`
    SharedAt   string   `json:"shared_at"`
}

type BrowseRequest struct {
    Peer    string `json:"peer"`
    SubPath string `json:"sub_path,omitempty"`
}

type BrowseResponse struct {
    Entries []BrowseEntry `json:"entries"`
    Error   string        `json:"error,omitempty"`
}

type DownloadRequest struct {
    Peer       string   `json:"peer"`
    RemotePath string   `json:"remote_path"`
    LocalDest  string   `json:"local_dest"`
    MultiPeer  bool     `json:"multi_peer,omitempty"`
    ExtraPeers []string `json:"extra_peers,omitempty"`
    Files      []int    `json:"files,omitempty"`   // 0-indexed: download ONLY these files
    Exclude    []int    `json:"exclude,omitempty"` // 0-indexed: download all EXCEPT these
    List       bool     `json:"list,omitempty"`    // list files without downloading (#41)
}

type DownloadResponse struct {
    TransferID string `json:"transfer_id"`
    FileName   string `json:"filename"`
    FileSize   int64  `json:"file_size"`
    PeersUsed  int    `json:"peers_used,omitempty"` // multi-peer peer count
}

type ListFilesResponse struct {
    Files     []ListFileEntry `json:"files"`
    TotalSize int64           `json:"total_size"`
}

type ListFileEntry struct {
    Index int    `json:"index"` // 1-indexed
    Path  string `json:"path"`
    Size  int64  `json:"size"`
}

Security Limits

const (
    maxFilenameLen         = 4096     // max filename length in bytes
    maxFileSize            = 1 << 40  // 1 TB max single file
    maxChunkCount          = 1 << 20  // 1M chunks max per transfer
    maxManifestSize        = 40 << 20 // 40 MB max manifest wire size
    maxChunkWireSize       = 4 << 20  // 4 MB max single chunk on wire
    maxDecompressedChunk   = 8 << 20  // 8 MB max decompressed chunk
    maxFileCount           = 65535    // max files per transfer (uint16 wire format)
    maxConcurrentTransfers = 10       // global inbound transfer limit
    maxPerPeerTransfers    = 3        // per-peer inbound limit
    maxTrackedTransfers    = 10000    // max tracked transfer entries
    maxGlobalServing       = 10       // global download-serving goroutine limit (#41)
    maxPerPeerServing      = 3        // per-peer download-serving limit (#41)
)