webui: store RSS feeds/rules/indexers in the webui DB

Move RSS persistence out of the daemon blob store and into the webui's
own SQLite DB (feeds, rules, indexers tables; articles + affectedFeeds
held as JSON columns). Remove the now-unused daemon blob store
(set/get_webui_blob, blob_lock, data_dir).

With this, all webui-owned state — accounts, taxonomy, RSS — lives in
the webui DB; the daemon only keeps naut's own data (per-torrent labels
still flow through set_labels for Lua).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-23 22:11:17 -04:00
parent 9474e6f31a
commit 91d4b99aee
4 changed files with 178 additions and 96 deletions

View file

@ -114,8 +114,6 @@ struct daemon_state {
json_t *script_settings_schema; /* array of {key,label,type,default} */
json_t *script_settings; /* object: key -> value string (user-set)*/
char settings_file[PATH_MAX]; /* <state_dir>/script_settings.json */
char data_dir[PATH_MAX]; /* the resolved state dir (for blobs) */
pthread_mutex_t blob_lock; /* guards webui_<key>.json blob files */
pthread_mutex_t torrent_lock;
torrent_task *torrents[MAX_TORRENTS];
size_t torrent_count;
@ -1313,70 +1311,6 @@ static json_t *rpc_set_script_settings(void *opaque, const json_t *params,
return script_status_json(state);
}
/* --- generic web-UI blob store (RSS feeds, indexer config, ...) ------------ *
* The web layer owns these schemas; the daemon only persists them, one JSON
* document per key, under <state_dir>/webui_<key>.json. Keys are sanitized to a
* safe filename charset so a key can never escape the state directory. */
static bool blob_path(daemon_state *state, const char *key, char *out, size_t n) {
if (!key || !*key || !state->data_dir[0]) return false;
char safe[64];
size_t j = 0;
for (size_t i = 0; key[i] && j + 1 < sizeof safe; i++) {
char c = key[i];
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-')
safe[j++] = c;
}
safe[j] = 0;
if (j == 0) return false;
return (size_t)snprintf(out, n, "%s/webui_%s.json", state->data_dir, safe) < n;
}
static json_t *rpc_get_webui_blob(void *opaque, const json_t *params,
naut_err *error) {
daemon_state *state = opaque;
const char *key = json_string_value(json_object_get(params, "key"));
char path[PATH_MAX];
if (!blob_path(state, key, path, sizeof path)) {
*error = NAUT_ERR_INVAL;
return NULL;
}
pthread_mutex_lock(&state->blob_lock);
json_error_t jerr;
json_t *value = json_load_file(path, 0, &jerr);
pthread_mutex_unlock(&state->blob_lock);
json_t *out = json_object();
if (!out) { json_decref(value); *error = NAUT_ERR_NOMEM; return NULL; }
json_object_set_new(out, "value", value ? value : json_null());
*error = NAUT_OK;
return out;
}
static json_t *rpc_set_webui_blob(void *opaque, const json_t *params,
naut_err *error) {
daemon_state *state = opaque;
const char *key = json_string_value(json_object_get(params, "key"));
json_t *value = json_object_get(params, "value");
char path[PATH_MAX];
if (!value || !blob_path(state, key, path, sizeof path)) {
*error = NAUT_ERR_INVAL;
return NULL;
}
*error = NAUT_OK;
if (!state->persist_enabled) return json_object();
pthread_mutex_lock(&state->blob_lock);
char tmp[PATH_MAX + 8];
snprintf(tmp, sizeof tmp, "%s.tmp", path);
if (json_dump_file(value, tmp, JSON_INDENT(2)) != 0 ||
rename(tmp, path) != 0) {
NAUT_WARN("persist: write %s failed", path);
unlink(tmp);
}
pthread_mutex_unlock(&state->blob_lock);
return json_object();
}
/* --- data-overlap guard (block torrents that would write the same files) --- */
static uint8_t *slurp_file(const char *path, size_t *len) {
@ -2303,8 +2237,6 @@ static bool register_commands(daemon_state *state) {
naut_rpc_register(state->rpc, "script_status", rpc_script_status, state) == NAUT_OK &&
naut_rpc_register(state->rpc, "update_script", rpc_update_script, state) == NAUT_OK &&
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
naut_rpc_register(state->rpc, "get_webui_blob", rpc_get_webui_blob, state) == NAUT_OK &&
naut_rpc_register(state->rpc, "set_webui_blob", rpc_set_webui_blob, state) == NAUT_OK &&
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
}
@ -2593,9 +2525,6 @@ static bool resolve_state_dir(daemon_state *state, const char *override) {
"%s/script_settings.json", dir) >=
sizeof state->settings_file)
return false;
if ((size_t)snprintf(state->data_dir, sizeof state->data_dir, "%s", dir) >=
sizeof state->data_dir)
return false;
return true;
}
@ -2701,7 +2630,6 @@ int main(int argc, char **argv) {
pthread_mutex_init(&state.script_lock, NULL);
pthread_mutex_init(&state.settings_lock, NULL);
load_script_settings(&state); /* user-set values; schema comes from the script */
pthread_mutex_init(&state.blob_lock, NULL);
state.events = naut_event_bus_create();
state.rpc = naut_rpc_registry_create();
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
@ -2784,6 +2712,5 @@ int main(int argc, char **argv) {
json_decref(state.script_settings);
json_decref(state.script_settings_schema);
pthread_mutex_destroy(&state.settings_lock);
pthread_mutex_destroy(&state.blob_lock);
return 0;
}