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:
parent
50a357968a
commit
8dde48c05a
27 changed files with 2706 additions and 403 deletions
|
|
@ -20,9 +20,37 @@ static char *dup_cstr(const uint8_t *p, size_t n) {
|
|||
return s;
|
||||
}
|
||||
|
||||
/* collect a single announce string or an announce-list (list of tiers) */
|
||||
static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
||||
size_t cap = 0;
|
||||
static naut_err add_tracker(naut_metainfo *mi, size_t *capacity,
|
||||
const uint8_t *url, size_t url_len,
|
||||
uint32_t tier) {
|
||||
for (size_t i = 0; i < mi->num_trackers; i++)
|
||||
if (strlen(mi->trackers[i]) == url_len &&
|
||||
memcmp(mi->trackers[i], url, url_len) == 0)
|
||||
return NAUT_OK;
|
||||
if (mi->num_trackers == *capacity) {
|
||||
size_t next_capacity = *capacity ? *capacity * 2 : 8;
|
||||
char **next_trackers =
|
||||
realloc(mi->trackers, next_capacity * sizeof(*next_trackers));
|
||||
if (!next_trackers) return NAUT_ERR_NOMEM;
|
||||
mi->trackers = next_trackers;
|
||||
uint32_t *next_tiers =
|
||||
realloc(mi->tracker_tiers,
|
||||
next_capacity * sizeof(*next_tiers));
|
||||
if (!next_tiers) return NAUT_ERR_NOMEM;
|
||||
mi->tracker_tiers = next_tiers;
|
||||
*capacity = next_capacity;
|
||||
}
|
||||
char *copy = dup_cstr(url, url_len);
|
||||
if (!copy) return NAUT_ERR_NOMEM;
|
||||
mi->trackers[mi->num_trackers] = copy;
|
||||
mi->tracker_tiers[mi->num_trackers] = tier;
|
||||
mi->num_trackers++;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
/* Preserve the outer announce-list as BEP-12 failover tiers. */
|
||||
static naut_err collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
||||
size_t capacity = 0;
|
||||
const naut_bc *al = naut_bc_dict_get(root, "announce-list");
|
||||
if (al && al->type == NAUT_BC_LIST) {
|
||||
for (size_t t = 0; t < al->v.list.count; t++) {
|
||||
|
|
@ -32,21 +60,20 @@ static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
|||
const naut_bc *url = naut_bc_list_at(tier, u);
|
||||
const uint8_t *p; size_t n;
|
||||
if (!naut_bc_get_str(url, &p, &n)) continue;
|
||||
if (mi->num_trackers == cap) {
|
||||
cap = cap ? cap * 2 : 8;
|
||||
mi->trackers = realloc(mi->trackers, cap * sizeof(char *));
|
||||
}
|
||||
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
|
||||
naut_err error =
|
||||
add_tracker(mi, &capacity, p, n, (uint32_t)t);
|
||||
if (error != NAUT_OK) return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mi->num_trackers == 0) {
|
||||
const uint8_t *p; size_t n;
|
||||
if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) {
|
||||
mi->trackers = malloc(sizeof(char *));
|
||||
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
|
||||
naut_err error = add_tracker(mi, &capacity, p, n, 0);
|
||||
if (error != NAUT_OK) return error;
|
||||
}
|
||||
}
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
/* v1 file list: single-file (info.length) or multi-file (info.files[]) */
|
||||
|
|
@ -220,7 +247,12 @@ naut_err naut_metainfo_parse(const uint8_t *data, size_t len, naut_metainfo *out
|
|||
} else {
|
||||
collect_files_v2(info, out); /* v2-only: walk the file tree */
|
||||
}
|
||||
collect_trackers(root, out);
|
||||
e = collect_trackers(root, out);
|
||||
if (e != NAUT_OK) {
|
||||
out->_owned = o;
|
||||
naut_metainfo_free(out);
|
||||
return e;
|
||||
}
|
||||
|
||||
out->_owned = o;
|
||||
return NAUT_OK;
|
||||
|
|
@ -245,7 +277,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
|
|||
|
||||
if (num_trackers) {
|
||||
out->trackers = calloc(num_trackers, sizeof(*out->trackers));
|
||||
if (!out->trackers) {
|
||||
out->tracker_tiers =
|
||||
calloc(num_trackers, sizeof(*out->tracker_tiers));
|
||||
if (!out->trackers || !out->tracker_tiers) {
|
||||
naut_metainfo_free(out);
|
||||
return NAUT_ERR_NOMEM;
|
||||
}
|
||||
|
|
@ -257,6 +291,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
|
|||
naut_metainfo_free(out);
|
||||
return NAUT_ERR_NOMEM;
|
||||
}
|
||||
/* Magnet tr= parameters have no tier metadata. Treat them as
|
||||
* ordered failover entries instead of announcing to all at once. */
|
||||
out->tracker_tiers[i] = (uint32_t)i;
|
||||
}
|
||||
out->num_trackers = num_trackers;
|
||||
}
|
||||
|
|
@ -270,6 +307,7 @@ void naut_metainfo_free(naut_metainfo *mi) {
|
|||
free(mi->files);
|
||||
for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]);
|
||||
free(mi->trackers);
|
||||
free(mi->tracker_tiers);
|
||||
if (mi->_owned) {
|
||||
owned *o = mi->_owned;
|
||||
naut_bc_free(o->doc);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ struct naut_download {
|
|||
bool *file_done;
|
||||
naut_file_complete_cb file_cb;
|
||||
void *file_cb_ctx;
|
||||
naut_piece_complete_cb piece_cb;
|
||||
void *piece_cb_ctx;
|
||||
};
|
||||
|
||||
static bool bget(const uint8_t *a, uint32_t i) { return (a[i>>3] >> (i&7)) & 1; }
|
||||
|
|
@ -157,6 +159,11 @@ void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool) {
|
|||
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx) {
|
||||
d->file_cb = cb; d->file_cb_ctx = ctx;
|
||||
}
|
||||
void naut_download_set_piece_cb(naut_download *d, naut_piece_complete_cb cb,
|
||||
void *ctx) {
|
||||
d->piece_cb = cb;
|
||||
d->piece_cb_ctx = ctx;
|
||||
}
|
||||
bool naut_download_file_complete(const naut_download *d, uint32_t f) {
|
||||
return f < d->num_files && d->file_done[f];
|
||||
}
|
||||
|
|
@ -300,6 +307,7 @@ static naut_err finish_verified(naut_download *d, uint32_t p,
|
|||
d->bytes_done += ps;
|
||||
free_ps(d, p);
|
||||
*done = true;
|
||||
if (d->piece_cb) d->piece_cb(d->piece_cb_ctx, p);
|
||||
notify_files(d, p);
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ typedef struct {
|
|||
void *handle;
|
||||
char *path;
|
||||
char *name;
|
||||
naut_plugin_shutdown_fn shutdown;
|
||||
uint64_t *subscriptions;
|
||||
size_t subscription_count;
|
||||
size_t subscription_capacity;
|
||||
|
|
@ -209,6 +210,36 @@ static void host_log(void *opaque, int level, const char *message) {
|
|||
else NAUT_INFO("plugin: %s", message);
|
||||
}
|
||||
|
||||
static naut_err host_call_rpc(void *opaque, const char *method,
|
||||
const char *request_json,
|
||||
char **response_json) {
|
||||
naut_plugin_manager *manager = opaque;
|
||||
if (!manager || !method || !*method || !response_json)
|
||||
return NAUT_ERR_INVAL;
|
||||
*response_json = NULL;
|
||||
json_t *params = NULL;
|
||||
if (request_json && *request_json) {
|
||||
json_error_t json_error;
|
||||
params = json_loads(request_json,
|
||||
JSON_REJECT_DUPLICATES | JSON_DECODE_ANY,
|
||||
&json_error);
|
||||
if (!params) return NAUT_ERR_PROTO;
|
||||
}
|
||||
naut_err error = NAUT_OK;
|
||||
json_t *result = naut_rpc_dispatch(manager->rpc, method, params, &error);
|
||||
json_decref(params);
|
||||
if (error != NAUT_OK) {
|
||||
json_decref(result);
|
||||
return error;
|
||||
}
|
||||
char *text = json_dumps(result ? result : json_null(),
|
||||
JSON_COMPACT | JSON_ENCODE_ANY);
|
||||
json_decref(result);
|
||||
if (!text) return NAUT_ERR_NOMEM;
|
||||
*response_json = text;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
naut_plugin_manager *naut_plugin_manager_create(
|
||||
naut_rpc_registry *rpc, naut_event_bus *events) {
|
||||
if (!rpc || !events) return NULL;
|
||||
|
|
@ -225,6 +256,7 @@ void naut_plugin_manager_destroy(naut_plugin_manager *manager) {
|
|||
naut_rpc_unregister(manager->rpc, manager->rpc_adapters[i]->method);
|
||||
for (size_t i = 0; i < manager->plugin_count; i++) {
|
||||
loaded_plugin *plugin = &manager->plugins[i];
|
||||
if (plugin->shutdown) plugin->shutdown();
|
||||
for (size_t s = 0; s < plugin->subscription_count; s++)
|
||||
naut_event_unsubscribe(manager->events, plugin->subscriptions[s]);
|
||||
free(plugin->subscriptions);
|
||||
|
|
@ -280,6 +312,10 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
|
|||
memset(plugin, 0, sizeof(*plugin));
|
||||
return NAUT_ERR_PROTO;
|
||||
}
|
||||
dlerror();
|
||||
plugin->shutdown = (naut_plugin_shutdown_fn)dlsym(plugin->handle,
|
||||
"naut_plugin_shutdown");
|
||||
dlerror();
|
||||
naut_host_api host = {
|
||||
.abi_version = NAUT_PLUGIN_ABI_VERSION,
|
||||
.struct_size = sizeof(host),
|
||||
|
|
@ -290,6 +326,7 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
|
|||
.subscribe_event = host_subscribe_event,
|
||||
.emit_event = host_emit_event,
|
||||
.log = host_log,
|
||||
.call_rpc = host_call_rpc,
|
||||
};
|
||||
size_t rpc_start = manager->rpc_count;
|
||||
size_t event_start = manager->event_count;
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ typedef struct {
|
|||
int direct_fd;
|
||||
int64_t start; /* global offset of this file's first byte */
|
||||
int64_t length;
|
||||
char *path; /* full on-disk path (for relocate) */
|
||||
bool externalized; /* moved out; region no longer backed here */
|
||||
char *path; /* current on-disk path (updated by relocate) */
|
||||
} file_slot;
|
||||
|
||||
struct naut_storage {
|
||||
|
|
@ -140,7 +139,6 @@ static naut_err io_at(naut_storage *s, int64_t offset, void *buf, size_t len, bo
|
|||
while (len > 0) {
|
||||
const file_slot *f = locate(s, offset);
|
||||
if (!f) return NAUT_ERR_RANGE; /* zero-length file region */
|
||||
if (f->externalized) return NAUT_ERR_RANGE; /* moved out; not backed here */
|
||||
off_t fo = (off_t)(offset - f->start);
|
||||
size_t chunk = len;
|
||||
int64_t avail = f->length - fo;
|
||||
|
|
@ -193,11 +191,31 @@ done:
|
|||
return e;
|
||||
}
|
||||
|
||||
static naut_err reopen_slot(file_slot *file, const char *path,
|
||||
bool direct_io) {
|
||||
file->fd = open(path, O_RDWR);
|
||||
if (file->fd < 0) return NAUT_ERR_IO;
|
||||
#ifdef O_DIRECT
|
||||
if (direct_io && file->length > 0) {
|
||||
file->direct_fd = open(path, O_RDWR | O_DIRECT);
|
||||
if (file->direct_fd < 0)
|
||||
NAUT_WARN("O_DIRECT reopen unavailable for %s: %s", path,
|
||||
strerror(errno));
|
||||
}
|
||||
#else
|
||||
(void)direct_io;
|
||||
#endif
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest) {
|
||||
if (!s || !dest || !*dest) return NAUT_ERR_INVAL;
|
||||
if (file_index >= s->nfiles) return NAUT_ERR_RANGE;
|
||||
file_slot *f = &s->files[file_index];
|
||||
if (f->externalized) return NAUT_ERR_INVAL;
|
||||
|
||||
char *newpath = strdup(dest);
|
||||
if (!newpath) return NAUT_ERR_NOMEM;
|
||||
bool had_direct = f->direct_fd >= 0;
|
||||
if (f->direct_fd >= 0) {
|
||||
fsync(f->direct_fd);
|
||||
close(f->direct_fd);
|
||||
|
|
@ -207,17 +225,45 @@ naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *d
|
|||
|
||||
/* ensure the destination directory exists */
|
||||
char dcopy[4096];
|
||||
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) return NAUT_ERR_INVAL;
|
||||
if (make_parents(dcopy) != NAUT_OK) return NAUT_ERR_IO;
|
||||
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) {
|
||||
free(newpath);
|
||||
(void)reopen_slot(f, f->path, had_direct);
|
||||
return NAUT_ERR_INVAL;
|
||||
}
|
||||
if (make_parents(dcopy) != NAUT_OK) {
|
||||
free(newpath);
|
||||
(void)reopen_slot(f, f->path, had_direct);
|
||||
return NAUT_ERR_IO;
|
||||
}
|
||||
|
||||
if (rename(f->path, dest) != 0) {
|
||||
if (errno != EXDEV) { NAUT_ERROR("rename %s -> %s: %s", f->path, dest, strerror(errno)); return NAUT_ERR_IO; }
|
||||
if (errno != EXDEV) {
|
||||
NAUT_ERROR("rename %s -> %s: %s", f->path, dest,
|
||||
strerror(errno));
|
||||
free(newpath);
|
||||
(void)reopen_slot(f, f->path, had_direct);
|
||||
return NAUT_ERR_IO;
|
||||
}
|
||||
naut_err e = copy_file(f->path, dest); /* cross-filesystem */
|
||||
if (e != NAUT_OK) return e;
|
||||
if (e != NAUT_OK) {
|
||||
free(newpath);
|
||||
(void)reopen_slot(f, f->path, had_direct);
|
||||
return e;
|
||||
}
|
||||
if (unlink(f->path) != 0) NAUT_WARN("unlink %s after copy: %s", f->path, strerror(errno));
|
||||
}
|
||||
f->externalized = true;
|
||||
NAUT_INFO("relocated file %zu -> %s", file_index, dest);
|
||||
|
||||
/* Keep tracking the file at its new home: update the path and reopen so the
|
||||
* engine can still read/write/seed it from the new location (no externalize,
|
||||
* so the owning process never loses track of a moved file). */
|
||||
free(f->path);
|
||||
f->path = newpath;
|
||||
|
||||
if (reopen_slot(f, dest, had_direct) != NAUT_OK) {
|
||||
NAUT_ERROR("reopen %s after move: %s", dest, strerror(errno));
|
||||
return NAUT_ERR_IO;
|
||||
}
|
||||
NAUT_INFO("relocated file %zu -> %s (still tracked)", file_index, dest);
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue