summaryrefslogtreecommitdiff
path: root/src/app.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.c')
-rw-r--r--src/app.c639
1 files changed, 639 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
new file mode 100644
index 0000000..9d1ee7a
--- /dev/null
+++ b/src/app.c
@@ -0,0 +1,639 @@
+#include "app.h"
+#include "mytime.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <linux/limits.h>
+#include <libgen.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sqlite3.h>
+#include <fcntl.h>
+
+static void sqliteError(void *pArg, int iErrCode, const char *zMsg) {
+ // @CLEANUP: We should stuff these somewhere, but for now just print it
+ fprintf(stderr, "(%d) %s\n", iErrCode, zMsg);
+}
+
+static int generate_secret(char *out) {
+ unsigned char bytes[16];
+ int fd = open("/dev/urandom", O_RDONLY);
+ if (fd < 0) return -1;
+ ssize_t n = read(fd, bytes, 16);
+ close(fd);
+ if (n != 16) return -1;
+ static const char hex[] = "0123456789abcdef";
+ for (int i = 0; i < 16; i++) {
+ out[i*2] = hex[(bytes[i] >> 4) & 0x0f];
+ out[i*2+1] = hex[bytes[i] & 0x0f];
+ }
+ out[32] = '\0';
+ return 0;
+}
+
+// https://stackoverflow.com/questions/2336242/recursive-mkdir-system-call-on-unix
+static void _mkdir(const char *dir) {
+ char tmp[PATH_MAX];
+ char *p = NULL;
+ size_t len;
+ int rc;
+
+ snprintf(tmp, sizeof(tmp),"%s",dir);
+ len = strlen(tmp);
+ if (tmp[len - 1] == '/')
+ tmp[len - 1] = 0;
+ for (p = tmp + 1; *p; p++)
+ if (*p == '/') {
+ *p = 0;
+ rc = mkdir(tmp, S_IRWXU);
+ if(rc != 0 && errno != EEXIST) {
+ abort();
+ }
+ *p = '/';
+ }
+ rc = mkdir(tmp, S_IRWXU);
+ if(rc != 0 && errno != EEXIST) {
+ abort();
+ }
+
+}
+
+void app_startup(struct App *app) {
+ // @CLEANUP: Move this to main? doesn't belong here at least
+ sqlite3_config(SQLITE_CONFIG_LOG, sqliteError, NULL);
+ sqlite3_config(SQLITE_CONFIG_URI, 1);
+
+ int err;
+
+ printf("Running migrations against %s\n", app->dbname);
+
+ if(memcmp("file::memory", app->dbname, 12) != 0) {
+ char *full = strdup(app->dbname);
+ char *dbdir = dirname(full);
+ _mkdir(dbdir);
+ free(full);
+ }
+
+ sqlite3 *conn;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ fprintf(stderr, "(%d) %s\n", err, sqlite3_errmsg(conn));
+ abort();
+ }
+ sqlite3_db_config(conn, SQLITE_DBCONFIG_ENABLE_FKEY, 1, NULL);
+
+ if(sqlite3_exec(conn, "CREATE TABLE IF NOT EXISTS migration (id INTEGER PRIMARY KEY NOT NULL)", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT MAX(id) FROM migration", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ uint64_t maxVersion = 0;
+ while((err = sqlite3_step(stmt)) == SQLITE_ROW) {
+ maxVersion = sqlite3_column_int64(stmt, 0);
+ }
+ if(err != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(maxVersion < 1) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "CREATE TABLE backups ("
+ "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ "client TEXT NOT NULL, "
+ "status TEXT NOT NULL, "
+ "started TEXT NOT NULL, "
+ "completed TEXT "
+ ") STRICT";
+ if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_int64(stmt, 1, 1) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(maxVersion < 2) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "CREATE TABLE clients ("
+ "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ "name TEXT NOT NULL "
+ ") STRICT;"
+ "INSERT INTO clients(name) "
+ "SELECT b.client "
+ "FROM backups b "
+ "GROUP BY b.client;"
+ "CREATE TABLE backups_next ("
+ "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ "client INTEGER NOT NULL, "
+ "status TEXT NOT NULL, "
+ "started TEXT NOT NULL, "
+ "completed TEXT ,"
+ "FOREIGN KEY(client) REFERENCES clients(id)"
+ ") STRICT;"
+ "INSERT INTO backups_next(id, client, status, started, completed) "
+ "SELECT b.id, c.id, b.status, b.started, b.completed "
+ "FROM backups b "
+ "JOIN clients c ON b.client = c.name;"
+ "DROP TABLE backups;"
+ "ALTER TABLE backups_next RENAME TO backups;";
+ if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_int64(stmt, 1, 2) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(maxVersion < 3) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "CREATE UNIQUE INDEX clients_name ON clients(name)";
+ if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_int64(stmt, 1, 3) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(maxVersion < 4) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "ALTER TABLE clients ADD COLUMN secret TEXT NOT NULL DEFAULT ''";
+ if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_int64(stmt, 1, 4) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+ }
+}
+
+int submit_report(struct App *app, char *client, char *secret, int is_finish) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ struct timespec now = get_current_time();
+
+ int64_t clientId;
+ {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id, secret FROM clients WHERE name = ?1", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 1, client, -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_ROW) {
+ abort();
+ }
+
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
+ abort();
+ }
+ clientId = sqlite3_column_int64(stmt, 0);
+
+ const char *stored_secret = (const char *)sqlite3_column_text(stmt, 1);
+ if(stored_secret == NULL || strcmp(stored_secret, secret) != 0) {
+ sqlite3_finalize(stmt);
+ sqlite3_close(conn);
+ return -2;
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(!is_finish) {
+ // 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();
+ }
+ if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 2, "RUNNING", -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_double(stmt, 3, TIME_AS_FLOAT(now)) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ } else {
+ // REV_FINISH
+ uint64_t backupId;
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id FROM backups WHERE client = ?1 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_ROW) {
+ abort();
+ }
+
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
+ abort();
+ }
+ backupId = sqlite3_column_int64(stmt, 0);
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ stmt = NULL;
+
+ if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = ?1, completed = datetime(?2, 'unixepoch', 'subsec') WHERE id = ?3", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 1, "FINISHED", -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_double(stmt, 2, TIME_AS_FLOAT(now)) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 3, backupId) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
+
+int create_client(struct App *app, char *name, struct CreateClientResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ if(generate_secret(result->secret) != 0) {
+ abort();
+ }
+
+ {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO clients (name, secret) VALUES (?1, ?2)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 2, result->secret, -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ result->id = sqlite3_last_insert_rowid(conn);
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
+
+int delete_client(struct App *app, int64_t client_id) {
+ sqlite3 *conn;
+ if(sqlite3_open(app->dbname, &conn) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "DELETE FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
+
+int list_clients(struct App *app, struct ListClientResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ {
+ sqlite3_stmt *stmt;
+ const char *sql =
+ "SELECT c.id, c.name, c.secret, unixepoch(b_success.completed,'subsec'), b_last.status "
+ "FROM clients c "
+ "LEFT JOIN backups b_success ON b_success.client = c.id "
+ " AND b_success.id = (SELECT id FROM backups WHERE client = c.id AND status = 'FINISHED' ORDER BY id DESC LIMIT 1) "
+ "LEFT JOIN backups b_last ON b_last.client = c.id "
+ " AND b_last.id = (SELECT id FROM backups WHERE client = c.id ORDER BY id DESC LIMIT 1) "
+ "WHERE c.id > ?1 ORDER BY c.id ASC LIMIT ?2";
+ if(sqlite3_prepare_v2(conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, -1) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 2, 16) != SQLITE_OK) {
+ abort();
+ }
+
+ int ret;
+ int index = 0;
+ while((ret = sqlite3_step(stmt)) == SQLITE_ROW) {
+ assert(index < 16);
+
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
+ abort();
+ }
+
+ int64_t clientId = sqlite3_column_int64(stmt, 0);
+ const uint8_t *name = sqlite3_column_text(stmt, 1);
+ const uint8_t *secret = sqlite3_column_text(stmt, 2);
+ const double last_backup = sqlite3_column_double(stmt, 3);
+ const uint8_t *backup_status = sqlite3_column_text(stmt, 4);
+
+ result->clients[index].id = clientId;
+ strncpy(result->clients[index].name, (char*)name, CLIENT_NAME_MAX);
+ strncpy(result->clients[index].secret, secret ? (char*)secret : "", CLIENT_SECRET_LEN);
+ result->clients[index].last_backup = time_from_double(last_backup);
+
+ if (backup_status == NULL) {
+ strncpy(result->clients[index].status, "MISSING", BACKUP_STATUS_MAX);
+ result->clients[index].is_stale = 0;
+ } else if (strcmp((char*)backup_status, "RUNNING") == 0) {
+ strncpy(result->clients[index].status, "RUNNING", BACKUP_STATUS_MAX);
+ result->clients[index].is_stale = 0;
+ } else if (strcmp((char*)backup_status, "INTERRUPTED") == 0) {
+ strncpy(result->clients[index].status, "INTERRUPTED", BACKUP_STATUS_MAX);
+ result->clients[index].is_stale = 0;
+ } else {
+ strncpy(result->clients[index].status, "OK", BACKUP_STATUS_MAX);
+ result->clients[index].is_stale = 0;
+ }
+
+ index++;
+ }
+ if(ret != SQLITE_DONE) {
+ abort();
+ }
+
+ result->clients_len = index;
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
+
+int get_client(struct App *app, int64_t client_id, struct GetClientOnlyResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ result->found = 0;
+
+ 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(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
+
+int get_client_backups(struct App *app, int64_t client_id, struct GetBackupsResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ uint16_t limit = result->backups_len;
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id, status, unixepoch(started, 'subsec'), unixepoch(completed, 'subsec') FROM backups WHERE client = ?1 ORDER BY started DESC LIMIT ?2", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int(stmt, 2, limit) != SQLITE_OK) {
+ abort();
+ }
+
+ int ret;
+ int index = 0;
+ while((ret = sqlite3_step(stmt)) == SQLITE_ROW) {
+ if(index >= limit) break;
+
+ result->backups[index].id = sqlite3_column_int64(stmt, 0);
+
+ const uint8_t *status = sqlite3_column_text(stmt, 1);
+ const double started = sqlite3_column_double(stmt, 2);
+ const double completed = sqlite3_column_double(stmt, 3);
+
+ strncpy(result->backups[index].status, status ? (char*)status : "", BACKUP_STATUS_MAX);
+ result->backups[index].started = time_from_double(started);
+ result->backups[index].completed = time_from_double(completed);
+
+ 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;
+}