mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-31 01:05:57 +00:00
87 lines
6.1 KiB
Markdown
87 lines
6.1 KiB
Markdown
# Simplex.FileTransfer.Agent
|
||
|
||
> XFTP agent: worker-based file send/receive/delete with retry, encryption, redirect chains, and file description generation.
|
||
|
||
**Source**: [`FileTransfer/Agent.hs`](../../../../src/Simplex/FileTransfer/Agent.hs)
|
||
|
||
## Architecture
|
||
|
||
The XFTP agent uses five worker types organized in three categories:
|
||
|
||
| Worker | Key (server) | Purpose |
|
||
|--------|-------------|---------|
|
||
| `xftpRcvWorker` | `Just server` | Download chunks from a specific XFTP server |
|
||
| `xftpRcvLocalWorker` | `Nothing` | Decrypt completed downloads locally |
|
||
| `xftpSndPrepareWorker` | `Nothing` | Encrypt files and create chunks on servers |
|
||
| `xftpSndWorker` | `Just server` | Upload chunks to a specific XFTP server |
|
||
| `xftpDelWorker` | `Just server` | Delete chunks from a specific XFTP server |
|
||
|
||
Workers are created on-demand via `getAgentWorker` and keyed by server address. The local workers (keyed by `Nothing`) handle CPU-bound operations that don't require network access.
|
||
|
||
## Non-obvious behavior
|
||
|
||
### 1. startXFTPWorkers vs startXFTPSndWorkers
|
||
|
||
`startXFTPWorkers` starts all three worker categories (rcv, snd, del). `startXFTPSndWorkers` starts only snd workers. This distinction exists because receiving and deleting require a full agent context, while sending can operate with a partial setup (used when the agent is in send-only mode).
|
||
|
||
### 2. Download completion triggers local worker
|
||
|
||
When `downloadFileChunk` determines that all chunks are received (`all chunkReceived chunks`), it calls `getXFTPRcvWorker True c Nothing` to wake the local decryption worker. The `True` parameter signals that work is available. Without this, the local worker would sleep until the next `waitForWork` check.
|
||
|
||
### 3. Decryption verifies both digest and size before decrypting
|
||
|
||
`decryptFile` first computes the total size of all encrypted chunk files, then their SHA-512 digest. If either mismatches the expected values, it throws an error *before* starting decryption. This prevents wasting CPU on corrupted or tampered downloads.
|
||
|
||
### 4. Redirect chain with depth limit
|
||
|
||
When a received file has a `redirect`, the local worker:
|
||
1. Decrypts the redirect file (a YAML file description)
|
||
2. Validates the inner description's size and digest against `RedirectFileInfo`
|
||
3. Registers the inner file's chunks and starts downloading them
|
||
|
||
The redirect chain is implicitly limited to depth 1: `createRcvFileRedirect` creates the destination file entry with `redirect = Nothing`, and `updateRcvFileRedirect` does not update the redirect column. So even if the decoded inner description contains a redirect field, the database record for the destination file has no redirect, preventing further chaining.
|
||
|
||
### 5. Decrypting worker resumes from RFSDecrypting
|
||
|
||
If the agent restarts while a file is in `RFSDecrypting` status, the local worker detects this and deletes the partially-decrypted output file before restarting decryption. This prevents corrupted output from a previous incomplete decryption attempt.
|
||
|
||
### 6. Encryption worker resumes from SFSEncrypting
|
||
|
||
Similarly, `prepareFile` checks `status /= SFSEncrypted` and deletes the partial encrypted file if status is `SFSEncrypting`. This allows clean restart of interrupted encryption.
|
||
|
||
### 7. Redirect files must be single-chunk
|
||
|
||
`encryptFileForUpload` for redirect files calls `singleChunkSize` instead of `prepareChunkSizes`. If the redirect file description doesn't fit in a single chunk, it throws `FILE SIZE`. This ensures redirect files are atomic — they either download completely or not at all.
|
||
|
||
### 8. addRecipients recursive batching
|
||
|
||
During upload, `addRecipients` recursively calls itself if a chunk needs more recipients than `xftpMaxRecipientsPerRequest`. Each iteration sends an FADD command for up to `maxRecipients` new recipients, accumulates the results, and recurses until all recipients are registered.
|
||
|
||
### 9. File description generation cross-product
|
||
|
||
`createRcvFileDescriptions` (in both `Agent.hs` and `Client/Main.hs`) performs a cross-product transformation: M chunks × R replicas × N recipients → N file descriptions, each containing M chunks with R replicas. The `addRcvChunk` accumulator builds a `Map rcvNo (Map chunkNo FileChunk)` to correctly distribute replicas across recipient descriptions.
|
||
|
||
### 10. withRetryIntervalLimit caps consecutive retries
|
||
|
||
`withRetryIntervalLimit maxN` allows at most `maxN` total attempts (initial attempt at `n=0` plus `maxN-1` retries). When all attempts are exhausted for temporary errors, the operation is silently abandoned for this work cycle — the chunk remains in pending state and may be retried on the next cycle. Only permanent errors (handled by `retryDone`) mark the file as errored.
|
||
|
||
### 11. Retry distinguishes temporary from permanent errors
|
||
|
||
`retryOnError` checks `temporaryOrHostError`: temporary/host errors trigger retry with exponential backoff; permanent errors (AUTH, SIZE, etc.) immediately mark the file as failed. On host errors during retry, a warning notification is sent to the client.
|
||
|
||
### 12. Delete workers skip files older than rcvFilesTTL
|
||
|
||
`runXFTPDelWorker` uses `rcvFilesTTL` (not a dedicated delete TTL) to filter pending deletions. Files older than this TTL would already be expired on the server, so attempting deletion is pointless. This reuses the receive TTL as a proxy for server-side expiration.
|
||
|
||
### 13. closeXFTPAgent atomically swaps worker maps
|
||
|
||
`closeXFTPAgent` uses `swapTVar workers M.empty` to atomically replace each worker map with an empty map, then cancels all retrieved workers. This prevents races where a new worker could be inserted between reading and clearing the map.
|
||
|
||
### 14. assertAgentForeground dual check
|
||
|
||
`assertAgentForeground` both throws if the agent is inactive (`throwWhenInactive`) and blocks until it's in the foreground (`waitUntilForeground`). This is called before every chunk operation to ensure the agent isn't suspended or backgrounded during file transfers.
|
||
|
||
### 15. Per-server stats tracking
|
||
|
||
Every chunk download, upload, and delete operation increments per-server statistics (`downloads`, `uploads`, `deletions`, `downloadAttempts`, `uploadAttempts`, `deleteAttempts`, and error variants). Size-based stats (`downloadsSize`, `uploadsSize`) track throughput in kilobytes.
|