webui: fix add-torrent crash, add categories/tags, real names
The daemon segfaulted the moment any torrent existed: map_torrent built its JSON with one 30-key json_pack whose format string had drifted out of sync with the argument list, so json_pack misread an int as a char* and crashed in the next snapshot build (an empty fleet hid it). Rebuild the object field-by-field with json_object_set_new so it can't drift again. Add a web-layer category/tag store (in memory, like qBittorrent's own Web API) so the UI can actually create categories and tags and assign them: - /api/categories[/delete] and /api/tags[/delete] persist and return them - /api/meta returns the stored categories/tags - /api/action handles setCategory/addTags/removeTags (still 501 for engine-level verbs the daemon can't do) - map_torrent fills each torrent's category/tags from the store Uploaded torrents kept their temp upload path as the display name; keep the name the UI sends at add time and prefer it in the grid. Fix a use-after-free in api_action that read the action string after freeing the request, which corrupted the error body into a 500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
41ed172272
commit
f4f4e86be4
1 changed files with 336 additions and 63 deletions
|
|
@ -81,6 +81,14 @@ typedef struct {
|
||||||
pthread_mutex_t speed_lock;
|
pthread_mutex_t speed_lock;
|
||||||
speed_slot speeds[SPEED_SLOTS];
|
speed_slot speeds[SPEED_SLOTS];
|
||||||
|
|
||||||
|
/* Categories and tags are pure UI organization the engine knows nothing
|
||||||
|
* about, so the web layer owns them (in memory, like qBittorrent's own
|
||||||
|
* Web API does). assignments maps "<id>" -> {category, tags:[...]}. */
|
||||||
|
pthread_mutex_t meta_lock;
|
||||||
|
json_t *categories; /* array of {name, savePath} */
|
||||||
|
json_t *tags; /* array of tag name strings */
|
||||||
|
json_t *assignments; /* object keyed by stringified torrent id */
|
||||||
|
|
||||||
webui_session sessions[MAX_SESSIONS];
|
webui_session sessions[MAX_SESSIONS];
|
||||||
} webui_state;
|
} webui_state;
|
||||||
|
|
||||||
|
|
@ -580,6 +588,158 @@ static json_int_t compute_eta(uint64_t done, uint64_t total, double dlspeed) {
|
||||||
return ETA_INFINITY;
|
return ETA_INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- category / tag store (web-layer owned, guarded by meta_lock) ---- */
|
||||||
|
|
||||||
|
static int find_category(const char *name) {
|
||||||
|
size_t index;
|
||||||
|
json_t *value;
|
||||||
|
json_array_foreach(g_webui.categories, index, value)
|
||||||
|
if (strcmp(json_string_or(value, "name", ""), name) == 0)
|
||||||
|
return (int)index;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int find_tag(const char *name) {
|
||||||
|
size_t index;
|
||||||
|
json_t *value;
|
||||||
|
json_array_foreach(g_webui.tags, index, value)
|
||||||
|
if (strcmp(json_string_value(value), name) == 0) return (int)index;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_add_category(const char *name, const char *save_path) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
if (find_category(name) < 0)
|
||||||
|
json_array_append_new(g_webui.categories, json_pack(
|
||||||
|
"{s:s,s:s}", "name", name, "savePath", save_path ? save_path : ""));
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_remove_category(const char *name) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
int index = find_category(name);
|
||||||
|
if (index >= 0) json_array_remove(g_webui.categories, (size_t)index);
|
||||||
|
/* drop the category from any torrent that had it */
|
||||||
|
const char *key;
|
||||||
|
json_t *entry;
|
||||||
|
json_object_foreach(g_webui.assignments, key, entry)
|
||||||
|
if (strcmp(json_string_or(entry, "category", ""), name) == 0)
|
||||||
|
json_object_set_new(entry, "category", json_string(""));
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_add_tag(const char *name) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
if (find_tag(name) < 0)
|
||||||
|
json_array_append_new(g_webui.tags, json_string(name));
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_remove_tag(const char *name) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
int index = find_tag(name);
|
||||||
|
if (index >= 0) json_array_remove(g_webui.tags, (size_t)index);
|
||||||
|
const char *key;
|
||||||
|
json_t *entry;
|
||||||
|
json_object_foreach(g_webui.assignments, key, entry) {
|
||||||
|
json_t *tags = json_object_get(entry, "tags");
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < json_array_size(tags)) {
|
||||||
|
if (strcmp(json_string_value(json_array_get(tags, i)), name) == 0)
|
||||||
|
json_array_remove(tags, i);
|
||||||
|
else
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t *assignment_locked(uint64_t id, bool create) {
|
||||||
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
|
||||||
|
json_t *entry = json_object_get(g_webui.assignments, key);
|
||||||
|
if (!entry && create) {
|
||||||
|
entry = json_pack("{s:s,s:o}", "category", "", "tags", json_array());
|
||||||
|
json_object_set_new(g_webui.assignments, key, entry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_set_category(uint64_t id, const char *category) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *entry = assignment_locked(id, true);
|
||||||
|
if (entry)
|
||||||
|
json_object_set_new(entry, "category",
|
||||||
|
json_string(category ? category : ""));
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_update_tags(uint64_t id, json_t *tags, bool add) {
|
||||||
|
if (!json_is_array(tags)) return;
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *entry = assignment_locked(id, true);
|
||||||
|
json_t *have = entry ? json_object_get(entry, "tags") : NULL;
|
||||||
|
if (have) {
|
||||||
|
size_t index;
|
||||||
|
json_t *value;
|
||||||
|
json_array_foreach(tags, index, value) {
|
||||||
|
const char *name = json_string_value(value);
|
||||||
|
if (!name) continue;
|
||||||
|
size_t pos = 0;
|
||||||
|
bool present = false;
|
||||||
|
for (; pos < json_array_size(have); pos++)
|
||||||
|
if (strcmp(json_string_value(json_array_get(have, pos)),
|
||||||
|
name) == 0) { present = true; break; }
|
||||||
|
if (add && !present)
|
||||||
|
json_array_append_new(have, json_string(name));
|
||||||
|
else if (!add && present)
|
||||||
|
json_array_remove(have, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_set_name(uint64_t id, const char *name) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *entry = assignment_locked(id, true);
|
||||||
|
if (entry) json_object_set_new(entry, "name", json_string(name));
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool store_get_name(uint64_t id, char *out, size_t out_size) {
|
||||||
|
bool found = false;
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *entry = assignment_locked(id, false);
|
||||||
|
const char *name = entry ? json_string_value(json_object_get(entry, "name"))
|
||||||
|
: NULL;
|
||||||
|
if (name && *name) {
|
||||||
|
snprintf(out, out_size, "%s", name);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store_forget(uint64_t id) {
|
||||||
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_object_del(g_webui.assignments, key);
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fill in category + tags for a torrent from the assignment store. */
|
||||||
|
static void apply_assignment(json_t *out, uint64_t id) {
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *entry = assignment_locked(id, false);
|
||||||
|
const char *category = entry ? json_string_or(entry, "category", "") : "";
|
||||||
|
json_t *tags = entry ? json_object_get(entry, "tags") : NULL;
|
||||||
|
json_object_set_new(out, "category", json_string(category));
|
||||||
|
json_object_set_new(out, "tags",
|
||||||
|
tags ? json_deep_copy(tags) : json_array());
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
}
|
||||||
|
|
||||||
static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
||||||
uint64_t id = json_u64(torrent, "torrent_id");
|
uint64_t id = json_u64(torrent, "torrent_id");
|
||||||
uint64_t done = 0, total = 0;
|
uint64_t done = 0, total = 0;
|
||||||
|
|
@ -591,6 +751,13 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
||||||
snprintf(hash, sizeof hash, "%llu", (unsigned long long)id);
|
snprintf(hash, sizeof hash, "%llu", (unsigned long long)id);
|
||||||
char *name = torrent_name(torrent);
|
char *name = torrent_name(torrent);
|
||||||
if (!name) return NULL;
|
if (!name) return NULL;
|
||||||
|
/* Prefer the display name the UI captured at add time over the daemon's
|
||||||
|
* temp upload path. */
|
||||||
|
char override[256];
|
||||||
|
if (store_get_name(id, override, sizeof override)) {
|
||||||
|
char *better = strdup(override);
|
||||||
|
if (better) { free(name); name = better; }
|
||||||
|
}
|
||||||
const char *state = ui_state(json_string_value(json_object_get(torrent,
|
const char *state = ui_state(json_string_value(json_object_get(torrent,
|
||||||
"state")),
|
"state")),
|
||||||
progress);
|
progress);
|
||||||
|
|
@ -618,40 +785,57 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
||||||
hosts = tracker_hosts(trackers);
|
hosts = tracker_hosts(trackers);
|
||||||
}
|
}
|
||||||
|
|
||||||
json_t *out = json_pack(
|
uint64_t discovered = json_u64(torrent, "peers_discovered");
|
||||||
"{s:s,s:s,s:I,s:f,s:I,s:i,s:I,s:i,s:i,s:i,s:f,s:s,s:o,s:s,"
|
const char *output = json_string_or(torrent, "output", "");
|
||||||
"s:I,s:I,s:I,s:I,s:f,s:i,s:o,s:b,s:b,s:b,s:I,s:I,s:s,s:s}",
|
/* Built field-by-field on purpose: a single 30-key json_pack drifts out of
|
||||||
"hash", hash,
|
* sync with its argument list silently and then crashes on a type mismatch. */
|
||||||
"name", name,
|
json_t *out = json_object();
|
||||||
"size", (json_int_t)total,
|
if (!out) {
|
||||||
"progress", progress,
|
free(name);
|
||||||
"dlspeed", (json_int_t)dlspeed,
|
if (detail) {
|
||||||
"upspeed", 0,
|
json_decref(trackers);
|
||||||
"eta", compute_eta(done, total, dlspeed),
|
json_decref(files);
|
||||||
"seeds", (int)json_u64(torrent, "peers"),
|
json_decref(peers_list);
|
||||||
"seedsTotal", (int)json_u64(torrent, "peers_discovered"),
|
json_decref(hosts);
|
||||||
"peers", (int)json_u64(torrent, "peers_connecting"),
|
}
|
||||||
"peersTotal", (int)json_u64(torrent, "peers_discovered"),
|
return NULL;
|
||||||
"ratio", 0.0,
|
}
|
||||||
"category", "",
|
json_object_set_new(out, "hash", json_string(hash));
|
||||||
"tags", json_array(),
|
json_object_set_new(out, "name", json_string(name));
|
||||||
"savePath", json_string_or(torrent, "output", ""),
|
json_object_set_new(out, "size", json_integer((json_int_t)total));
|
||||||
"addedOn", (json_int_t)0,
|
json_object_set_new(out, "progress", json_real(progress));
|
||||||
"completionOn", progress >= 1.0 ? (json_int_t)0 : (json_int_t)-1,
|
json_object_set_new(out, "dlspeed", json_integer((json_int_t)dlspeed));
|
||||||
"lastActivity", (json_int_t)0,
|
json_object_set_new(out, "upspeed", json_integer(0));
|
||||||
"downloaded", (json_int_t)done,
|
json_object_set_new(out, "eta", json_integer(compute_eta(done, total, dlspeed)));
|
||||||
"uploaded", (json_int_t)0,
|
json_object_set_new(out, "seeds",
|
||||||
"availability", 1.0,
|
json_integer((json_int_t)json_u64(torrent, "peers")));
|
||||||
"priority", 1,
|
json_object_set_new(out, "seedsTotal", json_integer((json_int_t)discovered));
|
||||||
"trackerHosts", hosts ? hosts : json_array(),
|
json_object_set_new(out, "peers",
|
||||||
"seqDl", false,
|
json_integer((json_int_t)json_u64(torrent, "peers_connecting")));
|
||||||
"superSeeding", false,
|
json_object_set_new(out, "peersTotal", json_integer((json_int_t)discovered));
|
||||||
"forceStart", false,
|
json_object_set_new(out, "ratio", json_real(0.0));
|
||||||
"timeActive", (json_int_t)json_u64(torrent, "elapsed_seconds"),
|
json_object_set_new(out, "savePath", json_string(output));
|
||||||
"pieceSize", pieces ? (json_int_t)(total / pieces) : (json_int_t)0,
|
json_object_set_new(out, "addedOn", json_integer(0));
|
||||||
"state", state,
|
json_object_set_new(out, "completionOn",
|
||||||
"contentPath", json_string_or(torrent, "output", ""));
|
json_integer(progress >= 1.0 ? 0 : -1));
|
||||||
if (out && detail) {
|
json_object_set_new(out, "lastActivity", json_integer(0));
|
||||||
|
json_object_set_new(out, "downloaded", json_integer((json_int_t)done));
|
||||||
|
json_object_set_new(out, "uploaded", json_integer(0));
|
||||||
|
json_object_set_new(out, "availability", json_real(1.0));
|
||||||
|
json_object_set_new(out, "priority", json_integer(1));
|
||||||
|
json_object_set_new(out, "trackerHosts", hosts ? hosts : json_array());
|
||||||
|
json_object_set_new(out, "seqDl", json_false());
|
||||||
|
json_object_set_new(out, "superSeeding", json_false());
|
||||||
|
json_object_set_new(out, "forceStart", json_false());
|
||||||
|
json_object_set_new(out, "timeActive",
|
||||||
|
json_integer((json_int_t)json_u64(torrent, "elapsed_seconds")));
|
||||||
|
json_object_set_new(out, "pieceSize",
|
||||||
|
json_integer(pieces ? (json_int_t)(total / pieces) : 0));
|
||||||
|
json_object_set_new(out, "state", json_string(state));
|
||||||
|
json_object_set_new(out, "contentPath", json_string(output));
|
||||||
|
/* category + tags come from the web-layer assignment store */
|
||||||
|
apply_assignment(out, id);
|
||||||
|
if (detail) {
|
||||||
json_object_set_new(out, "comment", json_string(""));
|
json_object_set_new(out, "comment", json_string(""));
|
||||||
json_object_set_new(out, "createdBy", json_string("Naut"));
|
json_object_set_new(out, "createdBy", json_string("Naut"));
|
||||||
json_object_set_new(out, "creationDate", json_integer(0));
|
json_object_set_new(out, "creationDate", json_integer(0));
|
||||||
|
|
@ -663,11 +847,6 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
|
||||||
json_object_set_new(out, "trackers", trackers ? trackers : json_array());
|
json_object_set_new(out, "trackers", trackers ? trackers : json_array());
|
||||||
json_object_set_new(out, "peersList", peers_list ? peers_list : json_array());
|
json_object_set_new(out, "peersList", peers_list ? peers_list : json_array());
|
||||||
json_object_set_new(out, "files", files ? files : json_array());
|
json_object_set_new(out, "files", files ? files : json_array());
|
||||||
} else if (detail) {
|
|
||||||
json_decref(trackers);
|
|
||||||
json_decref(files);
|
|
||||||
json_decref(peers_list);
|
|
||||||
json_decref(hosts);
|
|
||||||
}
|
}
|
||||||
free(name);
|
free(name);
|
||||||
return out;
|
return out;
|
||||||
|
|
@ -791,8 +970,11 @@ static void api_meta(int fd) {
|
||||||
http_text(fd, 500, "Internal Server Error", "oom");
|
http_text(fd, 500, "Internal Server Error", "oom");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
json_object_set_new(json, "categories", json_array());
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
json_object_set_new(json, "tags", json_array());
|
json_object_set_new(json, "categories",
|
||||||
|
json_deep_copy(g_webui.categories));
|
||||||
|
json_object_set_new(json, "tags", json_deep_copy(g_webui.tags));
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
json_object_set_new(json, "trackers", json_array());
|
json_object_set_new(json, "trackers", json_array());
|
||||||
json_object_set_new(preferences, "save_path",
|
json_object_set_new(preferences, "save_path",
|
||||||
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
|
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
|
||||||
|
|
@ -894,6 +1076,12 @@ static void api_add(int fd, const char *body, size_t len) {
|
||||||
const char *magnet = json_string_value(json_object_get(req, "magnet"));
|
const char *magnet = json_string_value(json_object_get(req, "magnet"));
|
||||||
const char *data = json_string_value(json_object_get(req, "data"));
|
const char *data = json_string_value(json_object_get(req, "data"));
|
||||||
const char *save_path = json_string_value(json_object_get(req, "savePath"));
|
const char *save_path = json_string_value(json_object_get(req, "savePath"));
|
||||||
|
/* The UI parses the .torrent client-side and sends a display name; the
|
||||||
|
* daemon only knows the temp upload path, so we keep the name here. */
|
||||||
|
const char *display = json_string_value(json_object_get(req, "name"));
|
||||||
|
char display_name[256] = {0};
|
||||||
|
if (display && *display)
|
||||||
|
snprintf(display_name, sizeof display_name, "%s", display);
|
||||||
if (!save_path || !*save_path) save_path = ".";
|
if (!save_path || !*save_path) save_path = ".";
|
||||||
if (!source) source = magnet;
|
if (!source) source = magnet;
|
||||||
if ((!source || !*source) && (!data || !*data)) {
|
if ((!source || !*source) && (!data || !*data)) {
|
||||||
|
|
@ -913,9 +1101,10 @@ static void api_add(int fd, const char *body, size_t len) {
|
||||||
http_text(fd, 502, "Bad Gateway", "add_torrent failed");
|
http_text(fd, 502, "Bad Gateway", "add_torrent failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
uint64_t new_id = json_u64(result, "torrent_id");
|
||||||
|
if (display_name[0]) store_set_name(new_id, display_name);
|
||||||
char id[32];
|
char id[32];
|
||||||
snprintf(id, sizeof id, "%llu",
|
snprintf(id, sizeof id, "%llu", (unsigned long long)new_id);
|
||||||
(unsigned long long)json_u64(result, "torrent_id"));
|
|
||||||
json_t *reply = json_pack("{s:b,s:s}", "ok", 1, "hash", id);
|
json_t *reply = json_pack("{s:b,s:s}", "ok", 1, "hash", id);
|
||||||
http_json(fd, 200, reply);
|
http_json(fd, 200, reply);
|
||||||
json_decref(reply);
|
json_decref(reply);
|
||||||
|
|
@ -939,6 +1128,7 @@ static void api_delete(int fd, const char *body, size_t len) {
|
||||||
json_decref(params);
|
json_decref(params);
|
||||||
if (result) {
|
if (result) {
|
||||||
removed++;
|
removed++;
|
||||||
|
store_forget(id);
|
||||||
json_decref(result);
|
json_decref(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -950,22 +1140,85 @@ static void api_delete(int fd, const char *body, size_t len) {
|
||||||
if (removed) publish_snapshot();
|
if (removed) publish_snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The engine has no pause/resume/recheck/queue/category/limit verbs yet, so
|
/* Category/tag assignment is web-layer state the plugin owns, so those verbs
|
||||||
* rather than claim success we tell the UI the action is unsupported. The
|
* are honored here. Engine-level verbs (pause/resume/recheck/queue/rate
|
||||||
* front end surfaces a non-2xx as an honest "Action failed" toast. */
|
* limits) have no daemon support yet, so we return 501 instead of pretending
|
||||||
|
* they worked; the front end shows that as an honest "Action failed" toast. */
|
||||||
static void api_action(int fd, const char *body, size_t len) {
|
static void api_action(int fd, const char *body, size_t len) {
|
||||||
json_t *req = read_body_json(body, len);
|
json_t *req = read_body_json(body, len);
|
||||||
const char *action = json_string_value(json_object_get(req, "action"));
|
const char *raw_action = json_string_value(json_object_get(req, "action"));
|
||||||
|
char action[64];
|
||||||
|
snprintf(action, sizeof action, "%s", raw_action ? raw_action : "");
|
||||||
|
json_t *hashes = json_object_get(req, "hashes");
|
||||||
|
json_t *params = json_object_get(req, "params");
|
||||||
|
bool handled = false;
|
||||||
|
if (json_is_array(hashes) &&
|
||||||
|
(strcmp(action, "setCategory") == 0 ||
|
||||||
|
strcmp(action, "addTags") == 0 ||
|
||||||
|
strcmp(action, "removeTags") == 0)) {
|
||||||
|
size_t index;
|
||||||
|
json_t *hash;
|
||||||
|
json_array_foreach(hashes, index, hash) {
|
||||||
|
uint64_t id = 0;
|
||||||
|
if (!parse_id(json_string_value(hash), &id)) continue;
|
||||||
|
if (strcmp(action, "setCategory") == 0)
|
||||||
|
store_set_category(id,
|
||||||
|
json_string_value(json_object_get(params, "category")));
|
||||||
|
else
|
||||||
|
store_update_tags(id, json_object_get(params, "tags"),
|
||||||
|
strcmp(action, "addTags") == 0);
|
||||||
|
}
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
json_decref(req);
|
||||||
|
if (handled) {
|
||||||
|
json_t *json = json_pack("{s:b}", "ok", 1);
|
||||||
|
http_json(fd, 200, json);
|
||||||
|
json_decref(json);
|
||||||
|
publish_snapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
char message[128];
|
char message[128];
|
||||||
snprintf(message, sizeof message,
|
snprintf(message, sizeof message,
|
||||||
"action '%s' is not supported by the engine",
|
"action '%s' is not supported by the engine", action);
|
||||||
action ? action : "");
|
|
||||||
json_decref(req);
|
|
||||||
json_t *json = json_pack("{s:b,s:s}", "ok", 0, "error", message);
|
json_t *json = json_pack("{s:b,s:s}", "ok", 0, "error", message);
|
||||||
http_json(fd, 501, json);
|
http_json(fd, 501, json);
|
||||||
json_decref(json);
|
json_decref(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* POST /api/categories and /api/categories/delete */
|
||||||
|
static void api_categories(int fd, const char *body, size_t len, bool remove) {
|
||||||
|
json_t *req = read_body_json(body, len);
|
||||||
|
const char *name = json_string_value(json_object_get(req, "name"));
|
||||||
|
if (name && *name) {
|
||||||
|
if (remove) store_remove_category(name);
|
||||||
|
else store_add_category(name,
|
||||||
|
json_string_value(json_object_get(req, "savePath")));
|
||||||
|
}
|
||||||
|
json_decref(req);
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *reply = json_deep_copy(g_webui.categories);
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
http_json(fd, 200, reply);
|
||||||
|
json_decref(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* POST /api/tags and /api/tags/delete */
|
||||||
|
static void api_tags(int fd, const char *body, size_t len, bool remove) {
|
||||||
|
json_t *req = read_body_json(body, len);
|
||||||
|
const char *name = json_string_value(json_object_get(req, "name"));
|
||||||
|
if (name && *name) {
|
||||||
|
if (remove) store_remove_tag(name);
|
||||||
|
else store_add_tag(name);
|
||||||
|
}
|
||||||
|
json_decref(req);
|
||||||
|
pthread_mutex_lock(&g_webui.meta_lock);
|
||||||
|
json_t *reply = json_deep_copy(g_webui.tags);
|
||||||
|
pthread_mutex_unlock(&g_webui.meta_lock);
|
||||||
|
http_json(fd, 200, reply);
|
||||||
|
json_decref(reply);
|
||||||
|
}
|
||||||
|
|
||||||
static void api_stream(int fd) {
|
static void api_stream(int fd) {
|
||||||
const char *head =
|
const char *head =
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n"
|
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n"
|
||||||
|
|
@ -1084,18 +1337,18 @@ static void handle_api(int fd, const char *method, char *path,
|
||||||
json_t *json = json_pack("{s:b}", "alt_speed_enabled", 0);
|
json_t *json = json_pack("{s:b}", "alt_speed_enabled", 0);
|
||||||
http_json(fd, 200, json);
|
http_json(fd, 200, json);
|
||||||
json_decref(json);
|
json_decref(json);
|
||||||
} else if ((strcmp(path, "/api/categories") == 0 ||
|
} else if (strcmp(path, "/api/categories") == 0 &&
|
||||||
strcmp(path, "/api/categories/delete") == 0) &&
|
|
||||||
strcmp(method, "POST") == 0) {
|
strcmp(method, "POST") == 0) {
|
||||||
json_t *json = json_array();
|
api_categories(fd, body, body_len, false);
|
||||||
http_json(fd, 200, json);
|
} else if (strcmp(path, "/api/categories/delete") == 0 &&
|
||||||
json_decref(json);
|
|
||||||
} else if ((strcmp(path, "/api/tags") == 0 ||
|
|
||||||
strcmp(path, "/api/tags/delete") == 0) &&
|
|
||||||
strcmp(method, "POST") == 0) {
|
strcmp(method, "POST") == 0) {
|
||||||
json_t *json = json_array();
|
api_categories(fd, body, body_len, true);
|
||||||
http_json(fd, 200, json);
|
} else if (strcmp(path, "/api/tags") == 0 &&
|
||||||
json_decref(json);
|
strcmp(method, "POST") == 0) {
|
||||||
|
api_tags(fd, body, body_len, false);
|
||||||
|
} else if (strcmp(path, "/api/tags/delete") == 0 &&
|
||||||
|
strcmp(method, "POST") == 0) {
|
||||||
|
api_tags(fd, body, body_len, true);
|
||||||
} else if (strcmp(path, "/api/torrents") == 0 && strcmp(method, "GET") == 0) {
|
} else if (strcmp(path, "/api/torrents") == 0 && strcmp(method, "GET") == 0) {
|
||||||
serve_cached_torrents(fd);
|
serve_cached_torrents(fd);
|
||||||
} else if (path_after(path, "/api/torrents/") && strcmp(method, "GET") == 0) {
|
} else if (path_after(path, "/api/torrents/") && strcmp(method, "GET") == 0) {
|
||||||
|
|
@ -1358,15 +1611,27 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
||||||
goto fail_snap_cond;
|
goto fail_snap_cond;
|
||||||
if (pthread_mutex_init(&g_webui.speed_lock, NULL) != 0)
|
if (pthread_mutex_init(&g_webui.speed_lock, NULL) != 0)
|
||||||
goto fail_speed_lock;
|
goto fail_speed_lock;
|
||||||
|
if (pthread_mutex_init(&g_webui.meta_lock, NULL) != 0)
|
||||||
|
goto fail_meta_lock;
|
||||||
|
g_webui.categories = json_array();
|
||||||
|
g_webui.tags = json_array();
|
||||||
|
g_webui.assignments = json_object();
|
||||||
|
if (!g_webui.categories || !g_webui.tags || !g_webui.assignments)
|
||||||
|
goto fail_store;
|
||||||
init_auth();
|
init_auth();
|
||||||
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
||||||
"webui");
|
"webui");
|
||||||
if (error != NAUT_OK) goto fail_named;
|
if (error != NAUT_OK) goto fail_store;
|
||||||
error = start_server();
|
error = start_server();
|
||||||
if (error != NAUT_OK) goto fail_named;
|
if (error != NAUT_OK) goto fail_store;
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
|
|
||||||
fail_named:
|
fail_store:
|
||||||
|
json_decref(g_webui.categories);
|
||||||
|
json_decref(g_webui.tags);
|
||||||
|
json_decref(g_webui.assignments);
|
||||||
|
pthread_mutex_destroy(&g_webui.meta_lock);
|
||||||
|
fail_meta_lock:
|
||||||
pthread_mutex_destroy(&g_webui.speed_lock);
|
pthread_mutex_destroy(&g_webui.speed_lock);
|
||||||
fail_speed_lock:
|
fail_speed_lock:
|
||||||
pthread_cond_destroy(&g_webui.snap_cond);
|
pthread_cond_destroy(&g_webui.snap_cond);
|
||||||
|
|
@ -1408,6 +1673,14 @@ naut_err naut_plugin_shutdown(void) {
|
||||||
g_webui.snapshot_str = NULL;
|
g_webui.snapshot_str = NULL;
|
||||||
g_webui.torrents_str = NULL;
|
g_webui.torrents_str = NULL;
|
||||||
|
|
||||||
|
json_decref(g_webui.categories);
|
||||||
|
json_decref(g_webui.tags);
|
||||||
|
json_decref(g_webui.assignments);
|
||||||
|
g_webui.categories = NULL;
|
||||||
|
g_webui.tags = NULL;
|
||||||
|
g_webui.assignments = NULL;
|
||||||
|
|
||||||
|
pthread_mutex_destroy(&g_webui.meta_lock);
|
||||||
pthread_mutex_destroy(&g_webui.speed_lock);
|
pthread_mutex_destroy(&g_webui.speed_lock);
|
||||||
pthread_cond_destroy(&g_webui.snap_cond);
|
pthread_cond_destroy(&g_webui.snap_cond);
|
||||||
pthread_mutex_destroy(&g_webui.snap_lock);
|
pthread_mutex_destroy(&g_webui.snap_lock);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue