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 --jsonshurli 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):
| Mode | Behavior |
|---|---|
off | Reject all incoming transfers |
contacts | Auto-accept from authorized peers (default) |
ask | Queue for manual approval via shurli accept/shurli reject |
open | Accept from any authorized peer without prompting |
timed | Temporarily 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 10mDefault receive directory: ~/Downloads/shurli/
Change it with:
shurli config set transfer.receive_dir /path/to/your/dirIf 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,4Directory 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,laptopEach 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/passwdare 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
| Key | Default | Description |
|---|---|---|
transfer.receive_mode | contacts | Receive mode: off, contacts, ask, open, timed |
transfer.receive_dir | ~/Downloads/shurli/ | Directory for received files |
transfer.compress | true | Enable zstd compression |
transfer.erasure_overhead | 0.1 | Reed-Solomon parity ratio (0.0-0.5) |
transfer.max_concurrent | 5 | Max concurrent outbound transfers |
transfer.max_inbound_transfers | 20 | Max concurrent inbound transfers (global) |
transfer.max_per_peer_transfers | 5 | Max concurrent inbound transfers per peer |
transfer.max_file_size | 0 (unlimited) | Max file size to accept (bytes) |
transfer.timed_duration | 10m | Default duration for timed receive mode |
transfer.notify | none | Notification mode: none, desktop, command |
transfer.notify_command | "" | Command template with {from}, {file}, {size} |
transfer.log_path | ~/.shurli/logs/transfers.log | Transfer event log path |
transfer.multi_peer_enabled | true | Enable multi-peer swarming downloads |
transfer.multi_peer_max_peers | 4 | Max 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() *FileTransferPluginCreates 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:
| Byte | Name | Description |
|---|---|---|
0x01 | requestTypeDownload | Full file/directory download via SHFT streaming |
0x02 | requestTypeProbe | Hash probe for multi-peer coordination (single files only) |
0x03 | requestTypeList | File 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.StreamHandlerReturns 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() errorConfiguration
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() stringMulti-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) errortype 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() int64type 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() *ShareRegistryCreates 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) errorProtocol Handlers
func (r *ShareRegistry) BrowseForPeer(peerID peer.ID) []BrowseEntry
func (r *ShareRegistry) HandleBrowse() sdk.StreamHandler
func (r *ShareRegistry) HandleDownload(ts *TransferService) sdk.StreamHandlertype 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() inttype 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() []dirFileEntrytype 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) errortype 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) errorReads 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) stringReturns human-readable string for reject reason byte.
func SanitizeDisplayName(name string) stringSanitizes 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)
)