view database.c @ 78:9bfe5ce3cc45

pasterd: rework themes
author David Demelier <markand@malikania.fr>
date Thu, 16 Mar 2023 20:45:59 +0100
parents 1a98bc0daa49
children 52029a52a385
line wrap: on
line source

/*
 * database.c -- sqlite storage
 *
 * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <sqlite3.h>

#include "database.h"
#include "json-util.h"
#include "log.h"
#include "util.h"

#include "sql/clear.h"
#include "sql/get.h"
#include "sql/init.h"
#include "sql/insert.h"
#include "sql/recents.h"
#include "sql/search.h"

#define ID_MAX (12 + 1)

static sqlite3 *db;

static inline json_t *
convert(sqlite3_stmt *stmt)
{
	return json_pack("{ss ss ss ss ss sI si si}",
		"id",           sqlite3_column_text(stmt, 0),
		"title",        sqlite3_column_text(stmt, 1),
		"author",       sqlite3_column_text(stmt, 2),
		"language",     sqlite3_column_text(stmt, 3),
		"code",         sqlite3_column_text(stmt, 4),
		"timestamp",    (json_int_t)sqlite3_column_int64(stmt, 5),
		"visible",      sqlite3_column_int(stmt, 6),
		"duration",     sqlite3_column_int(stmt, 7)
	);
}

static int
exists(const char *id)
{
	assert(id);

	sqlite3_stmt *stmt = NULL;
	int ret = 1;

	if (sqlite3_prepare(db, sql_get, -1, &stmt, NULL) == SQLITE_OK) {
		sqlite3_bind_text(stmt, 1, id, -1, NULL);
		ret = sqlite3_step(stmt) == SQLITE_ROW;
		sqlite3_finalize(stmt);
	}

	return ret;
}

static const char *
create_id(char *id)
{
	static const char table[] = "abcdefghijklmnopqrstuvwxyz1234567890";

	for (int i = 0; i < ID_MAX; ++i)
		id[i] = table[rand() % (sizeof (table) - 1)];

	id[ID_MAX - 1] = 0;
}

static int
set_id(json_t *paste)
{
	/*
	 * Avoid infinite loop, we only try to create a new id in 30 steps.
	 *
	 * On error, the function `exist` returns true to indicate we should
	 * not try to save with that id.
	 */
	int tries = 0;
	char id[ID_MAX];

	do {
		create_id(id);
	} while (++tries < 30 && exists(id));

	if (tries >= 30)
		return -1;

	json_object_set_new(paste, "id", json_string(id));

	return 0;
}

int
database_open(const char *path)
{
	assert(path);

	log_info("database: opening %s", path);

	if (sqlite3_open(path, &db) != SQLITE_OK) {
		log_warn("database: unable to open %s: %s", path, sqlite3_errmsg(db));
		return -1;
	}

	/* Wait for 30 seconds to lock the database. */
	sqlite3_busy_timeout(db, 30000);

	if (sqlite3_exec(db, sql_init, NULL, NULL, NULL) != SQLITE_OK) {
		log_warn("database: unable to initialize %s: %s", path, sqlite3_errmsg(db));
		return -1;
	}

	return 0;
}

json_t *
database_recents(size_t limit)
{
	json_t *array = NULL;
	sqlite3_stmt *stmt = NULL;
	size_t i = 0;

	log_debug("database: accessing most recents");

	if (sqlite3_prepare(db, sql_recents, -1, &stmt, NULL) != SQLITE_OK ||
	    sqlite3_bind_int64(stmt, 1, limit) != SQLITE_OK)
		goto sqlite_err;

	array = json_array();

	for (; i < limit && sqlite3_step(stmt) == SQLITE_ROW; ++i)
		json_array_append_new(array, convert(stmt));

	log_debug("database: found %zu pastes", i);
	sqlite3_finalize(stmt);

	return array;

sqlite_err:
	log_warn("database: error (recents): %s\n", sqlite3_errmsg(db));

	if (stmt)
		sqlite3_finalize(stmt);

	return NULL;
}

json_t *
database_get(const char *id)
{
	assert(id);

	json_t *object = NULL;
	sqlite3_stmt* stmt = NULL;

	log_debug("database: accessing paste with uuid: %s", id);

	if (sqlite3_prepare(db, sql_get, -1, &stmt, NULL) != SQLITE_OK ||
	    sqlite3_bind_text(stmt, 1, id, -1, NULL) != SQLITE_OK)
		goto sqlite_err;

	switch (sqlite3_step(stmt)) {
	case SQLITE_ROW:
		object = convert(stmt);
		break;
	case SQLITE_MISUSE:
	case SQLITE_ERROR:
		goto sqlite_err;
	default:
		break;
	}

	sqlite3_finalize(stmt);

	return object;

sqlite_err:
	if (stmt)
		sqlite3_finalize(stmt);

	log_warn("database: error (get): %s", sqlite3_errmsg(db));

	return NULL;
}

int
database_insert(json_t *paste)
{
	assert(paste);

	sqlite3_stmt *stmt = NULL;

	log_debug("database: creating new paste");

	if (sqlite3_exec(db, "BEGIN EXCLUSIVE TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
		log_warn("database: could not lock database: %s", sqlite3_errmsg(db));
		return -1;
	}

	if (set_id(paste) < 0) {
		log_warn("database: unable to randomize unique identifier");
		sqlite3_exec(db, "END TRANSACTION", NULL, NULL, NULL);
		return -1;
	}

	if (sqlite3_prepare(db, sql_insert, -1, &stmt, NULL) != SQLITE_OK)
		goto sqlite_err;

	sqlite3_bind_text(stmt, 1, ju_get_string(paste, "id"), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, ju_get_string(paste, "title"), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 3, ju_get_string(paste, "author"), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 4, ju_get_string(paste, "language"), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 5, ju_get_string(paste, "code"), -1, SQLITE_STATIC);
	sqlite3_bind_int(stmt, 6, ju_get_bool(paste, "visible"));
	sqlite3_bind_int64(stmt, 7, ju_get_int(paste, "duration"));

	if (sqlite3_step(stmt) != SQLITE_DONE)
		goto sqlite_err;

	sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
	sqlite3_finalize(stmt);

	log_info("database: new paste (%s) from %s expires in one %lld seconds",
	    ju_get_string(paste, "id"),
	    ju_get_string(paste, "author"),
	    ju_get_int(paste, "duration")
	);

	return 0;

sqlite_err:
	log_warn("database: error (insert): %s", sqlite3_errmsg(db));
	sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);

	if (stmt)
		sqlite3_finalize(stmt);

	/* Make sure it is not used anymore. */
	json_object_del(paste, "id");

	return 0;
}

json_t *
database_search(size_t limit,
                const char *title,
                const char *author,
                const char *language)
{
	json_t *array = NULL;
	sqlite3_stmt *stmt = NULL;
	size_t i = 0;

	log_debug("database: searching title=%s, author=%s, language=%s",
	    title    ? title    : "",
	    author   ? author   : "",
	    language ? language : "");

	/* Select everything if not specified. */
	title    = title    ? title    : "%";
	author   = author   ? author   : "%";
	language = language ? language : "%";

	if (sqlite3_prepare(db, sql_search, -1, &stmt, NULL) != SQLITE_OK)
		goto sqlite_err;
	if (sqlite3_bind_text(stmt, 1, title, -1, NULL) != SQLITE_OK)
		goto sqlite_err;
	if (sqlite3_bind_text(stmt, 2, author, -1, NULL) != SQLITE_OK)
		goto sqlite_err;
	if (sqlite3_bind_text(stmt, 3, language, -1, NULL) != SQLITE_OK)
		goto sqlite_err;
	if (sqlite3_bind_int64(stmt, 4, limit) != SQLITE_OK)
		goto sqlite_err;

	array = json_array();

	for (; i < limit && sqlite3_step(stmt) == SQLITE_ROW; ++i)
		json_array_append_new(array, convert(stmt));

	log_debug("database: found %zu pastes", i);
	sqlite3_finalize(stmt);

	return array;

sqlite_err:
	log_warn("database: error (search): %s\n", sqlite3_errmsg(db));

	if (stmt)
		sqlite3_finalize(stmt);

	return NULL;
}

void
database_clear(void)
{
	log_debug("database: clearing deprecated pastes");

	if (sqlite3_exec(db, sql_clear, NULL, NULL, NULL) != SQLITE_OK)
		log_warn("database: error (clear): %s\n", sqlite3_errmsg(db));
}

void
database_finish(void)
{
	log_debug("database: closing");

	if (db) {
		sqlite3_close(db);
		db = NULL;
	}
}