Files
Jamie Pineandcapy-ai-staging[bot] 10dbc18d0f Replica fetch: resumable .part transfers, bandwidth cap, pause, and progress (#3109)
* Resume interrupted replica fetches from a .part file

A replica transfer now lands in <source>.<generation>.<blake3 prefix>.part
and stays there when the stream breaks or the daemon stops. The next
attempt asks the owner for the artifact's tail from the part's length over
the new FetchSnapshotFrom and FetchDatabaseFrom requests, whose header
still names the whole artifact; a header that names different bytes
discards the part and starts over, and the finished file is validated
against the header checksum before it replaces the live replica as before.
Parts are removed only when the owner's listing no longer carries their
generation. A nonzero offset serves the on-disk snapshot without the
pre-fetch save, so a resumed copy finishes against the same bytes.

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Cap replica transfer bandwidth with a persisted daemon setting

replication.max_bytes_per_sec in spacedrive.json bounds every replica
artifact served and received through one token bucket (zero is
unlimited); the cap applies to transfers already in flight. It is set
through config.app.update and sd config set replication.max_bytes_per_sec
(bytes, or K/M/G suffixes), read back through sd config get and show.

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Pause and resume replica fetching

sd sources replication pause stops this device starting replica
transfers and halts any in flight at its next chunk, keeping the partial
file; resume continues from it and runs a replication pass against every
connected device at once. The switch is replication.paused in the daemon
config, so a paused daemon stays paused across a restart, and a pass that
runs while paused still refreshes listings, owner facts and fleet totals.
A pause does not count as a fetch failure, so no backoff accrues.

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Show replica fetch progress in sources list, status and events

Every fetch in flight is tracked with bytes on disk, total and a recent
rate. sources.list carries it on the replica's row (or a replica_fetching
row for a first copy that has no share yet), core.status and the new
mounts.replication_status query summarize it with the pause switch and
cap, and the daemon emits a typed ReplicationProgress event about once a
second per transfer plus a finished one when it stops. sd sources list and
sd status print the figures; ts-client types regenerated.

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Document resumable replica fetch, the cap, pause and progress

Source replication gets its own section in networking.mdx with the .part
contract, the wire change, the cap, the pause switch and the progress
fields; file-sync.mdx points there; the replication plan marks its cleanup
item superseded on the replica side and distinct from the restart refetch;
PROJECT_STATUS.md gains the register row.

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Keep the part when a resumed request gets no header; cap sidecar pages

A resumed request that fails outright (dropped link, or an owner too old
to know the offset variants) says nothing about the bytes on disk, so the
part now stays and the whole-file retry lands on the same name. Sidecar
pages count against the bandwidth cap on both ends, as the setting's
contract says.

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Add the replication field to the test harness config

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

* Check the pause switch after the cap's wait, not before

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
2026-10-05 11:40:19 +00:00

229 lines
8.9 KiB
Plaintext

---
title: Networking
description: Direct encrypted transport for paired Spacedrive nodes
sidebarTitle: Networking
---
Spacedrive connects trusted devices through Iroh over QUIC. It attempts a
direct path first and uses a relay when network topology requires one. The
transport carries typed protocols for operations, source queries, byte ranges,
sync, and device presence.
A relay forwards encrypted traffic. It does not become a library authority or
receive permission to inspect a source.
## Network identity
Each daemon has a persistent cryptographic node identity. Pairing associates
that identity with a library device record and explicit grants.
Network discovery and authorization are separate:
- Discovery answers how to reach a node.
- Authentication proves which node connected.
- Authorization decides which libraries, sources, subtrees, and operations it
may use.
Seeing a node on the local network never grants file access.
## Reachability
Spacedrive combines:
- local network discovery for nearby nodes;
- direct QUIC connections when addresses permit them;
- internet discovery for a known node identity;
- relay fallback when NAT or firewalls block a direct path.
Connections can migrate between viable paths without changing device identity
or operation semantics.
## Protocol registry
Each protocol has a stable identifier, version, request limits, and
authorization rule. The network service routes an accepted stream to its
registered handler.
Core protocols include:
| Protocol | Purpose |
| --- | --- |
| Pairing | Establish and confirm device trust |
| Operations | Run scoped queries and actions |
| Source query | Browse or search a served source generation |
| Byte range | Stream original files and sidecars |
| Library sync | Exchange small shared state and assertions |
| Source sync | Replicate an authority-owned generation |
| Presence | Report device and source availability |
| Spacedrop | Offer an explicit file transfer |
Protocol versions are negotiated before application data is processed. An
unsupported version fails clearly instead of decoding with the wrong schema.
## Remote operations
Remote operation requests use the same registry as local clients. The serving
node creates a request context from the paired identity and grant, dispatches
the operation, and returns structured output or a typed error.
Long work remains a durable job on the executing node. The caller receives a
job ID and can query progress or logs later, including after reconnecting.
## Source queries
A node can serve one source without sending its entire library database. Query
responses name the source and generation that produced them.
Clients combine local, cached, and live remote results while preserving
completeness:
- complete means all required sources answered;
- partial means one or more sources were offline or denied;
- stale means a retained generation is older than the known authority.
## Byte streaming
The byte-range protocol reads local files, provider objects, caches, and
sidecars through one provider boundary. It supports:
- bounded ranges;
- backpressure;
- cancellation;
- content length and version evidence;
- resuming from verified offsets;
- authorization per request.
Control messages do not carry large file bodies. This keeps queries responsive
while previews, mounts, and transfers stream in parallel.
## Source replication
When a paired device connects, this device copies the index of every source
the peer serves into `mounts-remote/<device>/` and loads it as a read-only
replica. The copy is an arena snapshot for a source at its volume root, or a
single-file database export for a nested source. The owner's response header
names the whole artifact: its length, its generation, and the blake3 hash of
its bytes. A delivery must match that hash and parse before it replaces the
replica already published.
### Partial transfers
A transfer lands in `<source>.<generation>.<blake3 prefix>.part` and stays
there when the stream breaks or the daemon stops. The next attempt asks the
owner for the artifact's tail from the part's length (`FetchSnapshotFrom` or
`FetchDatabaseFrom`). The owner answers with the same whole-artifact header
and streams only the bytes past the offset; a resumed snapshot request skips
the owner's pre-fetch save so the bytes stay the ones the part was cut from.
A header whose generation or hash prefix does not match the part discards it
and starts over. A part is removed only when the owner's listing no longer
carries its generation, so an owner that is offline for a week leaves the
part waiting. The finished file is validated against the header hash before
it replaces the live replica.
Both peers must run a build that knows the `From` requests. An owner that
does not answers them with an error, and the receiver starts the transfer
from zero as before.
### Bandwidth cap
`replication.max_bytes_per_sec` in the daemon config bounds replica bytes
moved per second, artifacts and thumbnail sidecar pages, serving and
receiving combined, through one token bucket with a one-second burst. Zero is unlimited. Set it with
`sd config set replication.max_bytes_per_sec 200K` (bytes, or K, M, G in
powers of 1024); the change applies to transfers already in flight. An
initial replica runs to gigabytes and starts as soon as a device connects, so
set the cap before pairing on a metered or shared link.
### Pause
`sd sources replication pause` stops this device starting replica transfers
and halts any in flight at its next chunk, keeping the partial file. The
daemon still refreshes peer listings, owner facts and fleet totals, still
serves its own sources to peers, and still answers remote operations.
`sd sources replication resume` continues each partial file where it stopped
and runs a replication pass against every connected device at once. The
switch is `replication.paused` in the daemon config, so a paused daemon stays
paused across a restart. A pause is not a fetch failure: no backoff accrues.
### Progress
Every fetch in flight reports `bytes` on disk (including what an earlier
attempt left in the part), `total`, a `bytes_per_sec` rate averaged over the
last few seconds, and `resumed_from`. `sources.list` carries the figures on
the replica's row, or on a `replica_fetching` row for a first copy that has
no replica yet. `core.status` and `mounts.replication_status` return the
pause switch, the cap, and every transfer; `sd status` prints them as one
line (`Replicating 2 sources, 553.0 MB of 2.9 GB, 212.0 KB/s`), and
`sd sources list` appends `fetching ...` to the status column. The daemon
emits `ReplicationProgress { transfer }` about once a second per transfer
and once more with `finished: true` when the transfer leaves the active set,
complete or stopped.
## Sync traffic
Library assertions and source generations use separate streams and watermarks.
Source generation follows one ingest authority. Multi-writer assertions merge
according to their type.
Backpressure prevents an initial source replication from starving interactive
queries. Peers can resume from acknowledged watermarks after a disconnect.
File bytes do not replicate merely because metadata synchronized. [File
Sync](/core/file-sync) controls durable byte placement.
## Security
The network boundary enforces:
- authenticated peer identity;
- protocol and message-size limits;
- library membership;
- source and subtree grants;
- query versus mutation permission;
- expiry and revocation;
- request timeouts and cancellation;
- audit context for durable actions.
Provider credentials, key material, and raw database handles never cross the
network protocol.
## Failure behavior
Network loss does not make a durable action silently succeed. The executing
node continues or stops according to the job contract. The caller observes an
unknown connection state until it can query the job again.
Remote reads fail with explicit availability. Cached metadata can remain
browsable, but the interface does not pretend that unavailable bytes are local.
Paired devices reconnect on their own. The device with the lower endpoint ID
dials as soon as a connection drops or the daemon starts; the other side waits
15 seconds for that dial and then dials itself, so a peer whose retry state was
lost (its own restart, a dial that cannot reach us) does not leave the pair
stuck. Each dial has a 30 second bound, retries back off from 2 seconds to 30
seconds for 12 attempts, one loop runs per device, and a 30 second sweep
re-arms any device still unconnected. Shutdown closes the endpoint so peers see
the drop at once rather than at the QUIC idle timeout.
## Observability
Status and logs expose:
- peer identity and connection state;
- direct or relayed path;
- protocol version;
- bytes and active streams;
- reconnect attempts;
- rejected authorization;
- source and job context.
Logs must not contain pairing secrets, bearer credentials, or signed provider
URLs.
## Related systems
- [Device Pairing](/core/pairing)
- [Library Sync](/core/library-sync)
- [API](/core/api)
- [Devices](/core/devices)