nautd/webui: scripting, labels, settings, set-location, pause fix

Session checkpoint on webui-plugin:
- engine dump (nautctl dump) + engine endgame integration
- per-file move locations persistence; torrent-level "Set location"
  with reset/keep-relative/leave-separate handling + residual prune
- Lua: naut.get_labels, define_settings/get_setting (script_host struct)
- daemon-owned labels (category+tags) + taxonomy persistence; webui write-through
- fix: pausing a completed/seeding torrent now sticks (stop wins over result)
- automation tab responsive layout; anime_sort label gating + settings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-21 23:19:41 -04:00
parent 6dc711cf57
commit b633b7d216
40 changed files with 3305 additions and 3267 deletions

View file

@ -1,4 +1,5 @@
/* dht.h - BEP-5 KRPC codec and bounded IPv4 get_peers traversal. */
/* dht.h - bounded IPv4 BEP-5 get_peers traversal (KRPC codec from
* torrent-tracker; iterative walk + UDP socket in src/discovery). */
#ifndef NAUT_DHT_H
#define NAUT_DHT_H
@ -9,54 +10,6 @@
#define NAUT_DHT_MAX_NODES 256
#define NAUT_DHT_MAX_PEERS 256
typedef struct {
uint8_t id[NAUT_DHT_ID_LEN];
uint8_t ip[4];
uint16_t port;
} naut_dht_node;
typedef enum {
NAUT_DHT_RESPONSE,
NAUT_DHT_ERROR
} naut_dht_message_type;
typedef struct {
naut_dht_message_type type;
uint8_t transaction[8];
size_t transaction_len;
uint8_t id[NAUT_DHT_ID_LEN];
bool has_id;
uint8_t token[64];
size_t token_len;
naut_dht_node *nodes;
size_t num_nodes;
naut_peer_addr *peers;
size_t num_peers;
int error_code;
} naut_dht_response;
naut_err naut_dht_build_ping(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
uint8_t **out, size_t *out_len);
naut_err naut_dht_build_find_node(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t target[20],
uint8_t **out, size_t *out_len);
naut_err naut_dht_build_get_peers(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t info_hash[20],
uint8_t **out, size_t *out_len);
naut_err naut_dht_build_announce_peer(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t info_hash[20],
uint16_t port, bool implied_port,
const void *token, size_t token_len,
uint8_t **out, size_t *out_len);
naut_err naut_dht_parse_response(const uint8_t *data, size_t len,
naut_dht_response *out);
void naut_dht_response_free(naut_dht_response *response);
/* Query bootstrap endpoints ("host:port") and iteratively follow returned
* compact nodes until peers are found or the bounded traversal is exhausted. */
naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,

View file

