nautd: block adding a torrent that would overlap existing data
spawn_torrent parses each torrent's file list (at add and restore) and refuses an add whose files would write where a registered torrent's data lives (NAUT_ERR_EXIST). Magnets are checked once metadata is known is out of scope; restore skips the check. webui surfaces it as HTTP 409. Mark #10 done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
187e8f2db2
commit
7bbc1ee22c
5 changed files with 149 additions and 8 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "naut/event.h"
|
||||
#include "naut/log.h"
|
||||
#include "naut/metainfo.h"
|
||||
#include "naut/plugin.h"
|
||||
#include "naut/rpc.h"
|
||||
#include "naut/script.h"
|
||||
|
|
@ -89,6 +90,8 @@ typedef struct {
|
|||
char *category; /* single category (qBittorrent-style), may be ""*/
|
||||
char **tags; /* user tags (multiple) */
|
||||
size_t num_tags; /* category + tags are the flat labels Lua sees */
|
||||
char **files; /* torrent's file relpaths (for overlap checks) */
|
||||
size_t num_files; /* parsed at add; empty for magnets pre-metadata */
|
||||
uint64_t dump_seq; /* bumped by a dump RPC; > dump_done_seq => pending */
|
||||
uint64_t dump_done_seq; /* highest dump_seq the worker has rendered */
|
||||
char *dump_text; /* latest rendered dump (owner: task) */
|
||||
|
|
@ -1381,10 +1384,100 @@ static json_t *rpc_set_label_taxonomy(void *opaque, const json_t *params,
|
|||
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) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
|
||||
long n = ftell(f);
|
||||
if (n < 0) { fclose(f); return NULL; }
|
||||
rewind(f);
|
||||
uint8_t *buf = malloc((size_t)n + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
size_t got = fread(buf, 1, (size_t)n, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)n) { free(buf); return NULL; }
|
||||
if (len) *len = got;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Parse a .torrent file's file list into relative paths. Returns the count (0
|
||||
* for magnets, which have no metadata yet, or on failure). Caller frees. */
|
||||
static size_t parse_torrent_files(const char *source, char ***out) {
|
||||
*out = NULL;
|
||||
if (!source || strncmp(source, "magnet:", 7) == 0) return 0;
|
||||
size_t len = 0;
|
||||
uint8_t *bytes = slurp_file(source, &len);
|
||||
if (!bytes) return 0;
|
||||
naut_metainfo mi;
|
||||
memset(&mi, 0, sizeof mi);
|
||||
if (naut_metainfo_parse(bytes, len, &mi) != NAUT_OK) { free(bytes); return 0; }
|
||||
free(bytes);
|
||||
char **files = mi.num_files ? calloc(mi.num_files, sizeof *files) : NULL;
|
||||
size_t c = 0;
|
||||
if (files)
|
||||
for (size_t i = 0; i < mi.num_files; i++)
|
||||
if (mi.files[i].path) {
|
||||
char *p = strdup(mi.files[i].path);
|
||||
if (p) files[c++] = p;
|
||||
}
|
||||
naut_metainfo_free(&mi);
|
||||
*out = files;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* Two absolute paths conflict if equal, or one is a directory-prefix of the
|
||||
* other (a file vs a folder that would contain it). */
|
||||
static bool paths_conflict(const char *a, const char *b) {
|
||||
size_t la = strlen(a), lb = strlen(b);
|
||||
if (la == lb) return strcmp(a, b) == 0;
|
||||
const char *shorter = la < lb ? a : b, *longer = la < lb ? b : a;
|
||||
size_t sl = la < lb ? la : lb;
|
||||
return strncmp(shorter, longer, sl) == 0 && longer[sl] == '/';
|
||||
}
|
||||
|
||||
/* Current absolute path of file `fi` (relpath `rel`) for `task`: its override if
|
||||
* individually moved, else output_dir/rel. Caller holds task->lock. */
|
||||
static void task_file_abs(const torrent_task *task, size_t fi, const char *rel,
|
||||
char *out, size_t outsz) {
|
||||
for (size_t i = 0; i < task->num_locations; i++)
|
||||
if (task->locations[i].file_index == fi) {
|
||||
snprintf(out, outsz, "%s", task->locations[i].path);
|
||||
return;
|
||||
}
|
||||
snprintf(out, outsz, "%s/%s", task->output_dir ? task->output_dir : "", rel);
|
||||
}
|
||||
|
||||
/* True if any of `news` (absolute paths) would write where an already-registered
|
||||
* torrent's file lives. Caller holds state->torrent_lock; locks each task. */
|
||||
static bool data_overlaps_locked(daemon_state *state, char *const *news,
|
||||
size_t nnew, char *conflict, size_t csz) {
|
||||
for (size_t t = 0; t < state->torrent_count; t++) {
|
||||
torrent_task *o = state->torrents[t];
|
||||
pthread_mutex_lock(&o->lock);
|
||||
bool removed = o->remove_requested;
|
||||
for (size_t fi = 0; !removed && fi < o->num_files; fi++) {
|
||||
char have[PATH_MAX];
|
||||
task_file_abs(o, fi, o->files[fi], have, sizeof have);
|
||||
for (size_t k = 0; k < nnew; k++)
|
||||
if (paths_conflict(have, news[k])) {
|
||||
snprintf(conflict, csz, "%s", have);
|
||||
pthread_mutex_unlock(&o->lock);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&o->lock);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build a torrent_task from validated inputs, register it under an id, and start
|
||||
* its worker. Returns the task (added to state->torrents) or NULL + *error. All
|
||||
* inputs are copied; the source file is never unlinked here (the caller owns
|
||||
* that decision so a failed restore does not delete a durable upload). */
|
||||
* that decision so a failed restore does not delete a durable upload). When
|
||||
* `check_overlap` is set, an add whose files would land on an existing torrent's
|
||||
* data is refused with NAUT_ERR_EXIST. */
|
||||
static torrent_task *spawn_torrent(daemon_state *state, const char *source,
|
||||
bool source_managed, bool source_is_temp,
|
||||
const char *output, const json_t *peers_json,
|
||||
|
|
@ -1392,7 +1485,8 @@ static torrent_task *spawn_torrent(daemon_state *state, const char *source,
|
|||
const json_t *meta_json,
|
||||
const json_t *id_opt, const char *name,
|
||||
bool start_paused, bool force_start,
|
||||
int queue_pos, naut_err *error) {
|
||||
int queue_pos, bool check_overlap,
|
||||
naut_err *error) {
|
||||
torrent_task *task = calloc(1, sizeof(*task));
|
||||
if (!task) { *error = NAUT_ERR_NOMEM; return NULL; }
|
||||
task->daemon = state;
|
||||
|
|
@ -1453,12 +1547,38 @@ static torrent_task *spawn_torrent(daemon_state *state, const char *source,
|
|||
}
|
||||
}
|
||||
|
||||
/* File list (for overlap detection); empty for magnets until metadata. */
|
||||
task->num_files = parse_torrent_files(source, &task->files);
|
||||
|
||||
pthread_mutex_lock(&state->torrent_lock);
|
||||
if (state->torrent_count == MAX_TORRENTS) {
|
||||
pthread_mutex_unlock(&state->torrent_lock);
|
||||
*error = NAUT_ERR_FULL;
|
||||
goto fail_task;
|
||||
}
|
||||
if (check_overlap && task->num_files) {
|
||||
char **news = calloc(task->num_files, sizeof *news);
|
||||
bool ok = news != NULL;
|
||||
for (size_t i = 0; ok && i < task->num_files; i++) {
|
||||
char tmp[PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s/%s", output, task->files[i]);
|
||||
news[i] = strdup(tmp);
|
||||
if (!news[i]) ok = false;
|
||||
}
|
||||
char conflict[PATH_MAX] = {0};
|
||||
bool overlap = ok && data_overlaps_locked(state, news, task->num_files,
|
||||
conflict, sizeof conflict);
|
||||
for (size_t i = 0; news && i < task->num_files; i++) free(news[i]);
|
||||
free(news);
|
||||
if (!ok || overlap) {
|
||||
pthread_mutex_unlock(&state->torrent_lock);
|
||||
if (overlap)
|
||||
NAUT_WARN("add blocked: data overlaps existing torrent at %s",
|
||||
conflict);
|
||||
*error = overlap ? NAUT_ERR_EXIST : NAUT_ERR_NOMEM;
|
||||
goto fail_task;
|
||||
}
|
||||
}
|
||||
if (id_opt) {
|
||||
if (!json_is_integer(id_opt) || json_integer_value(id_opt) < 0) {
|
||||
pthread_mutex_unlock(&state->torrent_lock);
|
||||
|
|
@ -1510,6 +1630,8 @@ fail_task:
|
|||
free(task->locations);
|
||||
for (size_t i = 0; i < task->num_tags; i++) free(task->tags[i]);
|
||||
free(task->tags);
|
||||
for (size_t i = 0; i < task->num_files; i++) free(task->files[i]);
|
||||
free(task->files);
|
||||
free(task->category);
|
||||
free(task->pending_save_path);
|
||||
pthread_mutex_destroy(&task->lock);
|
||||
|
|
@ -1565,7 +1687,8 @@ static json_t *rpc_add_torrent(void *opaque, const json_t *params,
|
|||
state, source, managed, is_temp, output, peers_json,
|
||||
/*locations_json=*/NULL, /*meta_json=*/params,
|
||||
json_object_get(params, "torrent_id"), name,
|
||||
start_paused, /*force_start=*/false, /*queue_pos=*/-1, error);
|
||||
start_paused, /*force_start=*/false, /*queue_pos=*/-1,
|
||||
/*check_overlap=*/true, error);
|
||||
if (!task) {
|
||||
if (managed || is_temp) unlink(source);
|
||||
return NULL;
|
||||
|
|
@ -2357,6 +2480,8 @@ static void destroy_torrent(torrent_task *task) {
|
|||
free(task->locations);
|
||||
for (size_t i = 0; i < task->num_tags; i++) free(task->tags[i]);
|
||||
free(task->tags);
|
||||
for (size_t i = 0; i < task->num_files; i++) free(task->files[i]);
|
||||
free(task->files);
|
||||
free(task->category);
|
||||
free(task->pending_save_path);
|
||||
free(task->name);
|
||||
|
|
@ -2498,7 +2623,8 @@ static void restore_torrents(daemon_state *state) {
|
|||
/*meta_json=*/rec,
|
||||
json_object_get(rec, "torrent_id"),
|
||||
json_string_value(json_object_get(rec, "name")),
|
||||
paused, force_start, queue_pos, &error))
|
||||
paused, force_start, queue_pos,
|
||||
/*check_overlap=*/false, &error))
|
||||
restored++;
|
||||
else {
|
||||
NAUT_WARN("restore: %s failed: %s", source, naut_strerror(error));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue