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>
885 lines
35 KiB
C
885 lines
35 KiB
C
/* naut_swarm — multi-peer download driver built on the torrent-peer engine.
|
|
*
|
|
* The engine (../torrent-peer, engine.h) owns all peer sockets, the wire
|
|
* protocol, the request pipeline, transports (TCP/µTP/MSE), and piece selection.
|
|
* This driver:
|
|
* - parses the .torrent / magnet and (for magnet) fetches the info dict,
|
|
* - discovers peers via HTTP/UDP trackers + DHT (src/discovery),
|
|
* - feeds discovered endpoints + a piece-priority vector to the engine,
|
|
* - drains delivered blocks, verifies+persists them through naut_download,
|
|
* - re-arms hash-failed pieces and reports progress for nautd / the web UI.
|
|
*
|
|
* usage: naut_swarm <file.torrent|magnet-uri> <output-dir> [<ip:port> ...]
|
|
*/
|
|
#include "naut/dht.h"
|
|
#include "naut/extension.h"
|
|
#include "naut/metainfo.h"
|
|
#include "naut/storage.h"
|
|
#include "naut/piece.h"
|
|
#include "naut/tracker.h"
|
|
#include "naut/log.h"
|
|
#include "naut/system.h"
|
|
#include "naut/swarm.h"
|
|
|
|
#include "engine.h" /* torrent-peer multi-peer engine */
|
|
|
|
#include <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#define DEFAULT_TARGET_PEERS 80
|
|
#define MAX_TARGET_PEERS 512
|
|
#define TRACKER_DEFAULT_INTERVAL 1800.0
|
|
#define TRACKER_MIN_INTERVAL 60.0
|
|
#define TRACKER_FAILURE_RETRY_INTERVAL 300.0
|
|
#define DHT_REFRESH_INTERVAL 300.0
|
|
#define READY_BATCH 64
|
|
|
|
typedef struct {
|
|
naut_peer_addr addr;
|
|
char name[32];
|
|
} endpoint_t;
|
|
|
|
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec*1e-9; }
|
|
|
|
static uint32_t target_peer_count(void) {
|
|
const char *env = getenv("NAUT_TARGET_PEERS");
|
|
if (!env || !*env) return DEFAULT_TARGET_PEERS;
|
|
char *end = NULL;
|
|
unsigned long value = strtoul(env, &end, 10);
|
|
if (!end || *end || value == 0) return DEFAULT_TARGET_PEERS;
|
|
return (uint32_t)NAUT_MIN(value, MAX_TARGET_PEERS);
|
|
}
|
|
|
|
static double tracker_delay_seconds(int32_t interval) {
|
|
if (interval <= 0) return TRACKER_DEFAULT_INTERVAL;
|
|
if (interval < (int32_t)TRACKER_MIN_INTERVAL)
|
|
return TRACKER_MIN_INTERVAL;
|
|
return (double)interval;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/* --- tracker stats ------------------------------------------------------- */
|
|
|
|
static uint32_t init_tracker_stats(char *const *trackers,
|
|
const uint32_t *tracker_tiers,
|
|
size_t num_trackers,
|
|
naut_swarm_tracker_stats *stats,
|
|
uint32_t capacity) {
|
|
uint32_t count = 0;
|
|
if (!stats) return 0;
|
|
for (size_t i = 0; i < num_trackers && count < capacity; i++) {
|
|
naut_swarm_tracker_stats *out = &stats[count++];
|
|
memset(out, 0, sizeof(*out));
|
|
snprintf(out->url, sizeof out->url, "%s", trackers[i]);
|
|
out->tier = tracker_tiers ? (int32_t)tracker_tiers[i] : (int32_t)i;
|
|
snprintf(out->status, sizeof out->status, "not contacted");
|
|
out->seeds = -1;
|
|
out->peers = -1;
|
|
out->leeches = -1;
|
|
out->downloaded = -1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static naut_swarm_tracker_stats *tracker_stat_for(
|
|
naut_swarm_tracker_stats *stats, uint32_t count, const char *url) {
|
|
if (!stats || !url) return NULL;
|
|
for (uint32_t i = 0; i < count; i++)
|
|
if (strcmp(stats[i].url, url) == 0) return &stats[i];
|
|
return NULL;
|
|
}
|
|
|
|
static void tracker_set_status(naut_swarm_tracker_stats *tracker,
|
|
const char *status, const char *message) {
|
|
if (!tracker) return;
|
|
snprintf(tracker->status, sizeof tracker->status, "%s",
|
|
status ? status : "");
|
|
snprintf(tracker->message, sizeof tracker->message, "%s",
|
|
message ? message : "");
|
|
}
|
|
|
|
static void snapshot_tracker_stats(naut_swarm_stats *stats,
|
|
const naut_swarm_tracker_stats *trackers,
|
|
uint32_t tracker_count) {
|
|
if (!stats || !trackers) return;
|
|
if (tracker_count > NAUT_SWARM_MAX_TRACKER_STATS)
|
|
tracker_count = NAUT_SWARM_MAX_TRACKER_STATS;
|
|
stats->tracker_count = tracker_count;
|
|
for (uint32_t i = 0; i < tracker_count; i++)
|
|
stats->tracker_stats[i] = trackers[i];
|
|
}
|
|
|
|
static void snapshot_file_stats(naut_swarm_stats *stats,
|
|
const naut_download *download,
|
|
const naut_metainfo *metainfo) {
|
|
if (!stats || !metainfo || !metainfo->files) return;
|
|
uint64_t offset = 0;
|
|
uint32_t count = 0;
|
|
for (size_t i = 0;
|
|
i < metainfo->num_files && count < NAUT_SWARM_MAX_FILE_STATS;
|
|
i++) {
|
|
const naut_file *file = &metainfo->files[i];
|
|
uint64_t size = file->length > 0 ? (uint64_t)file->length : 0;
|
|
uint64_t done = 0;
|
|
if (download && size > 0) {
|
|
uint64_t start = offset;
|
|
uint64_t end = offset + size;
|
|
uint32_t first = (uint32_t)(start / (uint64_t)metainfo->piece_length);
|
|
uint32_t last = (uint32_t)((end - 1) /
|
|
(uint64_t)metainfo->piece_length);
|
|
for (uint32_t p = first; p <= last; p++) {
|
|
if (!naut_download_have(download, p)) continue;
|
|
uint64_t piece_start = (uint64_t)p *
|
|
(uint64_t)metainfo->piece_length;
|
|
uint64_t piece_end = piece_start +
|
|
(uint64_t)metainfo->piece_length;
|
|
if (piece_end > (uint64_t)metainfo->total_length)
|
|
piece_end = (uint64_t)metainfo->total_length;
|
|
uint64_t lo = piece_start > start ? piece_start : start;
|
|
uint64_t hi = piece_end < end ? piece_end : end;
|
|
if (hi > lo) done += hi - lo;
|
|
}
|
|
}
|
|
naut_swarm_file_stats *out = &stats->file_stats[count++];
|
|
memset(out, 0, sizeof(*out));
|
|
snprintf(out->path, sizeof out->path, "%s",
|
|
file->path ? file->path : "");
|
|
out->size = size;
|
|
out->progress = size ? (double)done / (double)size : 1.0;
|
|
if (out->progress > 1.0) out->progress = 1.0;
|
|
out->priority = 1;
|
|
out->availability = 1.0;
|
|
offset += size;
|
|
}
|
|
stats->file_count = count;
|
|
}
|
|
|
|
/* Fill and deliver a progress snapshot from engine status + naut_download.
|
|
* Per-peer detail collapses to the engine's aggregate counts for now; rich
|
|
* per-peer rows are a later web-UI feature. */
|
|
static void report_progress(const naut_swarm_config *config,
|
|
engine *eng, uint32_t torrent_id,
|
|
const naut_download *download,
|
|
const naut_metainfo *metainfo,
|
|
const naut_swarm_tracker_stats *trackers,
|
|
uint32_t tracker_count,
|
|
double started_at) {
|
|
if (!config->on_progress) return;
|
|
torrent_status ts;
|
|
memset(&ts, 0, sizeof ts);
|
|
if (eng) engine_torrent_status(eng, torrent_id, &ts);
|
|
uint32_t connecting = ts.peers > ts.peers_connected + ts.peers_failed
|
|
? ts.peers - ts.peers_connected - ts.peers_failed : 0;
|
|
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 = ts.peers,
|
|
.peers_connecting = connecting,
|
|
.peers_active = ts.peers_connected,
|
|
.peers_failed = ts.peers_failed,
|
|
.stalled = ts.peers_connected == 0,
|
|
.elapsed_seconds = now() - started_at,
|
|
};
|
|
snapshot_tracker_stats(&stats, trackers, tracker_count);
|
|
snapshot_file_stats(&stats, download, metainfo);
|
|
if (download)
|
|
stats.piece_state_count = (uint32_t)naut_download_piece_states(
|
|
download, stats.piece_states, NAUT_SWARM_MAX_PIECE_STATS);
|
|
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);
|
|
}
|
|
|
|
/* Render a full diagnostic snapshot (block assembly + engine piece selection)
|
|
* when the caller requests one, and hand the text back through on_dump. Runs on
|
|
* the owner thread, the only place engine + download state can be read safely. */
|
|
static void service_dump(const naut_swarm_config *config, engine *eng,
|
|
uint32_t torrent_id, const naut_download *download) {
|
|
if (!config->should_dump || !config->on_dump) return;
|
|
if (!config->should_dump(config->context)) return;
|
|
|
|
char *buf = NULL;
|
|
size_t len = 0;
|
|
FILE *f = open_memstream(&buf, &len);
|
|
if (!f) {
|
|
config->on_dump(config->context, "dump: out of memory\n");
|
|
return;
|
|
}
|
|
naut_download_dump(download, f);
|
|
engine_dump_torrent(eng, torrent_id, f);
|
|
fclose(f);
|
|
config->on_dump(config->context, buf ? buf : "dump: render failed\n");
|
|
free(buf);
|
|
}
|
|
|
|
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);
|
|
uint8_t *b = malloc(n);
|
|
if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; }
|
|
fclose(f); *len = (size_t)n; return b;
|
|
}
|
|
|
|
/* --- endpoint collection ------------------------------------------------- */
|
|
|
|
static bool endpoint_add(endpoint_t **v, size_t *n, size_t *cap,
|
|
const naut_peer_addr *addr) {
|
|
if (addr->port == 0) return true;
|
|
for (size_t i = 0; i < *n; i++)
|
|
if ((*v)[i].addr.port == addr->port &&
|
|
memcmp((*v)[i].addr.ip, addr->ip, sizeof addr->ip) == 0)
|
|
return true;
|
|
if (*n == *cap) {
|
|
size_t newcap = *cap ? *cap * 2 : 32;
|
|
endpoint_t *next = realloc(*v, newcap * sizeof(*next));
|
|
if (!next) return false;
|
|
*v = next;
|
|
*cap = newcap;
|
|
}
|
|
endpoint_t *ep = &(*v)[(*n)++];
|
|
ep->addr = *addr;
|
|
snprintf(ep->name, sizeof ep->name, "%u.%u.%u.%u:%u",
|
|
addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3], addr->port);
|
|
return true;
|
|
}
|
|
|
|
static bool endpoint_parse(const char *s, naut_peer_addr *out) {
|
|
char host[INET_ADDRSTRLEN];
|
|
const char *colon = strrchr(s, ':');
|
|
if (!colon || colon == s || (size_t)(colon - s) >= sizeof host) return false;
|
|
char *end = NULL;
|
|
unsigned long port = strtoul(colon + 1, &end, 10);
|
|
if (!end || *end || port == 0 || port > UINT16_MAX) return false;
|
|
memcpy(host, s, (size_t)(colon - s));
|
|
host[colon - s] = 0;
|
|
struct in_addr ip;
|
|
if (inet_pton(AF_INET, host, &ip) != 1) return false;
|
|
memcpy(out->ip, &ip, sizeof out->ip);
|
|
out->port = (uint16_t)port;
|
|
return true;
|
|
}
|
|
|
|
static bool parse_udp_tracker(const char *url, char *host, size_t hostsz,
|
|
uint16_t *port) {
|
|
if (strncmp(url, "udp://", 6) != 0) return false;
|
|
const char *start = url + 6;
|
|
const char *slash = strchr(start, '/');
|
|
const char *end = slash ? slash : start + strlen(start);
|
|
const char *colon = memchr(start, ':', (size_t)(end - start));
|
|
if (!colon || colon == start) return false;
|
|
size_t hlen = (size_t)(colon - start);
|
|
if (hlen >= hostsz) return false;
|
|
char pbuf[16];
|
|
size_t plen = (size_t)(end - colon - 1);
|
|
if (plen == 0 || plen >= sizeof pbuf) return false;
|
|
memcpy(host, start, hlen);
|
|
host[hlen] = 0;
|
|
memcpy(pbuf, colon + 1, plen);
|
|
pbuf[plen] = 0;
|
|
char *tail = NULL;
|
|
unsigned long parsed = strtoul(pbuf, &tail, 10);
|
|
if (!tail || *tail || parsed == 0 || parsed > UINT16_MAX) return false;
|
|
*port = (uint16_t)parsed;
|
|
return true;
|
|
}
|
|
|
|
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],
|
|
uint64_t downloaded, uint64_t left,
|
|
naut_tracker_event event,
|
|
endpoint_t **eps, size_t *neps, size_t *cap,
|
|
int32_t *announce_interval,
|
|
naut_swarm_tracker_stats *tracker_stats,
|
|
uint32_t tracker_count) {
|
|
naut_announce_req req;
|
|
memset(&req, 0, sizeof req);
|
|
memcpy(req.info_hash, info_hash, sizeof req.info_hash);
|
|
memcpy(req.peer_id, peerid, sizeof req.peer_id);
|
|
req.port = 6881;
|
|
req.downloaded = downloaded;
|
|
req.left = total_length;
|
|
if (left <= total_length) req.left = left;
|
|
req.event = event;
|
|
req.numwant = 100;
|
|
memcpy(&req.key, peerid + 8, sizeof req.key);
|
|
if (announce_interval) *announce_interval = 0;
|
|
|
|
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;
|
|
memset(&response, 0, sizeof response);
|
|
response.seeders = response.leechers = -1;
|
|
naut_swarm_tracker_stats *tracker_stat =
|
|
tracker_stat_for(tracker_stats, tracker_count, tracker);
|
|
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 {
|
|
tracker_set_status(tracker_stat, "unsupported",
|
|
"unsupported tracker scheme");
|
|
NAUT_WARN("tracker scheme unsupported: %s", tracker);
|
|
continue;
|
|
}
|
|
if (e != NAUT_OK) {
|
|
tracker_set_status(tracker_stat, "error",
|
|
response.failure
|
|
? response.failure : "announce failed");
|
|
NAUT_WARN("tracker announce failed: %s", tracker);
|
|
naut_tracker_response_free(&response);
|
|
continue;
|
|
}
|
|
tier_succeeded = true;
|
|
if (announce_interval && response.interval > 0)
|
|
*announce_interval = response.interval;
|
|
if (tracker_stat) {
|
|
tracker_set_status(tracker_stat, "working", "");
|
|
tracker_stat->seeds = response.seeders;
|
|
tracker_stat->leeches = response.leechers;
|
|
tracker_stat->peers = response.num_peers > INT32_MAX
|
|
? INT32_MAX : (int32_t)response.num_peers;
|
|
tracker_stat->downloaded = -1;
|
|
}
|
|
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;
|
|
}
|
|
if (tier_succeeded) break;
|
|
tier_start = tier_end;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool discover_dht(const uint8_t info_hash[20],
|
|
endpoint_t **eps, size_t *neps, size_t *cap) {
|
|
static const char *defaults[] = {
|
|
"router.bittorrent.com:6881",
|
|
"router.utorrent.com:6881",
|
|
"dht.transmissionbt.com:6881",
|
|
};
|
|
const char *const *bootstrap = defaults;
|
|
size_t num_bootstrap = NAUT_ARRAY_LEN(defaults);
|
|
char *copy = NULL;
|
|
const char *custom[32];
|
|
|
|
const char *env = getenv("NAUT_DHT_BOOTSTRAP");
|
|
if (env && *env) {
|
|
copy = strdup(env);
|
|
if (!copy) return false;
|
|
num_bootstrap = 0;
|
|
char *save = NULL;
|
|
for (char *part = strtok_r(copy, ",", &save);
|
|
part && num_bootstrap < NAUT_ARRAY_LEN(custom);
|
|
part = strtok_r(NULL, ",", &save))
|
|
custom[num_bootstrap++] = part;
|
|
bootstrap = custom;
|
|
}
|
|
|
|
naut_peer_addr *peers = NULL;
|
|
size_t num_peers = 0;
|
|
naut_err e = num_bootstrap
|
|
? naut_dht_get_peers(bootstrap, num_bootstrap, info_hash,
|
|
&peers, &num_peers)
|
|
: NAUT_ERR_INVAL;
|
|
free(copy);
|
|
if (e != NAUT_OK) {
|
|
NAUT_WARN("DHT lookup found no peers");
|
|
return true;
|
|
}
|
|
NAUT_INFO("DHT returned %zu peers", num_peers);
|
|
for (size_t i = 0; i < num_peers; i++) {
|
|
if (!endpoint_add(eps, neps, cap, &peers[i])) {
|
|
free(peers);
|
|
return false;
|
|
}
|
|
}
|
|
free(peers);
|
|
return true;
|
|
}
|
|
|
|
/* Hand every not-yet-fed endpoint to the engine, which owns the connection. */
|
|
static void feed_engine(engine *eng, uint32_t torrent_id,
|
|
const endpoint_t *eps, size_t neps, size_t *fed) {
|
|
for (size_t i = *fed; i < neps; i++) {
|
|
char ip[16];
|
|
snprintf(ip, sizeof ip, "%u.%u.%u.%u",
|
|
eps[i].addr.ip[0], eps[i].addr.ip[1],
|
|
eps[i].addr.ip[2], eps[i].addr.ip[3]);
|
|
engine_add_peer(eng, torrent_id, ip, eps[i].addr.port);
|
|
}
|
|
*fed = neps;
|
|
}
|
|
|
|
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);
|
|
random_bytes(peerid + 8, sizeof peerid - 8);
|
|
|
|
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(config->source, &magnet) != NAUT_OK ||
|
|
!magnet.has_v1) {
|
|
NAUT_ERROR("magnet must contain a v1 btih hash");
|
|
naut_magnet_free(&magnet);
|
|
return NAUT_ERR_INVAL;
|
|
}
|
|
} else {
|
|
size_t tlen;
|
|
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 NAUT_ERR_PROTO;
|
|
}
|
|
free(tor);
|
|
}
|
|
|
|
endpoint_t *endpoints = NULL;
|
|
size_t neps = 0, epcap = 0;
|
|
uint32_t target_peers = target_peer_count();
|
|
int32_t tracker_interval = 0;
|
|
naut_swarm_tracker_stats tracker_stats[NAUT_SWARM_MAX_TRACKER_STATS];
|
|
uint32_t tracker_count = 0;
|
|
if (config->num_peers > 0) {
|
|
for (size_t i = 0; i < config->num_peers; i++) {
|
|
naut_peer_addr addr;
|
|
if (!endpoint_parse(config->peers[i], &addr)) {
|
|
NAUT_WARN("invalid peer address: %s", config->peers[i]);
|
|
continue;
|
|
}
|
|
if (!endpoint_add(&endpoints, &neps, &epcap, &addr)) {
|
|
NAUT_ERROR("out of memory collecting peers");
|
|
naut_metainfo_free(&mi);
|
|
naut_magnet_free(&magnet);
|
|
free(endpoints);
|
|
return NAUT_ERR_NOMEM;
|
|
}
|
|
}
|
|
} else {
|
|
const uint8_t *hash =
|
|
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;
|
|
tracker_count = init_tracker_stats(trackers, tracker_tiers,
|
|
num_trackers, tracker_stats,
|
|
NAUT_SWARM_MAX_TRACKER_STATS);
|
|
if (from_magnet) {
|
|
if (!discover_trackers(hash, 0, trackers, num_trackers,
|
|
tracker_tiers, peerid, 0, 0,
|
|
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
|
&tracker_interval, tracker_stats,
|
|
tracker_count) ||
|
|
(neps < target_peers &&
|
|
!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 NAUT_ERR_NOMEM;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Magnet: resolve the info dict from a peer before we can size the torrent.
|
|
* Neither the engine nor torrent-tracker does BEP-9; use Naut's own fetch. */
|
|
if (from_magnet && neps) {
|
|
uint8_t *info = NULL;
|
|
size_t info_len = 0;
|
|
naut_err metadata_error = NAUT_ERR_EMPTY;
|
|
for (size_t i = 0; i < neps; i++) {
|
|
metadata_error = naut_metadata_fetch(
|
|
&endpoints[i].addr, magnet.infohash_v1, peerid,
|
|
&info, &info_len);
|
|
if (metadata_error == NAUT_OK) break;
|
|
NAUT_WARN("metadata fetch from %s failed", endpoints[i].name);
|
|
}
|
|
if (metadata_error != NAUT_OK ||
|
|
naut_metainfo_parse_info(
|
|
info, info_len, (const char *const *)magnet.trackers,
|
|
magnet.num_trackers, &mi) != NAUT_OK) {
|
|
NAUT_ERROR("unable to fetch valid magnet metadata");
|
|
free(info);
|
|
naut_magnet_free(&magnet);
|
|
free(endpoints);
|
|
return metadata_error != NAUT_OK ? metadata_error :
|
|
NAUT_ERR_PROTO;
|
|
}
|
|
free(info);
|
|
NAUT_INFO("magnet metadata verified: %u pieces, %lld bytes",
|
|
mi.num_pieces, (long long)mi.total_length);
|
|
}
|
|
naut_magnet_free(&magnet);
|
|
|
|
if (neps == 0) {
|
|
if (from_magnet) {
|
|
NAUT_ERROR("magnet metadata unavailable: no peers discovered");
|
|
naut_metainfo_free(&mi);
|
|
free(endpoints);
|
|
return NAUT_ERR_NOTFOUND;
|
|
}
|
|
if (config->num_peers > 0)
|
|
NAUT_WARN("no valid peer addresses; torrent stalled");
|
|
}
|
|
|
|
naut_err err;
|
|
/* Reopen any files relocated on a prior run in place (no re-download). */
|
|
const char **overrides = NULL;
|
|
if (config->num_locations && mi.num_files) {
|
|
overrides = calloc(mi.num_files, sizeof *overrides);
|
|
if (overrides)
|
|
for (size_t i = 0; i < config->num_locations; i++) {
|
|
const naut_swarm_file_location *loc = &config->locations[i];
|
|
if (loc->path && loc->file_index < mi.num_files)
|
|
overrides[loc->file_index] = loc->path;
|
|
}
|
|
}
|
|
naut_storage_opts storage_opts = {
|
|
.direct_io = getenv("NAUT_DIRECT_IO") != NULL,
|
|
.preallocate = true,
|
|
.overrides = overrides,
|
|
};
|
|
naut_storage *st = naut_storage_open_opts(
|
|
mi.files, mi.num_files, config->output_dir, &storage_opts, &err);
|
|
free(overrides);
|
|
if (!st) {
|
|
NAUT_ERROR("storage: %s", naut_strerror(err));
|
|
naut_metainfo_free(&mi);
|
|
free(endpoints);
|
|
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 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);
|
|
err = naut_download_resume(d);
|
|
if (err != NAUT_OK) {
|
|
NAUT_ERROR("resume scan: %s", naut_strerror(err));
|
|
naut_download_destroy(d);
|
|
naut_storage_close(st);
|
|
naut_metainfo_free(&mi);
|
|
free(endpoints);
|
|
return err;
|
|
}
|
|
uint64_t resumed_bytes = naut_download_bytes_done(d);
|
|
uint64_t resumed_left = (uint64_t)mi.total_length > resumed_bytes
|
|
? (uint64_t)mi.total_length - resumed_bytes : 0;
|
|
NAUT_INFO("resume scan complete: %u/%u pieces correct, %llu bytes left",
|
|
naut_download_pieces_done(d), mi.num_pieces,
|
|
(unsigned long long)resumed_left);
|
|
|
|
if (!from_magnet && config->num_peers == 0 && !naut_download_complete(d)) {
|
|
if (!discover_trackers(mi.infohash_v1, (uint64_t)mi.total_length,
|
|
mi.trackers, mi.num_trackers,
|
|
mi.tracker_tiers, peerid,
|
|
resumed_bytes, resumed_left,
|
|
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
|
&tracker_interval, tracker_stats,
|
|
tracker_count) ||
|
|
(neps < target_peers &&
|
|
!discover_dht(mi.infohash_v1, &endpoints, &neps, &epcap))) {
|
|
NAUT_ERROR("out of memory collecting discovered peers");
|
|
naut_download_destroy(d);
|
|
naut_storage_close(st);
|
|
naut_metainfo_free(&mi);
|
|
free(endpoints);
|
|
return NAUT_ERR_NOMEM;
|
|
}
|
|
if (neps == 0)
|
|
NAUT_WARN("tracker and DHT discovery returned no peers; torrent stalled");
|
|
}
|
|
|
|
/* Spin up the engine and register the torrent. The engine owns sockets,
|
|
* the wire protocol, the pipeline, transports, and piece selection. */
|
|
engine_config ecfg;
|
|
memset(&ecfg, 0, sizeof ecfg);
|
|
ecfg.encryption = 1; /* offer MSE (RC4) + plaintext: most compatible */
|
|
ecfg.fallback = 1; /* retry transport/encryption combos per endpoint */
|
|
engine *eng = engine_create(&ecfg);
|
|
int32_t tid = eng ? engine_add_torrent(eng, mi.infohash_v1, peerid,
|
|
(uint64_t)mi.piece_length,
|
|
(uint64_t)mi.total_length,
|
|
mi.num_pieces)
|
|
: -1;
|
|
if (!eng || tid < 0) {
|
|
NAUT_ERROR("unable to create download engine");
|
|
if (eng) engine_destroy(eng);
|
|
naut_download_destroy(d);
|
|
naut_storage_close(st);
|
|
naut_metainfo_free(&mi);
|
|
free(endpoints);
|
|
return NAUT_ERR_NOMEM;
|
|
}
|
|
uint32_t torrent_id = (uint32_t)tid;
|
|
|
|
/* Priority vector: skip what resume already verified, request the rest. */
|
|
uint8_t *prio = malloc(mi.num_pieces ? mi.num_pieces : 1);
|
|
if (!prio) {
|
|
engine_destroy(eng);
|
|
naut_download_destroy(d);
|
|
naut_storage_close(st);
|
|
naut_metainfo_free(&mi);
|
|
free(endpoints);
|
|
return NAUT_ERR_NOMEM;
|
|
}
|
|
for (uint32_t p = 0; p < mi.num_pieces; p++)
|
|
prio[p] = naut_download_have(d, p) ? 0 : 1;
|
|
engine_set_priorities(eng, torrent_id, prio, mi.num_pieces);
|
|
|
|
size_t fed = 0;
|
|
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
|
|
|
double t0 = now();
|
|
double next_tracker_announce =
|
|
t0 + (tracker_interval > 0
|
|
? tracker_delay_seconds(tracker_interval)
|
|
: TRACKER_FAILURE_RETRY_INTERVAL);
|
|
double next_dht_lookup = t0 + DHT_REFRESH_INTERVAL;
|
|
naut_err run_error = NAUT_OK;
|
|
bool cancelled = false;
|
|
|
|
NAUT_INFO("swarm: engine started, %zu peers queued, %u pieces, %lld bytes",
|
|
neps, mi.num_pieces, (long long)mi.total_length);
|
|
emit_event(config, NAUT_EVENT_TORRENT_ADDED, 0, NULL, NULL);
|
|
report_progress(config, eng, torrent_id, d, &mi,
|
|
tracker_stats, tracker_count, t0);
|
|
|
|
engine_block blocks[READY_BATCH];
|
|
uint64_t applied_rate = UINT64_MAX; /* force first apply */
|
|
while (!naut_download_complete(d) && run_error == NAUT_OK) {
|
|
service_control(config, st);
|
|
service_dump(config, eng, torrent_id, d);
|
|
if (stop_requested(config)) { cancelled = true; break; }
|
|
|
|
/* Apply the live download throttle when it changes. */
|
|
if (config->download_rate) {
|
|
uint64_t rate = config->download_rate(config->context);
|
|
if (rate != applied_rate) {
|
|
engine_set_download_rate(eng, rate);
|
|
applied_rate = rate;
|
|
}
|
|
}
|
|
|
|
/* Top up the swarm from trackers / DHT when it runs thin. */
|
|
if (config->num_peers == 0) {
|
|
torrent_status ts;
|
|
engine_torrent_status(eng, torrent_id, &ts);
|
|
if (ts.peers_connected < target_peers) {
|
|
double t = now();
|
|
if (mi.num_trackers > 0 && t >= next_tracker_announce) {
|
|
uint64_t downloaded = naut_download_bytes_done(d);
|
|
uint64_t left = (uint64_t)mi.total_length > downloaded
|
|
? (uint64_t)mi.total_length - downloaded : 0;
|
|
int32_t interval = 0;
|
|
if (!discover_trackers(mi.infohash_v1,
|
|
(uint64_t)mi.total_length,
|
|
mi.trackers, mi.num_trackers,
|
|
mi.tracker_tiers, peerid,
|
|
downloaded, left, NAUT_TEV_NONE,
|
|
&endpoints, &neps, &epcap,
|
|
&interval, tracker_stats,
|
|
tracker_count)) {
|
|
run_error = NAUT_ERR_NOMEM;
|
|
break;
|
|
}
|
|
next_tracker_announce = t + (interval > 0
|
|
? tracker_delay_seconds(interval)
|
|
: TRACKER_FAILURE_RETRY_INTERVAL);
|
|
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
|
}
|
|
if (t >= next_dht_lookup) {
|
|
if (!discover_dht(mi.infohash_v1, &endpoints, &neps,
|
|
&epcap)) {
|
|
run_error = NAUT_ERR_NOMEM;
|
|
break;
|
|
}
|
|
next_dht_lookup = t + DHT_REFRESH_INTERVAL;
|
|
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
|
}
|
|
}
|
|
}
|
|
|
|
engine_wait(eng, 200);
|
|
uint32_t n;
|
|
while ((n = engine_poll_ready(eng, blocks, READY_BATCH)) > 0) {
|
|
for (uint32_t i = 0; i < n; i++) {
|
|
engine_block *b = &blocks[i];
|
|
uint8_t *data = (uint8_t *)engine_arena_base(eng, b->loop) +
|
|
(uint64_t)b->slot * PEER_BLOCK_SIZE;
|
|
bool done = false;
|
|
naut_err be = naut_download_on_block(d, b->piece, b->begin,
|
|
data, b->len, &done);
|
|
engine_release_slot(eng, b->loop, b->slot);
|
|
if (be == NAUT_ERR_PROTO) {
|
|
/* Bad/failed piece: re-arm it for another fetch. */
|
|
engine_request_piece(eng, torrent_id, b->piece);
|
|
} else if (be != NAUT_OK && be != NAUT_ERR_RANGE) {
|
|
run_error = be;
|
|
break;
|
|
} else if (done) {
|
|
engine_set_priority(eng, torrent_id, b->piece, 0);
|
|
}
|
|
}
|
|
if (run_error != NAUT_OK) break;
|
|
}
|
|
report_progress(config, eng, torrent_id, d, &mi,
|
|
tracker_stats, tracker_count, t0);
|
|
}
|
|
|
|
double dt = now() - t0;
|
|
bool ok = naut_download_complete(d);
|
|
if (ok) {
|
|
double mb = (double)mi.total_length / 1e6;
|
|
NAUT_INFO("COMPLETE: %u/%u pieces in %.2fs (%.1f MB/s), all SHA-1 verified",
|
|
naut_download_pieces_done(d), mi.num_pieces, dt,
|
|
dt > 0 ? mb / dt : 0.0);
|
|
report_progress(config, eng, torrent_id, d, &mi,
|
|
tracker_stats, tracker_count, t0);
|
|
emit_event(config, NAUT_EVENT_TORRENT_FINISHED, 0, NULL, NULL);
|
|
while (config->keep_alive && !stop_requested(config)) {
|
|
service_control(config, st);
|
|
service_dump(config, eng, torrent_id, d);
|
|
usleep(100000);
|
|
}
|
|
} else {
|
|
if (run_error != NAUT_OK)
|
|
NAUT_ERROR("swarm stopped: %s", naut_strerror(run_error));
|
|
NAUT_ERROR("INCOMPLETE: %u/%u pieces",
|
|
naut_download_pieces_done(d), mi.num_pieces);
|
|
}
|
|
|
|
service_control(config, st);
|
|
naut_storage_sync(st);
|
|
engine_destroy(eng);
|
|
free(prio);
|
|
free(endpoints);
|
|
naut_download_destroy(d);
|
|
naut_storage_close(st);
|
|
naut_metainfo_free(&mi);
|
|
if (ok) return NAUT_OK;
|
|
if (cancelled) return NAUT_ERR_AGAIN;
|
|
return run_error != NAUT_OK ? run_error : NAUT_ERR_IO;
|
|
}
|