From 8d3257fe558727a3d64902e27539b5a5c28ba49f Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Wed, 18 Feb 2026 22:14:54 +0100 Subject: Stop stalled backups --- src/web.c | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 2 deletions(-) (limited to 'src/web.c') 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, " \n" - " %s\n" + " %s\n" " %s\n" " \n" "
\n" @@ -520,6 +629,7 @@ static char *render_clients_page(struct Request *request, size_t *out_len) { "
\n" " \n" " \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, + "\n" + "\n" + "\n" + " Client: %s\n" + "\n" + "\n" + "

Clients > %s

\n" + "

Client: %s

\n" + "

Details

\n" + " \n" + " \n" + " \n" + " \n" + "
ID%ld
Name%s
Secret%s
\n", + result->name, + result->name, + result->name, + result->id, + result->name, + result->secret + ); + + fprintf(f, + "

Submit Report

\n" + "

Use this curl command to submit a backup report:

\n" + "
curl -X POST http://%s/api/report \\\n"
+		"  -H \"Content-Type: application/json\" \\\n"
+		"  -d '{\"client\": \"%s\", \"secret\": \"%s\", \"event\": \"begin_backup\"}'
\n" + "

Replace begin_backup with end_backup to mark completion.

\n", + request->host ? request->host : "localhost", + result->name, + result->secret + ); + + fprintf(f, "

Backup History

\n"); + + if(result->backups_len == 0) { + fprintf(f, "

No backups recorded.

\n"); + } else { + fprintf(f, + " \n" + " \n"); + + for (int i = 0; i < result->backups_len; i++) { + fprintf(f, + " \n" + " \n" + " \n" + " \n" + " \n" + " \n", + result->backups[i].id, + result->backups[i].status, + result->backups[i].started, + result->backups[i].completed[0] ? result->backups[i].completed : "-" + ); + } + + fprintf(f, "
IDStatusStartedCompleted
%ld%s%s%s
\n"); + } + + fprintf(f, + "\n" + "\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) { @@ -835,6 +1039,43 @@ static enum MHD_Result handler( return MHD_NO; } + 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 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, @@ -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, -- cgit v1.2.3