daemon: D3 (partial) — category.upsert/remove, queue.upsert, download.remove/addBatch/provideAuth
Closes a bounded, high-value slice of the remaining D3 stubs. settings.*/rules.*/ limiter.*/schedule.*/grabber.*/media.*/capture.*/queue.reorder/download.refreshUrl/ download.update stay deferred — reasons noted individually in deferrals.md (settings.* specifically: a real, large field<->key<->JSON-type mapping table across ~43 fields / ~50 SettingKeys, not started rather than rushed). category.upsert/remove: store/categories.hpp gains get/upsert/remove. upsert generates an id when absent and always ignores the payload's `builtin` (preserved from the existing row on replace, false on create); saveDir goes through the same fs::resolve_target canonicalize-and-root-check as download.add. remove refuses a builtin at both layers (dispatcher's -32602 pre-check; the store's own "DELETE ... AND builtin = 0" as defense in depth) and reassigns member tasks to reassignTo (default "general") inside one transaction before deleting the row. queue.upsert: store/queues.hpp gains get/upsert (set_state already existed from D4b). Same create-generates-id pattern; a create always starts 'stopped', a replace keeps the queue's current run state (upsert edits config, not run state — that's queue.start/stop). Also fixed in passing: on_complete has been a real column since migration 0001 but Queues::list/get never projected it onto Queue.onComplete. download.remove: cancels with discard_partial=true (always drops the .veloxpart pair — unlike download.cancel, which keeps them, the row is gone either way), deletes the finished file only when deleteFile is true and the task was complete (best-effort), deletes the row (segments cascade via the FK), and publishes event.task.removed (closing the last open note under D5). download.addBatch: on_download_add's body is now a shared add_one(), called once per item after merging each item's unset fields against params.defaults. download.provideAuth: forwards to EnginePort::provide_auth via a new TaskActionPort::provide_auth. `remember`/persisting to the Secret Service is accepted but not acted on — nothing in this build talks to libsecret yet. Verified against real veloxd + tools/testserver: category create/replace/ remove-with-reassignment, queue create/replace-keeps-run-state, a batch add sharing defaults.saveDir, and download.remove with deleteFile actually deleting the file and the task then 404ing download.get with -32010. Full ctest: 39/39. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace velox::daemon::store {
|
||||
@@ -10,8 +14,8 @@ namespace proto = velox::proto;
|
||||
|
||||
namespace {
|
||||
|
||||
// One queue row (columns queue_id, name, state, max_concurrent, schedule, in that order)
|
||||
// plus its member taskIds, read off the row a caller has already step()'d to.
|
||||
// One queue row (columns queue_id, name, state, max_concurrent, schedule, on_complete, in
|
||||
// that order) plus its member taskIds, read off the row a caller has already step()'d to.
|
||||
DbResult<proto::Queue> project_row(Db& db, Stmt& st) {
|
||||
proto::Queue q;
|
||||
q.queueId = st.column_text(0);
|
||||
@@ -22,6 +26,7 @@ DbResult<proto::Queue> project_row(Db& db, Stmt& st) {
|
||||
auto j = nlohmann::json::parse(st.column_text(4), nullptr, false);
|
||||
if (auto sched = proto::parse<proto::Schedule>(j, "schedule")) q.schedule = *sched;
|
||||
}
|
||||
if (auto oc = proto::parse_QueueOnComplete(st.column_text(5))) q.onComplete = *oc;
|
||||
|
||||
auto ts = db.prepare("SELECT task_id FROM tasks WHERE queue_id = ?1 ORDER BY queue_position");
|
||||
if (!ts) return std::unexpected(ts.error());
|
||||
@@ -41,7 +46,8 @@ DbResult<proto::Queue> project_row(Db& db, Stmt& st) {
|
||||
|
||||
DbResult<std::vector<proto::Queue>> Queues::list() {
|
||||
auto st = db_.prepare(
|
||||
"SELECT queue_id, name, state, max_concurrent, schedule FROM queues ORDER BY name");
|
||||
"SELECT queue_id, name, state, max_concurrent, schedule, on_complete FROM queues "
|
||||
"ORDER BY name");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
|
||||
std::vector<proto::Queue> out;
|
||||
@@ -58,7 +64,8 @@ DbResult<std::vector<proto::Queue>> Queues::list() {
|
||||
|
||||
DbResult<std::optional<proto::Queue>> Queues::get(std::string_view queue_id) {
|
||||
auto st = db_.prepare(
|
||||
"SELECT queue_id, name, state, max_concurrent, schedule FROM queues WHERE queue_id = ?1");
|
||||
"SELECT queue_id, name, state, max_concurrent, schedule, on_complete FROM queues "
|
||||
"WHERE queue_id = ?1");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto b = st->bind(1, queue_id); !b) return std::unexpected(b.error());
|
||||
auto row = st->step();
|
||||
@@ -78,4 +85,48 @@ DbResult<bool> Queues::set_state(std::string_view queue_id, std::string_view sta
|
||||
return sqlite3_changes(db_.raw()) > 0;
|
||||
}
|
||||
|
||||
DbResult<proto::Queue> Queues::upsert(proto::Queue queue) {
|
||||
if (queue.queueId.empty()) {
|
||||
std::random_device rd;
|
||||
std::uniform_int_distribution<std::uint64_t> d;
|
||||
char buf[17];
|
||||
std::snprintf(buf, sizeof(buf), "%016llx", static_cast<unsigned long long>(d(rd)));
|
||||
queue.queueId = std::string(buf);
|
||||
}
|
||||
|
||||
// A create defaults to 'stopped' (never auto-runs a brand-new queue); a replace keeps
|
||||
// whatever run state the queue is already in — queue.upsert edits the config, not the
|
||||
// run state (that's queue.start/stop).
|
||||
std::string state = "stopped";
|
||||
if (auto existing = get(queue.queueId); existing && existing->has_value())
|
||||
state = std::string(proto::to_string((*existing)->state));
|
||||
|
||||
const std::string schedule_json =
|
||||
queue.schedule ? nlohmann::json(*queue.schedule).dump() : std::string();
|
||||
const std::string on_complete =
|
||||
std::string(proto::to_string(queue.onComplete.value_or(proto::QueueOnComplete::Nothing)));
|
||||
|
||||
auto st = db_.prepare(
|
||||
"INSERT INTO queues(queue_id, name, state, max_concurrent, schedule, on_complete) "
|
||||
"VALUES(?1,?2,?3,?4,?5,?6) "
|
||||
"ON CONFLICT(queue_id) DO UPDATE SET "
|
||||
"name=excluded.name, max_concurrent=excluded.max_concurrent, "
|
||||
"schedule=excluded.schedule, on_complete=excluded.on_complete");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto b = st->bind(1, std::string_view(queue.queueId)); !b) return std::unexpected(b.error());
|
||||
if (auto b = st->bind(2, std::string_view(queue.name)); !b) return std::unexpected(b.error());
|
||||
if (auto b = st->bind(3, std::string_view(state)); !b) return std::unexpected(b.error());
|
||||
if (auto b = st->bind(4, queue.maxConcurrent); !b) return std::unexpected(b.error());
|
||||
if (auto r = queue.schedule ? st->bind(5, std::string_view(schedule_json)) : st->bind_null(5); !r)
|
||||
return std::unexpected(r.error());
|
||||
if (auto b = st->bind(6, std::string_view(on_complete)); !b) return std::unexpected(b.error());
|
||||
if (auto r = st->step(); !r) return std::unexpected(r.error());
|
||||
|
||||
auto stored = get(queue.queueId);
|
||||
if (!stored) return std::unexpected(stored.error());
|
||||
if (!stored->has_value())
|
||||
return std::unexpected(DbError{0, "queue.upsert: row vanished after insert"});
|
||||
return **stored;
|
||||
}
|
||||
|
||||
} // namespace velox::daemon::store
|
||||
|
||||
Reference in New Issue
Block a user