daemon: store/ query layer — tasks + settings (prep for the download.add vertical slice)
The read/write surface the dispatcher handlers need, so download.add persists and download.list / download.get project real rows when the engine lands. - store/tasks — TaskRow (1:1 with the schema), insert / get / remove / set_state / count, and list(filter, sort, offset, limit) that does all the WHERE / ORDER BY / LIMIT in SQL (M1 DoD: a 1000-row list never materialised client-side). Filter covers states / category / queue / case-insensitive filename+url substring / date range; sort is a whitelisted column + direction with NULLs last, default newest-first; the enum spellings in a state IN (...) come from proto::to_string, never from user text. to_summary() projects a row onto proto::TaskSummary including the flattened error block when the task failed / retry_wait / auto-paused. - store/settings — key -> JSON-text with a built-in default table mirroring Settings.schema.json / ADR 0012; get_raw / set_raw / overrides plus typed get_int / get_string / get_string_array for the governor config and saveTo.allowedRoots. Full settings.get/set wire projection lands with those handlers. - veloxd_store now links velox::proto + nlohmann_json for the projection. Test veloxd.store_tasks: insert/get round trip, PK duplicate rejected, the error-block projection, list total+paging+sort+every filter, set_state pause_reason clear-on-unpause, remove, and settings default vs override. ASan+UBSan and TSan clean; 34 daemon/cli tests green. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
#include "store/settings.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace velox::daemon::store {
|
||||
|
||||
namespace {
|
||||
|
||||
// Built-in defaults, mirroring Settings.schema.json / ADR 0012. Only the keys the daemon
|
||||
// currently reads or is likely to need before the full settings.get handler lands; the
|
||||
// rest resolve through the schema's own defaults at that layer.
|
||||
constexpr std::array<std::pair<std::string_view, std::string_view>, 14> kDefaults{{
|
||||
{"connection.maxSegmentsPerDownload", "8"},
|
||||
{"connection.bufferBytes", "1048576"},
|
||||
{"connection.maxConcurrentDownloads", "5"},
|
||||
{"connection.maxActiveSegments", "32"},
|
||||
{"connection.maxTotalBufferBytes", "134217728"},
|
||||
{"connection.timeoutSec", "30"},
|
||||
{"connection.maxRetries", "10"},
|
||||
{"connection.retryBackoffSec", "5"},
|
||||
{"saveTo.defaultDir", "\"~/Downloads\""},
|
||||
{"saveTo.allowedRoots", "[\"~/Downloads\"]"},
|
||||
{"saveTo.createSubfolderPerSite", "false"},
|
||||
{"downloads.speedLimitBps", "0"},
|
||||
{"downloads.speedLimitEnabled", "false"},
|
||||
{"downloads.duplicatePolicy", "\"ask\""},
|
||||
}};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<std::string_view> Settings::default_for(std::string_view key) {
|
||||
for (const auto& [k, v] : kDefaults) {
|
||||
if (k == key) return v;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DbResult<std::optional<std::string>> Settings::get_raw(std::string_view key) {
|
||||
auto st = db_.prepare("SELECT value FROM settings WHERE key = ?1");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto r = st->bind(1, key); !r) return std::unexpected(r.error());
|
||||
auto row = st->step();
|
||||
if (!row) return std::unexpected(row.error());
|
||||
if (*row) return std::optional<std::string>{st->column_text(0)};
|
||||
if (auto d = default_for(key)) return std::optional<std::string>{std::string(*d)};
|
||||
return std::optional<std::string>{};
|
||||
}
|
||||
|
||||
DbResult<void> Settings::set_raw(std::string_view key, std::string_view json_text) {
|
||||
auto st = db_.prepare(
|
||||
"INSERT INTO settings(key, value) VALUES(?1, ?2) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto r = st->bind(1, key); !r) return std::unexpected(r.error());
|
||||
if (auto r = st->bind(2, json_text); !r) return std::unexpected(r.error());
|
||||
if (auto r = st->step(); !r) return std::unexpected(r.error());
|
||||
return {};
|
||||
}
|
||||
|
||||
DbResult<std::map<std::string, std::string>> Settings::overrides() {
|
||||
auto st = db_.prepare("SELECT key, value FROM settings ORDER BY key");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
std::map<std::string, std::string> out;
|
||||
for (;;) {
|
||||
auto row = st->step();
|
||||
if (!row) return std::unexpected(row.error());
|
||||
if (!*row) break;
|
||||
out.emplace(st->column_text(0), st->column_text(1));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::int64_t Settings::get_int(std::string_view key) {
|
||||
auto raw = get_raw(key);
|
||||
if (raw && *raw) {
|
||||
auto j = nlohmann::json::parse(**raw, nullptr, false);
|
||||
if (j.is_number_integer()) return j.get<std::int64_t>();
|
||||
}
|
||||
if (auto d = default_for(key)) {
|
||||
auto j = nlohmann::json::parse(*d, nullptr, false);
|
||||
if (j.is_number_integer()) return j.get<std::int64_t>();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Settings::get_string(std::string_view key) {
|
||||
auto raw = get_raw(key);
|
||||
if (raw && *raw) {
|
||||
auto j = nlohmann::json::parse(**raw, nullptr, false);
|
||||
if (j.is_string()) return j.get<std::string>();
|
||||
}
|
||||
if (auto d = default_for(key)) {
|
||||
auto j = nlohmann::json::parse(*d, nullptr, false);
|
||||
if (j.is_string()) return j.get<std::string>();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> Settings::get_string_array(std::string_view key) {
|
||||
auto raw = get_raw(key);
|
||||
const std::string text = (raw && *raw) ? **raw
|
||||
: default_for(key) ? std::string(*default_for(key))
|
||||
: std::string("[]");
|
||||
auto j = nlohmann::json::parse(text, nullptr, false);
|
||||
std::vector<std::string> out;
|
||||
if (j.is_array()) {
|
||||
for (const auto& e : j) {
|
||||
if (e.is_string()) out.push_back(e.get<std::string>());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace velox::daemon::store
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
// Read/write access to the `settings` table (key -> JSON-text value). The table starts
|
||||
// empty; a key with no row falls back to the built-in default that mirrors
|
||||
// Settings.schema.json. Typed helpers cover what the daemon reads internally (governor
|
||||
// config, allowed roots); the full settings.get / settings.set projection onto the wire
|
||||
// `Settings` object lands with those handlers.
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "store/sqlite.hpp"
|
||||
|
||||
namespace velox::daemon::store {
|
||||
|
||||
class Settings {
|
||||
public:
|
||||
explicit Settings(Db& db) : db_(db) {}
|
||||
|
||||
// Raw JSON text for one key: the stored row, or the built-in default, or nullopt if
|
||||
// the key is unknown to the daemon entirely.
|
||||
DbResult<std::optional<std::string>> get_raw(std::string_view key);
|
||||
|
||||
// Replace one key's value. `json_text` must be a valid JSON document; the caller
|
||||
// (settings.set handler) validates it against the schema first.
|
||||
DbResult<void> set_raw(std::string_view key, std::string_view json_text);
|
||||
|
||||
// Every stored override (not merged with defaults).
|
||||
DbResult<std::map<std::string, std::string>> overrides();
|
||||
|
||||
// Typed convenience over get_raw + the defaults. A malformed stored value falls back
|
||||
// to the default rather than throwing.
|
||||
std::int64_t get_int(std::string_view key);
|
||||
std::string get_string(std::string_view key);
|
||||
std::vector<std::string> get_string_array(std::string_view key);
|
||||
|
||||
// The built-in default JSON for `key`, or nullopt if unknown.
|
||||
static std::optional<std::string_view> default_for(std::string_view key);
|
||||
|
||||
private:
|
||||
Db& db_;
|
||||
};
|
||||
|
||||
} // namespace velox::daemon::store
|
||||
@@ -0,0 +1,309 @@
|
||||
#include "store/tasks.hpp"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace velox::daemon::store {
|
||||
|
||||
namespace proto = velox::proto;
|
||||
|
||||
namespace {
|
||||
|
||||
// Column order shared by get() and list() — the row reader indexes into this.
|
||||
constexpr const char* kCols =
|
||||
"task_id, url, save_dir, filename, state, start_mode, created_at, "
|
||||
"effective_url, category_id, queue_id, description, pause_reason, "
|
||||
"etag, last_modified, content_type, last_try_at, completed_at, "
|
||||
"checksum_algo, checksum_value, "
|
||||
"size_bytes, downloaded_bytes, resumable, "
|
||||
"req_segments, eff_segments, req_buffer_bytes, eff_buffer_bytes, queue_position, "
|
||||
"error_code, error_message, error_http_status, error_retryable, error_attempt, "
|
||||
"error_next_retry_at";
|
||||
|
||||
DbResult<void> bind_opt(Stmt& s, int i, const std::optional<std::string>& v) {
|
||||
return v ? s.bind(i, std::string_view(*v)) : s.bind_null(i);
|
||||
}
|
||||
DbResult<void> bind_opt(Stmt& s, int i, const std::optional<std::int64_t>& v) {
|
||||
return v ? s.bind(i, *v) : s.bind_null(i);
|
||||
}
|
||||
|
||||
std::optional<std::string> col_opt_text(Stmt& s, int i) {
|
||||
if (s.column_is_null(i)) return std::nullopt;
|
||||
return s.column_text(i);
|
||||
}
|
||||
std::optional<std::int64_t> col_opt_int(Stmt& s, int i) {
|
||||
if (s.column_is_null(i)) return std::nullopt;
|
||||
return s.column_int(i);
|
||||
}
|
||||
|
||||
TaskRow read_row(Stmt& s) {
|
||||
TaskRow r;
|
||||
r.task_id = s.column_text(0);
|
||||
r.url = s.column_text(1);
|
||||
r.save_dir = s.column_text(2);
|
||||
r.filename = s.column_text(3);
|
||||
r.state = s.column_text(4);
|
||||
r.start_mode = s.column_text(5);
|
||||
r.created_at = s.column_text(6);
|
||||
r.effective_url = col_opt_text(s, 7);
|
||||
r.category_id = col_opt_text(s, 8);
|
||||
r.queue_id = col_opt_text(s, 9);
|
||||
r.description = col_opt_text(s, 10);
|
||||
r.pause_reason = col_opt_text(s, 11);
|
||||
r.etag = col_opt_text(s, 12);
|
||||
r.last_modified = col_opt_text(s, 13);
|
||||
r.content_type = col_opt_text(s, 14);
|
||||
r.last_try_at = col_opt_text(s, 15);
|
||||
r.completed_at = col_opt_text(s, 16);
|
||||
r.checksum_algo = col_opt_text(s, 17);
|
||||
r.checksum_value = col_opt_text(s, 18);
|
||||
r.size_bytes = col_opt_int(s, 19);
|
||||
r.downloaded_bytes = s.column_int(20);
|
||||
r.resumable = s.column_int(21) != 0;
|
||||
r.req_segments = col_opt_int(s, 22);
|
||||
r.eff_segments = s.column_int(23);
|
||||
r.req_buffer_bytes = col_opt_int(s, 24);
|
||||
r.eff_buffer_bytes = col_opt_int(s, 25);
|
||||
r.queue_position = col_opt_int(s, 26);
|
||||
r.error_code = col_opt_text(s, 27);
|
||||
r.error_message = col_opt_text(s, 28);
|
||||
r.error_http_status = col_opt_int(s, 29);
|
||||
if (!s.column_is_null(30)) r.error_retryable = s.column_int(30) != 0;
|
||||
r.error_attempt = col_opt_int(s, 31);
|
||||
r.error_next_retry_at = col_opt_text(s, 32);
|
||||
return r;
|
||||
}
|
||||
|
||||
// TaskSort.field -> a whitelisted column. Anything not backed by a stored column (live
|
||||
// speed, eta) sorts by recency instead of erroring.
|
||||
const char* sort_column(proto::TaskSortField f) {
|
||||
switch (f) {
|
||||
case proto::TaskSortField::Filename: return "filename";
|
||||
case proto::TaskSortField::SizeBytes: return "size_bytes";
|
||||
case proto::TaskSortField::State: return "state";
|
||||
case proto::TaskSortField::LastTryAt: return "last_try_at";
|
||||
case proto::TaskSortField::QueuePosition: return "queue_position";
|
||||
case proto::TaskSortField::Description: return "description";
|
||||
case proto::TaskSortField::CreatedAt:
|
||||
case proto::TaskSortField::EtaSeconds:
|
||||
case proto::TaskSortField::SpeedBps:
|
||||
default: return "created_at";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DbResult<void> Tasks::insert(const TaskRow& r) {
|
||||
auto st = db_.prepare(
|
||||
"INSERT INTO tasks("
|
||||
"task_id, url, save_dir, filename, state, start_mode, created_at, "
|
||||
"effective_url, category_id, queue_id, description, pause_reason, "
|
||||
"etag, last_modified, content_type, last_try_at, completed_at, "
|
||||
"checksum_algo, checksum_value, size_bytes, downloaded_bytes, resumable, "
|
||||
"req_segments, eff_segments, req_buffer_bytes, eff_buffer_bytes, queue_position, "
|
||||
"error_code, error_message, error_http_status, error_retryable, error_attempt, "
|
||||
"error_next_retry_at) VALUES("
|
||||
"?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,"
|
||||
"?23,?24,?25,?26,?27,?28,?29,?30,?31,?32,?33)");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
|
||||
auto chk = [](DbResult<void> r) { return r.has_value(); };
|
||||
bool ok = chk(st->bind(1, std::string_view(r.task_id))) &&
|
||||
chk(st->bind(2, std::string_view(r.url))) &&
|
||||
chk(st->bind(3, std::string_view(r.save_dir))) &&
|
||||
chk(st->bind(4, std::string_view(r.filename))) &&
|
||||
chk(st->bind(5, std::string_view(r.state))) &&
|
||||
chk(st->bind(6, std::string_view(r.start_mode))) &&
|
||||
chk(st->bind(7, std::string_view(r.created_at))) &&
|
||||
chk(bind_opt(*st, 8, r.effective_url)) && chk(bind_opt(*st, 9, r.category_id)) &&
|
||||
chk(bind_opt(*st, 10, r.queue_id)) && chk(bind_opt(*st, 11, r.description)) &&
|
||||
chk(bind_opt(*st, 12, r.pause_reason)) && chk(bind_opt(*st, 13, r.etag)) &&
|
||||
chk(bind_opt(*st, 14, r.last_modified)) && chk(bind_opt(*st, 15, r.content_type)) &&
|
||||
chk(bind_opt(*st, 16, r.last_try_at)) && chk(bind_opt(*st, 17, r.completed_at)) &&
|
||||
chk(bind_opt(*st, 18, r.checksum_algo)) &&
|
||||
chk(bind_opt(*st, 19, r.checksum_value)) && chk(bind_opt(*st, 20, r.size_bytes)) &&
|
||||
chk(st->bind(21, r.downloaded_bytes)) &&
|
||||
chk(st->bind(22, static_cast<std::int64_t>(r.resumable))) &&
|
||||
chk(bind_opt(*st, 23, r.req_segments)) && chk(st->bind(24, r.eff_segments)) &&
|
||||
chk(bind_opt(*st, 25, r.req_buffer_bytes)) &&
|
||||
chk(bind_opt(*st, 26, r.eff_buffer_bytes)) &&
|
||||
chk(bind_opt(*st, 27, r.queue_position)) && chk(bind_opt(*st, 28, r.error_code)) &&
|
||||
chk(bind_opt(*st, 29, r.error_message)) &&
|
||||
chk(bind_opt(*st, 30, r.error_http_status)) &&
|
||||
chk(r.error_retryable ? st->bind(31, static_cast<std::int64_t>(*r.error_retryable))
|
||||
: st->bind_null(31)) &&
|
||||
chk(bind_opt(*st, 32, r.error_attempt)) &&
|
||||
chk(bind_opt(*st, 33, r.error_next_retry_at));
|
||||
if (!ok) return std::unexpected(DbError{0, "failed to bind a task column"});
|
||||
|
||||
if (auto r2 = st->step(); !r2) return std::unexpected(r2.error());
|
||||
return {};
|
||||
}
|
||||
|
||||
DbResult<std::optional<TaskRow>> Tasks::get(std::string_view task_id) {
|
||||
auto st = db_.prepare(std::string("SELECT ") + kCols + " FROM tasks WHERE task_id = ?1");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto r = st->bind(1, task_id); !r) return std::unexpected(r.error());
|
||||
auto row = st->step();
|
||||
if (!row) return std::unexpected(row.error());
|
||||
if (!*row) return std::optional<TaskRow>{};
|
||||
return std::optional<TaskRow>{read_row(*st)};
|
||||
}
|
||||
|
||||
DbResult<Tasks::Page> Tasks::list(const std::optional<proto::TaskFilter>& filter,
|
||||
const std::optional<proto::TaskSort>& sort, std::int64_t offset,
|
||||
std::int64_t limit) {
|
||||
std::string where;
|
||||
std::vector<std::string> params; // bound 1:1 with the '?' placeholders, in order
|
||||
auto clause = [&](std::string c) {
|
||||
where += where.empty() ? " WHERE " : " AND ";
|
||||
where += std::move(c);
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
if (filter->states && !filter->states->empty()) {
|
||||
std::string in;
|
||||
for (const auto st : *filter->states) {
|
||||
in += in.empty() ? "" : ",";
|
||||
in += "'";
|
||||
in += proto::to_string(st); // an enum spelling, never user input
|
||||
in += "'";
|
||||
}
|
||||
clause("state IN (" + in + ")");
|
||||
}
|
||||
if (filter->categoryId) {
|
||||
clause("category_id = ?");
|
||||
params.push_back(*filter->categoryId);
|
||||
}
|
||||
if (filter->queueId) {
|
||||
clause("queue_id = ?");
|
||||
params.push_back(*filter->queueId);
|
||||
}
|
||||
if (filter->query) {
|
||||
clause("(instr(lower(filename), lower(?)) > 0 OR instr(lower(url), lower(?)) > 0)");
|
||||
params.push_back(*filter->query);
|
||||
params.push_back(*filter->query);
|
||||
}
|
||||
if (filter->addedAfter) {
|
||||
clause("created_at >= ?");
|
||||
params.push_back(*filter->addedAfter);
|
||||
}
|
||||
if (filter->addedBefore) {
|
||||
clause("created_at < ?");
|
||||
params.push_back(*filter->addedBefore);
|
||||
}
|
||||
}
|
||||
|
||||
std::int64_t total = 0;
|
||||
{
|
||||
auto st = db_.prepare("SELECT count(*) FROM tasks" + where);
|
||||
if (!st) return std::unexpected(st.error());
|
||||
for (std::size_t i = 0; i < params.size(); ++i)
|
||||
if (auto r = st->bind(static_cast<int>(i + 1), std::string_view(params[i])); !r)
|
||||
return std::unexpected(r.error());
|
||||
auto row = st->step();
|
||||
if (!row) return std::unexpected(row.error());
|
||||
if (*row) total = st->column_int(0);
|
||||
}
|
||||
|
||||
// Default sort is newest-first; an explicit sort is a whitelisted column + direction.
|
||||
// NULLs sort last in both directions so an unsized / unqueued task never floats up.
|
||||
std::string order = "created_at DESC";
|
||||
if (sort) {
|
||||
const char* col = sort_column(sort->field);
|
||||
const bool desc = sort->direction == proto::TaskSortDirection::Desc;
|
||||
order = std::string(col) + " IS NULL, " + col + (desc ? " DESC" : " ASC");
|
||||
}
|
||||
|
||||
const std::int64_t lim = limit > 0 ? limit : 500;
|
||||
const std::int64_t off = offset > 0 ? offset : 0;
|
||||
|
||||
Page page;
|
||||
page.total = total;
|
||||
{
|
||||
auto st = db_.prepare(std::string("SELECT ") + kCols + " FROM tasks" + where +
|
||||
" ORDER BY " + order + " LIMIT ? OFFSET ?");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
int n = 1;
|
||||
for (const auto& p : params)
|
||||
if (auto r = st->bind(n++, std::string_view(p)); !r) return std::unexpected(r.error());
|
||||
if (auto r = st->bind(n++, lim); !r) return std::unexpected(r.error());
|
||||
if (auto r = st->bind(n++, off); !r) return std::unexpected(r.error());
|
||||
for (;;) {
|
||||
auto row = st->step();
|
||||
if (!row) return std::unexpected(row.error());
|
||||
if (!*row) break;
|
||||
page.rows.push_back(read_row(*st));
|
||||
}
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
DbResult<bool> Tasks::set_state(std::string_view task_id, std::string_view state,
|
||||
const std::optional<std::string>& pause_reason) {
|
||||
auto st = db_.prepare(
|
||||
"UPDATE tasks SET state = ?2, pause_reason = ?3 WHERE task_id = ?1");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto r = st->bind(1, task_id); !r) return std::unexpected(r.error());
|
||||
if (auto r = st->bind(2, state); !r) return std::unexpected(r.error());
|
||||
const bool paused = state == "paused";
|
||||
if (auto r = paused && pause_reason ? st->bind(3, std::string_view(*pause_reason))
|
||||
: st->bind_null(3);
|
||||
!r)
|
||||
return std::unexpected(r.error());
|
||||
if (auto r = st->step(); !r) return std::unexpected(r.error());
|
||||
return sqlite3_changes(db_.raw()) > 0;
|
||||
}
|
||||
|
||||
DbResult<bool> Tasks::remove(std::string_view task_id) {
|
||||
auto st = db_.prepare("DELETE FROM tasks WHERE task_id = ?1");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
if (auto r = st->bind(1, task_id); !r) return std::unexpected(r.error());
|
||||
if (auto r = st->step(); !r) return std::unexpected(r.error());
|
||||
return sqlite3_changes(db_.raw()) > 0;
|
||||
}
|
||||
|
||||
DbResult<std::int64_t> Tasks::count() {
|
||||
auto st = db_.prepare("SELECT count(*) FROM tasks");
|
||||
if (!st) return std::unexpected(st.error());
|
||||
auto row = st->step();
|
||||
if (!row) return std::unexpected(row.error());
|
||||
return (*row) ? st->column_int(0) : 0;
|
||||
}
|
||||
|
||||
proto::TaskSummary to_summary(const TaskRow& r) {
|
||||
proto::TaskSummary s;
|
||||
s.taskId = r.task_id;
|
||||
s.filename = r.filename;
|
||||
s.saveDir = r.save_dir;
|
||||
s.url = r.url;
|
||||
s.effectiveUrl = r.effective_url;
|
||||
s.sizeBytes = r.size_bytes;
|
||||
s.downloadedBytes = r.downloaded_bytes;
|
||||
if (auto st = proto::parse_TaskState(r.state)) s.state = *st;
|
||||
s.speedBps = 0;
|
||||
s.resumable = r.resumable;
|
||||
s.segments = r.eff_segments;
|
||||
s.categoryId = r.category_id;
|
||||
s.queueId = r.queue_id;
|
||||
s.queuePosition = r.queue_position;
|
||||
s.description = r.description;
|
||||
s.createdAt = r.created_at;
|
||||
s.lastTryAt = r.last_try_at;
|
||||
s.completedAt = r.completed_at;
|
||||
|
||||
if (r.error_code) {
|
||||
proto::TaskError e;
|
||||
if (auto c = proto::parse_TaskErrorCode(*r.error_code)) e.code = *c;
|
||||
e.message = r.error_message.value_or("");
|
||||
e.httpStatus = r.error_http_status;
|
||||
e.retryable = r.error_retryable.value_or(false);
|
||||
e.attempt = r.error_attempt;
|
||||
e.nextRetryAt = r.error_next_retry_at;
|
||||
s.error = std::move(e);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace velox::daemon::store
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
// Read/write access to the `tasks` table, plus the projection onto the wire TaskSummary.
|
||||
// download.list does its filtering, sorting and paging here (M1 DoD: a 1000-row list under
|
||||
// 50 ms, never materialised client-side).
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "store/sqlite.hpp"
|
||||
#include "velox_proto.hpp"
|
||||
|
||||
namespace velox::daemon::store {
|
||||
|
||||
// One row of `tasks`, 1:1 with the schema. std::optional maps a NULL column.
|
||||
struct TaskRow {
|
||||
std::string task_id;
|
||||
std::string url;
|
||||
std::string save_dir;
|
||||
std::string filename;
|
||||
std::string state = "new";
|
||||
std::string start_mode = "auto";
|
||||
std::string created_at;
|
||||
|
||||
std::optional<std::string> effective_url;
|
||||
std::optional<std::string> category_id;
|
||||
std::optional<std::string> queue_id;
|
||||
std::optional<std::string> description;
|
||||
std::optional<std::string> pause_reason;
|
||||
std::optional<std::string> etag;
|
||||
std::optional<std::string> last_modified;
|
||||
std::optional<std::string> content_type;
|
||||
std::optional<std::string> last_try_at;
|
||||
std::optional<std::string> completed_at;
|
||||
std::optional<std::string> checksum_algo;
|
||||
std::optional<std::string> checksum_value;
|
||||
|
||||
std::optional<std::int64_t> size_bytes;
|
||||
std::int64_t downloaded_bytes = 0;
|
||||
bool resumable = false;
|
||||
|
||||
std::optional<std::int64_t> req_segments;
|
||||
std::int64_t eff_segments = 0;
|
||||
std::optional<std::int64_t> req_buffer_bytes;
|
||||
std::optional<std::int64_t> eff_buffer_bytes;
|
||||
std::optional<std::int64_t> queue_position;
|
||||
|
||||
std::optional<std::string> error_code;
|
||||
std::optional<std::string> error_message;
|
||||
std::optional<std::int64_t> error_http_status;
|
||||
std::optional<bool> error_retryable;
|
||||
std::optional<std::int64_t> error_attempt;
|
||||
std::optional<std::string> error_next_retry_at;
|
||||
};
|
||||
|
||||
class Tasks {
|
||||
public:
|
||||
explicit Tasks(Db& db) : db_(db) {}
|
||||
|
||||
DbResult<void> insert(const TaskRow& row);
|
||||
DbResult<std::optional<TaskRow>> get(std::string_view task_id);
|
||||
|
||||
struct Page {
|
||||
std::int64_t total = 0; // rows matching the filter, ignoring paging
|
||||
std::vector<TaskRow> rows;
|
||||
};
|
||||
DbResult<Page> list(const std::optional<velox::proto::TaskFilter>& filter,
|
||||
const std::optional<velox::proto::TaskSort>& sort, std::int64_t offset,
|
||||
std::int64_t limit);
|
||||
|
||||
// Move a task to `state`; `pause_reason` is written only when state == "paused"
|
||||
// (cleared otherwise). Returns false if there is no such task.
|
||||
DbResult<bool> set_state(std::string_view task_id, std::string_view state,
|
||||
const std::optional<std::string>& pause_reason);
|
||||
|
||||
DbResult<bool> remove(std::string_view task_id);
|
||||
DbResult<std::int64_t> count();
|
||||
|
||||
private:
|
||||
Db& db_;
|
||||
};
|
||||
|
||||
// Project a row onto the wire type. `state` and `error.code` strings are assumed valid
|
||||
// (the CHECK constraints and the state machine keep them so).
|
||||
velox::proto::TaskSummary to_summary(const TaskRow& row);
|
||||
|
||||
} // namespace velox::daemon::store
|
||||
Reference in New Issue
Block a user