summaryrefslogtreecommitdiff
path: root/src/web.c
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2026-02-18 22:14:54 +0100
committerJesper Jensen <jesper@jnsn.dev>2026-02-18 22:14:54 +0100
commit8d3257fe558727a3d64902e27539b5a5c28ba49f (patch)
tree33d97cdcf4dd89f3e396b522d14edcca65106708 /src/web.c
parent5fbbd990d6b42f5e7534cdb68fa5c368a4a69169 (diff)
Stop stalled backups
Diffstat (limited to 'src/web.c')
-rw-r--r--src/web.c245
1 files changed, 243 insertions, 2 deletions
diff --git a/src/web.c b/src/web.c
index df2dc81..e0d6d55 100644
--- a/src/web.c
+++ b/src/web.c
@@ -278,6 +278,28 @@ int submit_report(struct App *app, char *client, char *secret, enum ReportEvent
switch(event) {
case REV_BEGIN: {
sqlite3_stmt *stmt;
+
+ // Mark any existing RUNNING backup as INTERRUPTED
+ if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = 'INTERRUPTED', completed = datetime(?1, 'unixepoch', 'subsec') WHERE client = ?2 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_double(stmt, 1, TIME_AS_FLOAT(now)) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 2, clientId) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ stmt = NULL;
+
+ // Insert the new backup
if(sqlite3_prepare_v2(conn, "INSERT INTO backups (client, status, started) VALUES (?1, ?2, datetime(?3, 'unixepoch', 'subsec'))", -1, &stmt, NULL) != SQLITE_OK) {
abort();
}
@@ -480,6 +502,93 @@ int list_clients(struct Request *req, struct ListClientResult *result) {
return 0;
}
+int get_client_with_backups(struct Request *req, int64_t client_id, struct GetClientResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ result->found = 0;
+
+ // Query 1: Get client info
+ {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id, name, secret FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) {
+ abort();
+ }
+
+ int ret = sqlite3_step(stmt);
+ if(ret == SQLITE_ROW) {
+ result->found = 1;
+ result->id = sqlite3_column_int64(stmt, 0);
+ const uint8_t *name = sqlite3_column_text(stmt, 1);
+ const uint8_t *secret = sqlite3_column_text(stmt, 2);
+ strncpy(result->name, (char*)name, CLIENT_NAME_MAX);
+ strncpy(result->secret, (char*)secret, CLIENT_SECRET_LEN);
+ } else if(ret != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(!result->found) {
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+ }
+
+ // Query 2: Get backups for this client
+ {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id, status, started, completed FROM backups WHERE client = ?1 ORDER BY started DESC LIMIT 50", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) {
+ abort();
+ }
+
+ int ret;
+ int index = 0;
+ while((ret = sqlite3_step(stmt)) == SQLITE_ROW) {
+ if(index >= MAX_BACKUPS_DISPLAY) break;
+
+ result->backups[index].id = sqlite3_column_int64(stmt, 0);
+
+ const uint8_t *status = sqlite3_column_text(stmt, 1);
+ const uint8_t *started = sqlite3_column_text(stmt, 2);
+ const uint8_t *completed = sqlite3_column_text(stmt, 3);
+
+ strncpy(result->backups[index].status, status ? (char*)status : "", BACKUP_STATUS_MAX);
+ strncpy(result->backups[index].started, started ? (char*)started : "", BACKUP_DATETIME_MAX);
+ strncpy(result->backups[index].completed, completed ? (char*)completed : "", BACKUP_DATETIME_MAX);
+
+ index++;
+ }
+ if(ret != SQLITE_DONE && ret != SQLITE_ROW) {
+ abort();
+ }
+
+ result->backups_len = index;
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
+
static char *render_clients_page(struct Request *request, size_t *out_len) {
int ret;
struct ListClientResult *result = malloc(sizeof(struct ListClientResult) + sizeof(struct ListClientResultClient) * 16);
@@ -511,7 +620,7 @@ static char *render_clients_page(struct Request *request, size_t *out_len) {
for (int i = 0; i < result->clients_len; i++) {
fprintf(f,
" <tr>\n"
- " <td>%s</td>\n"
+ " <td><a href=\"/client/%ld\">%s</a></td>\n"
" <td>%s</td>\n"
" <td>\n"
" <form method=\"POST\" action=\"/clients/delete\" style=\"display:inline\">\n"
@@ -520,6 +629,7 @@ static char *render_clients_page(struct Request *request, size_t *out_len) {
" </form>\n"
" </td>\n"
" </tr>\n",
+ result->clients[i].id,
result->clients[i].name,
result->clients[i].secret,
result->clients[i].id
@@ -542,6 +652,99 @@ static char *render_clients_page(struct Request *request, size_t *out_len) {
return buf;
}
+static char *render_client_detail_page(struct Request *request, int64_t client_id, size_t *out_len) {
+ struct GetClientResult *result = malloc(sizeof(struct GetClientResult) + sizeof(struct BackupRecord) * MAX_BACKUPS_DISPLAY);
+ int ret = get_client_with_backups(request, client_id, result);
+ if(ret != 0) {
+ free(result);
+ return NULL;
+ }
+
+ if(!result->found) {
+ free(result);
+ return NULL;
+ }
+
+ char *buf = NULL;
+ size_t buf_len = 0;
+ FILE *f = open_memstream(&buf, &buf_len);
+ if (!f) {
+ free(result);
+ return NULL;
+ }
+
+ fprintf(f,
+ "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head>\n"
+ " <title>Client: %s</title>\n"
+ "</head>\n"
+ "<body>\n"
+ " <p><a href=\"/clients\">Clients</a> &gt; %s</p>\n"
+ " <h1>Client: %s</h1>\n"
+ " <h2>Details</h2>\n"
+ " <table border=\"1\">\n"
+ " <tr><th>ID</th><td>%ld</td></tr>\n"
+ " <tr><th>Name</th><td>%s</td></tr>\n"
+ " <tr><th>Secret</th><td>%s</td></tr>\n"
+ " </table>\n",
+ result->name,
+ result->name,
+ result->name,
+ result->id,
+ result->name,
+ result->secret
+ );
+
+ fprintf(f,
+ " <h2>Submit Report</h2>\n"
+ " <p>Use this curl command to submit a backup report:</p>\n"
+ " <pre>curl -X POST http://%s/api/report \\\n"
+ " -H \"Content-Type: application/json\" \\\n"
+ " -d '{\"client\": \"%s\", \"secret\": \"%s\", \"event\": \"begin_backup\"}'</pre>\n"
+ " <p>Replace <code>begin_backup</code> with <code>end_backup</code> to mark completion.</p>\n",
+ request->host ? request->host : "localhost",
+ result->name,
+ result->secret
+ );
+
+ fprintf(f, " <h2>Backup History</h2>\n");
+
+ if(result->backups_len == 0) {
+ fprintf(f, " <p>No backups recorded.</p>\n");
+ } else {
+ fprintf(f,
+ " <table border=\"1\">\n"
+ " <tr><th>ID</th><th>Status</th><th>Started</th><th>Completed</th></tr>\n");
+
+ for (int i = 0; i < result->backups_len; i++) {
+ fprintf(f,
+ " <tr>\n"
+ " <td>%ld</td>\n"
+ " <td>%s</td>\n"
+ " <td>%s</td>\n"
+ " <td>%s</td>\n"
+ " </tr>\n",
+ result->backups[i].id,
+ result->backups[i].status,
+ result->backups[i].started,
+ result->backups[i].completed[0] ? result->backups[i].completed : "-"
+ );
+ }
+
+ fprintf(f, " </table>\n");
+ }
+
+ fprintf(f,
+ "</body>\n"
+ "</html>\n");
+
+ fclose(f);
+ *out_len = buf_len;
+ free(result);
+ return buf;
+}
+
static enum MHD_Result create_post_collector(struct PostCollector *c) {
c->data = malloc(512);
c->size = 0;
@@ -625,6 +828,7 @@ static enum MHD_Result handler(
*ptr = request;
request->server = server;
+ request->host = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Host");
if(strcmp(method, "POST") == 0) {
if(strcmp(url, "/clients") == 0 || strcmp(url, "/clients/delete") == 0) {
@@ -849,6 +1053,43 @@ static enum MHD_Result handler(
);
MHD_destroy_response(response);
return ret;
+ } else if (strncmp(url, "/client/", 8) == 0) {
+ int64_t client_id;
+ if (sscanf(url + 8, "%ld", &client_id) != 1 || client_id <= 0) {
+ // Bad Request
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ 0, "", MHD_RESPMEM_PERSISTENT);
+ ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response);
+ MHD_destroy_response(response);
+ return ret;
+ }
+
+ size_t html_len;
+ char *html = render_client_detail_page(request, client_id, &html_len);
+
+ if (!html) {
+ // Not Found
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ 0, "", MHD_RESPMEM_PERSISTENT);
+ ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);
+ MHD_destroy_response(response);
+ return ret;
+ }
+
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ html_len,
+ html,
+ MHD_RESPMEM_MUST_FREE
+ );
+ ret = MHD_add_response_header(response, "Content-Type", "text/html");
+ if(ret != MHD_YES) return ret;
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_OK,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
} else {
struct MHD_Response *response = MHD_create_response_from_buffer(
strlen(server->page),
@@ -892,7 +1133,7 @@ int web_begin(struct Server *server, int port) {
server->page = PAGE;
d = MHD_start_daemon(
- MHD_USE_THREAD_PER_CONNECTION,
+ MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DUAL_STACK,
port,
NULL, NULL,
&handler, server,