@ -1,88 +0,0 @@
/* mse.h - BitTorrent Message Stream Encryption (MSE/PE) transport. */
#ifndef NAUT_MSE_H
#define NAUT_MSE_H
#include "naut/common.h"
#include "naut/peer.h"
#include "naut/rc4.h"
#include <sys/types.h>
#define NAUT_MSE_DH_LEN 96
typedef struct {
naut_rc4 send;
naut_rc4 recv;
bool active;
} naut_mse_stream;
/* ---- sans-IO handshake state machine ------------------------------------- *
* The outgoing MSE/PE handshake as a pure state machine over byte buffers no
* sockets so the same logic drives the blocking apps and the io_uring reactor
* (where blocking in a handshake would stall a whole core's worth of peers).
*
* Drive it like a codec: pump NEED_WRITE bytes out, feed NEED_READ bytes in,
* repeat until DONE or ERROR, then call _finish().
*
* h = naut_mse_handshake_begin(info_hash, peer_id, reserved);
* for (;;) switch (naut_mse_handshake_status(h)) {
* case NAUT_MSE_HS_NEED_WRITE: pull bytes, write them to the peer; break;
* case NAUT_MSE_HS_NEED_READ: read bytes from the peer, feed them; break;
* case NAUT_MSE_HS_DONE: naut_mse_handshake_finish(h, ...); goto ok;
* case NAUT_MSE_HS_ERROR: ... ; goto err;
* }
*/
typedef enum {
NAUT_MSE_HS_NEED_READ,
NAUT_MSE_HS_NEED_WRITE,
NAUT_MSE_HS_DONE,
NAUT_MSE_HS_ERROR,
} naut_mse_hs_status;
typedef struct naut_mse_handshake naut_mse_handshake;
naut_mse_handshake *naut_mse_handshake_begin(
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved);
void naut_mse_handshake_free(naut_mse_handshake *h);
naut_mse_hs_status naut_mse_handshake_status(const naut_mse_handshake *h);
/* Copy pending outgoing bytes into buf (up to cap); returns the count, 0 when
* nothing is queued. Call repeatedly until it returns 0. */
size_t naut_mse_handshake_pull(naut_mse_handshake *h, uint8_t *buf, size_t cap);
/* Feed received bytes; *consumed reports how many were absorbed (the rest, if
* any, must be re-fed after DONE that remainder is the start of the encrypted
* payload stream). Returns the new status. */
naut_mse_hs_status naut_mse_handshake_feed(naut_mse_handshake *h,
const uint8_t *data, size_t len,
size_t *consumed);
/* Valid once status is DONE: hand out the negotiated stream and the peer's
* decrypted BitTorrent handshake. */
naut_err naut_mse_handshake_finish(naut_mse_handshake *h,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]);
/* Blocking convenience wrapper over the state machine: perform the whole
* outgoing handshake on a blocking socket, offering RC4 only. The BitTorrent
* handshake is carried as IA; the peer's decrypted handshake is returned in
* remote_handshake. */
naut_err naut_mse_client_handshake(
int fd,
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]);
/* Stream I/O after a successful handshake. Encryption/decryption is in-place
* with connection-owned RC4 state. send_all preserves the caller's buffer. */
bool naut_mse_send_all(int fd, naut_mse_stream *stream,
const void *data, size_t len);
ssize_t naut_mse_recv(int fd, naut_mse_stream *stream,
void *data, size_t len);
#endif /* NAUT_MSE_H */

View file

@ -13,11 +13,17 @@
#include "naut/bitfield.h"
#include "naut/worker.h"
#include <stdio.h>
typedef struct naut_download naut_download;
naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st);
void naut_download_destroy(naut_download *d);
/* Scan existing storage and mark SHA-1 verified pieces complete before
* requesting from peers. Invalid or missing pieces are left for download. */
naut_err naut_download_resume(naut_download *d);
/* Optional hash offload. Completed-piece SHA-1 jobs run on the worker pool;
* naut_download_poll() finalizes verified pieces on the owning engine thread.
* The pool must outlive the download. */
@ -90,4 +96,11 @@ uint64_t naut_download_bytes_done(const naut_download *d);
size_t naut_download_piece_states(const naut_download *d, uint8_t *out,
size_t capacity);
/* Diagnostic: write a human-readable dump of block-assembly state to `out` —
* overall progress plus, for every piece not yet verified, how many of its
* blocks have arrived and how many requests are outstanding. Pairs with
* engine_dump_torrent() (which covers piece selection across peers) to
* investigate pieces that never finish downloading. */
void naut_download_dump(const naut_download *d, FILE *out);
#endif /* NAUT_PIECE_H */

View file

@ -1,29 +0,0 @@
/* pipeline.h - adaptive request window based on observed bandwidth-delay product. */
#ifndef NAUT_PIPELINE_H
#define NAUT_PIPELINE_H
#include "naut/common.h"
typedef struct {
double rtt_seconds;
double bytes_per_second;
double last_sample_at;
uint32_t depth;
uint32_t min_depth;
uint32_t max_depth;
uint32_t block_size;
} naut_pipeline;
void naut_pipeline_init(naut_pipeline *p, uint32_t block_size,
uint32_t min_depth, uint32_t max_depth,
uint32_t initial_depth);
/* Record one completed request. sent_at and received_at are monotonic seconds.
* The controller smooths RTT and delivery rate, then targets 2x BDP to absorb
* scheduling jitter without allowing an unbounded request window. */
void naut_pipeline_on_block(naut_pipeline *p, uint32_t bytes,
double sent_at, double received_at);
uint32_t naut_pipeline_depth(const naut_pipeline *p);
#endif /* NAUT_PIPELINE_H */

