webui: replace nautctl web server with a loadable plugin

Drop the web UI that was compiled into nautctl and serve the
torrent-ui front end (../torrent-ui/public) from a native plugin
(plugins/webui) loaded via `nautd --plugin`. The plugin talks to the
engine only through the host call_rpc ABI and adapts the daemon's RPC
surface to the qBittorrent-style contract the UI expects (snapshot/SSE,
torrent detail tabs, add/delete, cookie auth).

Also folds in the daemon refactor that owns per-torrent worker threads
and the swarm engine (naut_swarm) used by the plugin's data source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-17 00:21:48 -04:00
parent 50a357968a
commit 8dde48c05a
27 changed files with 2706 additions and 403 deletions

View file

@ -18,10 +18,13 @@
#include "naut/log.h"
#include "naut/pipeline.h"
#include "naut/system.h"
#include "naut/swarm.h"
#include "naut/worker.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
@ -33,6 +36,7 @@
#include <netinet/tcp.h>
#define REQUEST_TIMEOUT 15.0
#define CONNECT_TIMEOUT_MS 5000
#define EXT_RESERVED 0x0000000000100000ULL
typedef struct {
@ -61,6 +65,92 @@ typedef struct {
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec*1e-9; }
static void random_bytes(uint8_t *output, size_t length) {
int fd = open("/dev/urandom", O_RDONLY);
size_t offset = 0;
while (fd >= 0 && offset < length) {
ssize_t count = read(fd, output + offset, length - offset);
if (count > 0) {
offset += (size_t)count;
} else if (count < 0 && errno == EINTR) {
continue;
} else {
break;
}
}
if (fd >= 0) close(fd);
uint64_t fallback = (uint64_t)(now() * 1e9) ^
(uint64_t)(uintptr_t)output ^
(uint64_t)getpid();
while (offset < length) {
fallback ^= fallback << 13;
fallback ^= fallback >> 7;
fallback ^= fallback << 17;
output[offset++] = (uint8_t)fallback;
}
}
static void emit_event(const naut_swarm_config *config, naut_event_type type,
uint32_t index, const char *message, const char *path) {
if (!config->events) return;
naut_event event = {
.type = type,
.torrent_id = config->torrent_id,
.index = index,
.message = message,
.path = path,
};
naut_event_emit(config->events, &event);
}
static void on_file_complete(void *opaque, uint32_t index, const char *path) {
const naut_swarm_config *config = opaque;
char full_path[PATH_MAX];
const char *event_path = path;
if (path && path[0] != '/') {
int length = snprintf(full_path, sizeof full_path, "%s/%s",
config->output_dir, path);
if (length >= 0 && (size_t)length < sizeof full_path)
event_path = full_path;
}
emit_event(config, NAUT_EVENT_FILE_COMPLETE, index, NULL, event_path);
}
static void on_piece_complete(void *opaque, uint32_t index) {
const naut_swarm_config *config = opaque;
emit_event(config, NAUT_EVENT_PIECE_COMPLETE, index, NULL, NULL);
}
static void report_progress(const naut_swarm_config *config,
const naut_download *download,
const naut_metainfo *metainfo,
uint32_t peers_total, uint32_t peers_connecting,
uint32_t peers_active, uint32_t peers_failed,
double started_at) {
if (!config->on_progress) return;
naut_swarm_stats stats = {
.total_bytes = (uint64_t)metainfo->total_length,
.bytes_done = download ? naut_download_bytes_done(download) : 0,
.total_pieces = metainfo->num_pieces,
.pieces_done = download ? naut_download_pieces_done(download) : 0,
.peers_total = peers_total,
.peers_connecting = peers_connecting,
.peers_active = peers_active,
.peers_failed = peers_failed,
.elapsed_seconds = now() - started_at,
};
config->on_progress(config->context, &stats);
}
static bool stop_requested(const naut_swarm_config *config) {
return config->should_stop && config->should_stop(config->context);
}
static void service_control(const naut_swarm_config *config,
naut_storage *storage) {
if (config->on_control) config->on_control(config->context, storage);
}
static uint8_t *slurp(const char *path, size_t *len) {
FILE *f = fopen(path, "rb"); if (!f) return NULL;
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
@ -138,6 +228,7 @@ static bool parse_udp_tracker(const char *url, char *host, size_t hostsz,
static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length,
char *const *trackers, size_t num_trackers,
const uint32_t *tracker_tiers,
const uint8_t peerid[20],
endpoint_t **eps, size_t *neps, size_t *cap) {
naut_announce_req req;
@ -148,37 +239,62 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
req.left = total_length;
req.event = NAUT_TEV_STARTED;
req.numwant = 100;
req.key = (uint32_t)rand();
memcpy(&req.key, peerid + 8, sizeof req.key);
for (size_t i = 0; i < num_trackers; i++) {
const char *tracker = trackers[i];
naut_tracker_response response;
naut_err e = NAUT_ERR_INVAL;
if (strncmp(tracker, "http://", 7) == 0) {
char url[4096];
if (naut_tracker_http_url(tracker, &req, url, sizeof url) != 0)
e = naut_tracker_announce_http(url, &response);
} else if (strncmp(tracker, "udp://", 6) == 0) {
char host[256];
uint16_t port;
if (parse_udp_tracker(tracker, host, sizeof host, &port))
e = naut_tracker_announce_udp(host, port, &req, &response);
} else {
NAUT_WARN("tracker scheme unsupported: %s", tracker);
continue;
}
if (e != NAUT_OK) {
NAUT_WARN("tracker announce failed: %s", tracker);
continue;
}
NAUT_INFO("tracker %s returned %zu peers", tracker, response.num_peers);
for (size_t p = 0; p < response.num_peers; p++) {
if (!endpoint_add(eps, neps, cap, &response.peers[p])) {
naut_tracker_response_free(&response);
return false;
size_t tier_start = 0;
while (tier_start < num_trackers) {
uint32_t tier = tracker_tiers
? tracker_tiers[tier_start] : (uint32_t)tier_start;
size_t tier_end = tier_start + 1;
if (tracker_tiers)
while (tier_end < num_trackers &&
tracker_tiers[tier_end] == tier)
tier_end++;
size_t tier_count = tier_end - tier_start;
uint32_t random = 0;
random_bytes((uint8_t *)&random, sizeof random);
size_t first = tier_count ? random % tier_count : 0;
bool tier_succeeded = false;
for (size_t n = 0; n < tier_count; n++) {
size_t i = tier_start + (first + n) % tier_count;
const char *tracker = trackers[i];
naut_tracker_response response;
naut_err e = NAUT_ERR_INVAL;
if (strncmp(tracker, "http://", 7) == 0) {
char url[4096];
if (naut_tracker_http_url(tracker, &req, url,
sizeof url) != 0)
e = naut_tracker_announce_http(url, &response);
} else if (strncmp(tracker, "udp://", 6) == 0) {
char host[256];
uint16_t port;
if (parse_udp_tracker(tracker, host, sizeof host, &port))
e = naut_tracker_announce_udp(host, port, &req, &response);
} else {
NAUT_WARN("tracker scheme unsupported: %s", tracker);
continue;
}
if (e != NAUT_OK) {
NAUT_WARN("tracker announce failed: %s", tracker);
continue;
}
tier_succeeded = true;
NAUT_INFO("tracker %s returned %zu peers",
tracker, response.num_peers);
for (size_t p = 0; p < response.num_peers; p++) {
if (!endpoint_add(eps, neps, cap, &response.peers[p])) {
naut_tracker_response_free(&response);
return false;
}
}
naut_tracker_response_free(&response);
/* Trackers within a tier are alternatives, not a fan-out set.
* Once one accepts the announce, do not load the rest. */
break;
}
naut_tracker_response_free(&response);
if (tier_succeeded) break;
tier_start = tier_end;
}
return true;
}
@ -321,18 +437,81 @@ static void expire_requests(naut_download *d, peer_t *p, double t) {
}
}
static int connect_to(const endpoint_t *ep) {
static int connect_start(const endpoint_t *ep, bool *connected) {
*connected = false;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
close(fd);
return -1;
}
struct sockaddr_in a; memset(&a, 0, sizeof a);
a.sin_family = AF_INET;
a.sin_port = htons(ep->addr.port);
memcpy(&a.sin_addr, ep->addr.ip, sizeof ep->addr.ip);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
if (connect(fd, (struct sockaddr *)&a, sizeof a) == 0) {
*connected = true;
} else if (errno != EINPROGRESS) {
close(fd);
return -1;
}
return fd;
}
static bool connect_finish(int fd) {
int error = 0;
socklen_t length = sizeof error;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) != 0 ||
error != 0)
return false;
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) != 0)
return false;
int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
return true;
}
static bool peer_start(naut_download *download, const naut_metainfo *metainfo,
const uint8_t peerid[20], const endpoint_t *endpoint,
peer_t *peer, int fd) {
peer->fd = fd;
snprintf(peer->name, sizeof peer->name, "%s", endpoint->name);
peer->peer_choking = true;
naut_pipeline_init(&peer->pipeline, NAUT_BLOCK, 4, 1024, 32);
peer->rcap = 1 << 18;
peer->rbuf = malloc(peer->rcap);
if (!peer->rbuf ||
naut_bitfield_init(&peer->have, metainfo->num_pieces) != NAUT_OK) {
free(peer->rbuf);
peer->rbuf = NULL;
close(fd);
peer->fd = -1;
peer->dead = true;
return false;
}
uint8_t handshake[NAUT_HANDSHAKE_LEN];
naut_peer_handshake_build(handshake, metainfo->infohash_v1, peerid,
EXT_RESERVED);
uint8_t interested[5];
naut_peer_msg_simple(interested, NAUT_MSG_INTERESTED);
uint8_t *extension = NULL;
size_t extension_length = 0;
naut_err extension_error = naut_ext_build_handshake(
NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, 0, 0, &extension,
&extension_length);
bool sent = extension_error == NAUT_OK &&
send_all(fd, handshake, sizeof handshake) &&
send_all(fd, extension, extension_length) &&
send_all(fd, interested, sizeof interested);
free(extension);
if (!sent) {
peer_drop(download, peer);
return false;
}
return true;
}
/* process all complete messages currently buffered for peer p */
static void cancel_block(naut_download *d, peer_t *peers, int npeers,
peer_t *source, uint32_t piece, uint32_t begin) {
@ -481,50 +660,46 @@ parsed:
return NAUT_OK;
}
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr,
"usage: %s <file.torrent|magnet-uri> <out-dir> [ip:port ...]\n",
argv[0]);
return 2;
}
naut_log_set_level(NAUT_LOG_INFO);
naut_err naut_swarm_run(const naut_swarm_config *config) {
if (!config || !config->source || !*config->source ||
!config->output_dir || !*config->output_dir)
return NAUT_ERR_INVAL;
uint8_t peerid[20]; memcpy(peerid, "-NT0001-", 8);
srand((unsigned)time(NULL) ^ (unsigned)getpid());
for (int i = 8; i < 20; i++) peerid[i] = (uint8_t)(rand() & 0xff);
uint8_t peerid[20];
memcpy(peerid, "-NT0001-", 8);
random_bytes(peerid + 8, sizeof peerid - 8);
bool from_magnet = strncmp(argv[1], "magnet:?", 8) == 0;
bool from_magnet = strncmp(config->source, "magnet:?", 8) == 0;
naut_metainfo mi;
memset(&mi, 0, sizeof mi);
naut_magnet magnet;
memset(&magnet, 0, sizeof magnet);
if (from_magnet) {
if (naut_magnet_parse(argv[1], &magnet) != NAUT_OK ||
if (naut_magnet_parse(config->source, &magnet) != NAUT_OK ||
!magnet.has_v1) {
NAUT_ERROR("magnet must contain a v1 btih hash");
naut_magnet_free(&magnet);
return 1;
return NAUT_ERR_INVAL;
}
} else {
size_t tlen;
uint8_t *tor = slurp(argv[1], &tlen);
if (!tor) { NAUT_ERROR("read torrent"); return 1; }
uint8_t *tor = slurp(config->source, &tlen);
if (!tor) { NAUT_ERROR("read torrent"); return NAUT_ERR_IO; }
if (naut_metainfo_parse(tor, tlen, &mi) != NAUT_OK) {
NAUT_ERROR("parse torrent");
free(tor);
return 1;
return NAUT_ERR_PROTO;
}
free(tor);
}
endpoint_t *endpoints = NULL;
size_t neps = 0, epcap = 0;
if (argc > 3) {
for (int i = 3; i < argc; i++) {
if (config->num_peers > 0) {
for (size_t i = 0; i < config->num_peers; i++) {
naut_peer_addr addr;
if (!endpoint_parse(argv[i], &addr)) {
NAUT_WARN("invalid peer address: %s", argv[i]);
if (!endpoint_parse(config->peers[i], &addr)) {
NAUT_WARN("invalid peer address: %s", config->peers[i]);
continue;
}
if (!endpoint_add(&endpoints, &neps, &epcap, &addr)) {
@ -532,7 +707,7 @@ int main(int argc, char **argv) {
naut_metainfo_free(&mi);
naut_magnet_free(&magnet);
free(endpoints);
return 1;
return NAUT_ERR_NOMEM;
}
}
} else {
@ -540,18 +715,21 @@ int main(int argc, char **argv) {
from_magnet ? magnet.infohash_v1 : mi.infohash_v1;
char *const *trackers =
from_magnet ? magnet.trackers : mi.trackers;
const uint32_t *tracker_tiers =
from_magnet ? NULL : mi.tracker_tiers;
size_t num_trackers =
from_magnet ? magnet.num_trackers : mi.num_trackers;
uint64_t total = from_magnet ? 0 : (uint64_t)mi.total_length;
if (!discover_trackers(hash, total, trackers, num_trackers, peerid,
&endpoints, &neps, &epcap) ||
if (!discover_trackers(hash, total, trackers, num_trackers,
tracker_tiers, peerid, &endpoints, &neps,
&epcap) ||
(neps == 0 &&
!discover_dht(hash, &endpoints, &neps, &epcap))) {
NAUT_ERROR("out of memory collecting discovered peers");
naut_metainfo_free(&mi);
naut_magnet_free(&magnet);
free(endpoints);
return 1;
return NAUT_ERR_NOMEM;
}
}
@ -574,7 +752,8 @@ int main(int argc, char **argv) {
free(info);
naut_magnet_free(&magnet);
free(endpoints);
return 1;
return metadata_error != NAUT_OK ? metadata_error :
NAUT_ERR_PROTO;
}
free(info);
NAUT_INFO("magnet metadata verified: %u pieces, %lld bytes",
@ -583,11 +762,11 @@ int main(int argc, char **argv) {
naut_magnet_free(&magnet);
if (neps == 0) {
NAUT_ERROR(argc > 3 ? "no valid peer addresses" :
"tracker and DHT discovery returned no peers");
NAUT_ERROR(config->num_peers > 0 ? "no valid peer addresses" :
"tracker and DHT discovery returned no peers");
naut_metainfo_free(&mi);
free(endpoints);
return 1;
return NAUT_ERR_NOTFOUND;
}
naut_err err;
@ -596,20 +775,22 @@ int main(int argc, char **argv) {
.preallocate = true,
};
naut_storage *st = naut_storage_open_opts(
mi.files, mi.num_files, argv[2], &storage_opts, &err);
mi.files, mi.num_files, config->output_dir, &storage_opts, &err);
if (!st) {
NAUT_ERROR("storage: %s", naut_strerror(err));
naut_metainfo_free(&mi);
free(endpoints);
return 1;
return err != NAUT_OK ? err : NAUT_ERR_IO;
}
naut_download *d = naut_download_create(&mi, st);
if (!d) {
naut_storage_close(st);
naut_metainfo_free(&mi);
free(endpoints);
return 1;
return NAUT_ERR_NOMEM;
}
naut_download_set_file_cb(d, on_file_complete, (void *)config);
naut_download_set_piece_cb(d, on_piece_complete, (void *)config);
int online_cpus = naut_online_cpus();
uint32_t worker_count =
(uint32_t)NAUT_MAX(1, NAUT_MIN(8, online_cpus / 2));
@ -628,7 +809,7 @@ int main(int argc, char **argv) {
naut_storage_close(st);
naut_metainfo_free(&mi);
free(endpoints);
return 1;
return NAUT_ERR_NOMEM;
}
naut_download_set_worker_pool(d, workers);
@ -644,61 +825,109 @@ int main(int argc, char **argv) {
naut_storage_close(st);
naut_metainfo_free(&mi);
free(endpoints);
return 1;
return NAUT_ERR_NOMEM;
}
for (int i = 0; i < npeers; i++) peers[i].fd = -1;
int active = 0;
double t0 = now();
naut_err run_error = NAUT_OK;
bool cancelled = false;
uint32_t connecting = 0;
uint32_t active = 0;
uint32_t failed = 0;
struct pollfd *connect_fds = pfd;
for (int i = 0; i < npeers; i++) connect_fds[i].fd = -1;
report_progress(config, d, &mi, (uint32_t)npeers, 0, 0, 0, t0);
for (int i = 0; i < npeers; i++) {
int fd = connect_to(&endpoints[i]);
bool connected = false;
int fd = connect_start(&endpoints[i], &connected);
if (fd < 0) {
NAUT_WARN("connect %s failed", endpoints[i].name);
peers[i].dead = true;
failed++;
continue;
}
peer_t *p = &peers[i];
p->fd = fd;
snprintf(p->name, sizeof p->name, "%s", endpoints[i].name);
p->peer_choking = true;
naut_pipeline_init(&p->pipeline, NAUT_BLOCK, 4, 1024, 32);
p->rcap = 1 << 18;
p->rbuf = malloc(p->rcap);
if (!p->rbuf || naut_bitfield_init(&p->have, mi.num_pieces) != NAUT_OK) {
free(p->rbuf);
p->rbuf = NULL;
close(fd);
p->fd = -1;
p->dead = true;
continue;
peers[i].fd = fd;
if (connected) {
if (!connect_finish(fd) ||
!peer_start(d, &mi, peerid, &endpoints[i], &peers[i], fd)) {
NAUT_WARN("connect %s failed", endpoints[i].name);
if (peers[i].fd >= 0) close(peers[i].fd);
peers[i].fd = -1;
peers[i].dead = true;
failed++;
continue;
}
active++;
emit_event(config, NAUT_EVENT_PEER_CONNECTED, 0,
peers[i].name, NULL);
} else {
connect_fds[i].fd = fd;
connect_fds[i].events = POLLOUT;
connecting++;
}
uint8_t hs[NAUT_HANDSHAKE_LEN];
naut_peer_handshake_build(hs, mi.infohash_v1, peerid, EXT_RESERVED);
uint8_t intr[5]; naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED);
uint8_t *ext = NULL;
size_t ext_len = 0;
naut_err ext_error = naut_ext_build_handshake(
NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, 0, 0, &ext, &ext_len);
bool sent = ext_error == NAUT_OK &&
send_all(fd, hs, sizeof hs) &&
send_all(fd, ext, ext_len) &&
send_all(fd, intr, 5);
free(ext);
if (!sent) {
peer_drop(d, p);
continue;
}
double connect_deadline = now() + CONNECT_TIMEOUT_MS / 1000.0;
while (connecting > 0 && now() < connect_deadline &&
!stop_requested(config)) {
report_progress(config, d, &mi, (uint32_t)npeers, connecting,
active, failed, t0);
int ready = poll(connect_fds, (nfds_t)neps, 100);
if (ready < 0) {
if (errno == EINTR) continue;
run_error = NAUT_ERR_IO;
break;
}
active++;
if (ready == 0) continue;
for (int i = 0; i < npeers; i++) {
if (connect_fds[i].fd < 0 ||
!(connect_fds[i].revents &
(POLLOUT | POLLERR | POLLHUP | POLLNVAL)))
continue;
int fd = connect_fds[i].fd;
connect_fds[i].fd = -1;
connecting--;
if (!connect_finish(fd) ||
!peer_start(d, &mi, peerid, &endpoints[i], &peers[i], fd)) {
NAUT_WARN("connect %s failed", endpoints[i].name);
if (peers[i].fd >= 0) close(peers[i].fd);
peers[i].fd = -1;
peers[i].dead = true;
failed++;
continue;
}
active++;
emit_event(config, NAUT_EVENT_PEER_CONNECTED, 0,
peers[i].name, NULL);
}
}
for (int i = 0; i < npeers; i++) {
if (connect_fds[i].fd < 0) continue;
close(connect_fds[i].fd);
connect_fds[i].fd = -1;
peers[i].fd = -1;
peers[i].dead = true;
connecting--;
failed++;
NAUT_WARN("connect %s timed out", endpoints[i].name);
}
free(endpoints);
if (!active) {
NAUT_ERROR("no peers reachable");
if (run_error == NAUT_OK) run_error = NAUT_ERR_IO;
goto done;
}
NAUT_INFO("swarm: %d peers, %u pieces, %lld bytes", active, mi.num_pieces, (long long)mi.total_length);
NAUT_INFO("swarm: %u/%d peers connected, %u pieces, %lld bytes",
active, npeers, mi.num_pieces, (long long)mi.total_length);
double t0 = now();
naut_err run_error = NAUT_OK;
emit_event(config, NAUT_EVENT_TORRENT_ADDED, 0, NULL, NULL);
report_progress(config, d, &mi, (uint32_t)npeers, 0, active, failed, t0);
while (!naut_download_complete(d) && run_error == NAUT_OK) {
service_control(config, st);
if (stop_requested(config)) {
cancelled = true;
break;
}
int nf = 0;
for (int i = 0; i < npeers; i++) {
if (peers[i].dead) continue;
@ -717,10 +946,18 @@ int main(int argc, char **argv) {
if (naut_download_complete(d)) break;
NAUT_ERROR("all peers gone (%.0f%% done)",
100.0 * naut_download_pieces_done(d) / mi.num_pieces);
run_error = NAUT_ERR_IO;
break;
}
report_progress(config, d, &mi, (uint32_t)npeers, 0,
(uint32_t)live_peers,
(uint32_t)npeers - (uint32_t)live_peers, t0);
int r = poll(pfd, nf, 200);
if (r < 0) {
if (errno == EINTR) continue;
run_error = NAUT_ERR_IO;
break;
}
int r = poll(pfd, nf, 2000);
if (r < 0) { if (errno == EINTR) continue; break; }
for (int k = 0; k < nf; k++) {
if (idx_map[k] < 0) {
@ -769,6 +1006,13 @@ int main(int argc, char **argv) {
NAUT_INFO("COMPLETE: %u/%u pieces from swarm in %.2fs (%.1f MB/s), all SHA-1 verified%s",
naut_download_pieces_done(d), mi.num_pieces, dt, mb/dt,
naut_download_in_endgame(d) ? " (passed through endgame)" : "");
report_progress(config, d, &mi, (uint32_t)npeers, 0, 0,
(uint32_t)npeers, t0);
emit_event(config, NAUT_EVENT_TORRENT_FINISHED, 0, NULL, NULL);
while (config->keep_alive && !stop_requested(config)) {
service_control(config, st);
usleep(100000);
}
} else {
if (run_error != NAUT_OK)
NAUT_ERROR("swarm stopped: %s", naut_strerror(run_error));
@ -777,6 +1021,7 @@ int main(int argc, char **argv) {
done:
ok = naut_download_complete(d);
service_control(config, st);
naut_storage_sync(st);
for (int i = 0; i < npeers; i++) {
if (peers[i].blocks_received)
@ -797,5 +1042,26 @@ done:
free(peers); free(pfd); free(idx_map);
naut_worker_pool_destroy(workers);
naut_download_destroy(d); naut_storage_close(st); naut_metainfo_free(&mi);
return ok ? 0 : 1;
if (ok) return NAUT_OK;
if (cancelled) return NAUT_ERR_AGAIN;
return run_error != NAUT_OK ? run_error : NAUT_ERR_IO;
}
#ifndef NAUT_SWARM_LIBRARY
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr,
"usage: %s <file.torrent|magnet-uri> <out-dir> [ip:port ...]\n",
argv[0]);
return 2;
}
naut_log_set_level(NAUT_LOG_INFO);
naut_swarm_config config = {
.source = argv[1],
.output_dir = argv[2],
.peers = argc > 3 ? (const char *const *)&argv[3] : NULL,
.num_peers = argc > 3 ? (size_t)(argc - 3) : 0,
};
return naut_swarm_run(&config) == NAUT_OK ? 0 : 1;
}
#endif