#include "app.h" #include "mytime.h" #include #include #include #include #include #include #include #include #include #include #include static const char *backup_status_db_strings[] = { [STATUS_RUNNING] = "RUNNING", [STATUS_FINISHED] = "FINISHED", [STATUS_INTERRUPTED] = "INTERRUPTED", }; const char *backup_status_to_db_string(enum BackupStatus status) { assert(status >= 0 && status <= STATUS_INTERRUPTED); return backup_status_db_strings[status]; } enum BackupStatus backup_status_from_db_string(const char *str) { assert(str != NULL); for (int i = 0; i <= STATUS_INTERRUPTED; i++) { if (strcmp(str, backup_status_db_strings[i]) == 0) { return (enum BackupStatus)i; } } abort(); } 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) abort(); ssize_t n = read(fd, bytes, 16); close(fd); if (n != 16) abort(); 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 = ?3, completed = datetime(?1, 'unixepoch', 'subsec') WHERE client = ?2 AND status = ?4", -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_bind_text(stmt, 3, backup_status_to_db_string(STATUS_INTERRUPTED), -1, SQLITE_STATIC) != SQLITE_OK) { abort(); } if(sqlite3_bind_text(stmt, 4, backup_status_to_db_string(STATUS_RUNNING), -1, SQLITE_STATIC) != 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, backup_status_to_db_string(STATUS_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 = ?2", -1, &stmt, NULL) != SQLITE_OK) { abort(); } if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) { abort(); } if(sqlite3_bind_text(stmt, 2, backup_status_to_db_string(STATUS_RUNNING), -1, SQLITE_STATIC) != 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, backup_status_to_db_string(STATUS_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) { result->clients[index].last_backup.tv_sec = 0; result->clients[index].status = 0; result->clients[index].is_stale = 0; } else { result->clients[index].status = backup_status_from_db_string((char*)backup_status); 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); result->backups[index].status = backup_status_from_db_string(status ? (char*)status : NULL); 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; }