View file

@ -11,6 +11,50 @@ typedef naut_err (*naut_script_move_file_cb)(void *context,
uint32_t file_index,
const char *destination);
/* Resolve a torrent's labels for `naut.get_labels(id)`. Returns a heap array of
* `*count` heap strings (caller frees each string then the array), or NULL with
* *count==0 if the torrent has no labels / is unknown. */
typedef char **(*naut_script_labels_cb)(void *context, uint64_t torrent_id,
size_t *count);
/* One user-configurable setting a script declares via naut.define_settings. */
typedef struct {
const char *key; /* stable identifier read by naut.get_setting */
const char *label; /* human label for the web UI form */
const char *type; /* "string" | "bool" | "number" */
const char *default_value; /* stringified default ("true"/"false" for bool)*/
} naut_script_setting_def;
/* The script (re)declared its settings schema. The host stores it and renders a
* form; `defs` is valid only for the duration of the call. */
typedef void (*naut_script_define_settings_cb)(void *context,
const naut_script_setting_def *defs,
size_t count);
/* Setting value kinds, so the Lua side can push the right type. */
typedef enum {
NAUT_SETTING_STRING = 0,
NAUT_SETTING_BOOL = 1,
NAUT_SETTING_NUMBER = 2,
} naut_setting_type;
/* Resolve a setting for `naut.get_setting(key)`: the user-set value if present,
* else the declared default. Returns a heap string (caller frees) and sets
* *type, or NULL if the key is unknown. */
typedef char *(*naut_script_get_setting_cb)(void *context, const char *key,
naut_setting_type *type);
/* Host callbacks the sandboxed script may invoke. Any may be NULL (the matching
* naut.* function then reports it is unavailable). `context` is passed back to
* each callback. */
typedef struct {
naut_script_move_file_cb move_file;
naut_script_labels_cb labels;
naut_script_define_settings_cb define_settings;
naut_script_get_setting_cb get_setting;
void *context;
} naut_script_host;
typedef struct {
uint64_t queued;
uint64_t handled;
@ -20,12 +64,12 @@ typedef struct {
} naut_script_stats;
/* script_path is loaded before the worker starts. queue_capacity bounds copied
* events and must be non-zero. The VM owns no filesystem or process APIs. */
* events and must be non-zero. The VM owns no filesystem or process APIs. `host`
* is copied; its callbacks are invoked from the script worker thread. */
naut_script *naut_script_create(naut_event_bus *events,
const char *script_path,
size_t queue_capacity,
naut_script_move_file_cb move_file,
void *move_context,
const naut_script_host *host,
naut_err *error);
void naut_script_destroy(naut_script *script);

View file

@ -18,6 +18,12 @@ typedef struct naut_storage naut_storage;
typedef struct {
bool direct_io;
bool preallocate;
/* Optional per-file path overrides, e.g. files moved out of `root` on a
* prior run. If non-NULL the array has one entry per file: where overrides[i]
* is non-NULL the file is opened at that path instead of `root`/<rel-path>,
* so a relocated file is picked up in place (no re-download, no placeholder
* recreated under `root`). A NULL entry uses the default location. */
const char *const *overrides;
} naut_storage_opts;
/* Open (creating + preallocating) all files under `root`. */

View file

@ -58,6 +58,7 @@ typedef struct {
uint32_t peers_connecting;
uint32_t peers_active;
uint32_t peers_failed;
bool stalled;
double elapsed_seconds;
uint32_t peer_count;
naut_swarm_peer_stats peer_stats[NAUT_SWARM_MAX_PEER_STATS];
@ -78,17 +79,42 @@ typedef void (*naut_swarm_control_cb)(void *context, naut_storage *storage);
typedef bool (*naut_swarm_stop_cb)(void *context);
/* Optional: return the desired engine-wide download cap in bytes/sec (0 =
* unlimited). Polled on the swarm owner thread; the engine limit is updated
* whenever the returned value changes. */
typedef uint64_t (*naut_swarm_rate_cb)(void *context);
/* Optional diagnostics. should_dump is polled on the swarm owner thread; when it
* returns true the swarm renders a full engine + piece-assembly state dump and
* hands the text to on_dump (also on the owner thread, where engine and download
* state can be read safely). Used by `nautctl dump` to investigate why a few
* pieces never finish downloading. */
typedef bool (*naut_swarm_dump_cb)(void *context);
typedef void (*naut_swarm_dump_sink)(void *context, const char *text);
/* A file's last known on-disk location, from a relocate on a prior run. Passed
* back in so the file is reopened in place instead of re-downloaded. */
typedef struct {
uint32_t file_index;
const char *path;
} naut_swarm_file_location;
typedef struct {
const char *source; /* .torrent path or magnet URI */
const char *output_dir;
const char *const *peers; /* optional explicit ip:port endpoints */
size_t num_peers;
const naut_swarm_file_location *locations; /* optional moved-file locations */
size_t num_locations;
uint64_t torrent_id;
naut_event_bus *events; /* optional */
bool keep_alive; /* retain completed storage until stopped */
naut_swarm_progress_cb on_progress;
naut_swarm_control_cb on_control;
naut_swarm_stop_cb should_stop;
naut_swarm_rate_cb download_rate; /* optional download throttle provider */
naut_swarm_dump_cb should_dump; /* optional state-dump request poll */
naut_swarm_dump_sink on_dump; /* optional rendered-dump sink */
void *context;
} naut_swarm_config;

View file

@ -1,9 +1,9 @@
/* tracker.h — HTTP and UDP tracker clients (BEP-3/BEP-23, BEP-15).
/* tracker.h — HTTP and UDP tracker announce client.
*
* Split into pure codec (URL building, bencode response parsing, UDP packet
* encode/decode all unit-testable without a socket) and thin blocking fetch
* helpers used by the swarm app. HTTPS/TLS is deferred to a later transport
* backend; the built-ins in this phase are plaintext HTTP and UDP.
* The wire codec (query building, bencode/UDP packet encode+decode) lives in the
* sibling `torrent-tracker` library; the implementation here (src/discovery)
* owns only the socket glue. HTTPS/TLS is deferred to a later transport backend;
* the built-ins are plaintext HTTP and UDP.
*/
#ifndef NAUT_TRACKER_H
#define NAUT_TRACKER_H
@ -37,25 +37,10 @@ typedef struct {
void naut_tracker_response_free(naut_tracker_response *r);
/* --- HTTP --- */
/* Build the full announce GET URL (base?...params) with percent-encoded binary
* info_hash/peer_id. Returns bytes written (excl NUL) or 0 on overflow. */
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
char *out, size_t outsz);
/* Parse a bencoded HTTP tracker response body (compact or dict peer list). */
naut_err naut_tracker_parse_http(const uint8_t *body, size_t len,
naut_tracker_response *out);
/* --- UDP (BEP-15): pure packet codec --- */
#define NAUT_UDP_CONNECT_REQ_LEN 16
#define NAUT_UDP_ANNOUNCE_REQ_LEN 98
void naut_udp_build_connect(uint8_t out[16], uint32_t txid);
naut_err naut_udp_parse_connect(const uint8_t *in, size_t len, uint32_t txid,
uint64_t *connection_id);
void naut_udp_build_announce(uint8_t out[98], uint64_t connection_id,
uint32_t txid, const naut_announce_req *req);
naut_err naut_udp_parse_announce(const uint8_t *in, size_t len, uint32_t txid,
naut_tracker_response *out);
/* --- live fetch helpers (blocking) --- */
/* HTTP GET the announce URL; fills out. Only http:// (no TLS yet). */