Initial project structure. (#1)

This commit is contained in:
Savinda Senevirathne
2021-06-02 10:15:07 +05:30
committed by GitHub
parent 5da5d4e303
commit 220d65bd0b
14 changed files with 1189 additions and 16 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
*~
*.swp
*.swo
.*.sw?
.DS_Store
build
.vscode
**/Makefile
**/CMakeCache.txt
**/cmake_install.cmake
**/CMakeFiles

28
CMakeLists.txt Normal file
View File

@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.16)
project(sagent)
# Force build type to Release build.
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY build)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY build)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY build)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-result -Wreturn-type")
add_executable(sagent
src/util/util.cpp
src/conf.cpp
src/salog.cpp
src/sqlite.cpp
src/main.cpp
)
target_link_libraries(sagent
sqlite3
${CMAKE_DL_LIBS} # Needed for stacktrace support
)
target_precompile_headers(sagent PUBLIC src/pchheader.hpp)

View File

@@ -1,20 +1,27 @@
# Introduction
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
# Sashimono Agent
# Getting Started
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
1. Installation process
2. Software dependencies
3. Latest releases
4. API references
## What's here?
*In development*
# Build and Test
TODO: Describe and show how to build your code and run the tests.
A C++ version of sashimono agent
# Contribute
TODO: Explain how other users and developers can contribute to make your code better.
## Libraries
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
- [ASP.NET Core](https://github.com/aspnet/Home)
- [Visual Studio Code](https://github.com/Microsoft/vscode)
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
## Setting up Sashimono Agent environment
Run the setup script located at the repo root (tested on Ubuntu 18.04).
```
./dev-setup.sh
```
## Build Sashimono Agent
1. Run `cmake .` (You only have to do this once)
1. Run `make` (Sashimono agent binary will be created as `./build/sagent`)
## Code structure
Code is divided into subsystems via namespaces.
**conf::** Handles configuration. Loads and holds the central configuration object. Used by most of the subsystems.
**util::** Contains shared data structures/helper functions used by multiple subsystems.
**sqlite::** Contains sqlite database management related helper functions.

53
dev-setup.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Usage ./dev-setup.sh
# Sashimono agent build environment setup script.
set -e # exit on error
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev
workdir=~/sagent-setup
mkdir $workdir
pushd $workdir > /dev/null 2>&1
# CMAKE
cmake=cmake-3.16.0-rc3-Linux-x86_64
wget https://github.com/Kitware/CMake/releases/download/v3.16.0-rc3/$cmake.tar.gz
tar -zxvf $cmake.tar.gz
sudo cp -r $cmake/bin/* /usr/local/bin/
sudo cp -r $cmake/share/* /usr/local/share/
rm $cmake.tar.gz && rm -r $cmake
# jsoncons
wget https://github.com/danielaparker/jsoncons/archive/v0.153.3.tar.gz
tar -zxvf v0.153.3.tar.gz
pushd jsoncons-0.153.3 > /dev/null 2>&1
sudo cp -r include/jsoncons /usr/local/include/
sudo mkdir -p /usr/local/include/jsoncons_ext/
sudo cp -r include/jsoncons_ext/bson /usr/local/include/jsoncons_ext/
popd > /dev/null 2>&1
rm v0.153.3.tar.gz && rm -r jsoncons-0.153.3
# Sqlite
sudo apt-get install -y sqlite3 libsqlite3-dev
# Plog
wget https://github.com/SergiusTheBest/plog/archive/1.1.5.tar.gz
tar -zxvf 1.1.5.tar.gz
pushd plog-1.1.5 > /dev/null 2>&1
sudo cp -r include/plog /usr/local/include/
popd > /dev/null 2>&1
rm 1.1.5.tar.gz && rm -r plog-1.1.5
# Update linker library cache.
sudo ldconfig
# Pop workdir
popd > /dev/null 2>&1
rm -r $workdir
# Build sagent
cmake .
make

334
src/conf.cpp Normal file
View File

@@ -0,0 +1,334 @@
#include "conf.hpp"
#include "util/util.hpp"
namespace conf
{
// Global contract context struct exposed to the application.
sa_context ctx;
// Global configuration struct exposed to the application.
sa_config cfg;
constexpr int FILE_PERMS = 0644;
bool init_success = false;
/**
* Loads and initializes the config for execution. Must be called once during application startup.
* @return 0 for success. -1 for failure.
*/
int init()
{
if (validate_dir_paths() == -1 ||
read_config(cfg) == -1 ||
validate_config(cfg) == -1)
return -1;
init_success = true;
return 0;
}
/**
* Cleanup any resources.
*/
void deinit()
{
if (init_success)
{
// Deinit here.
}
}
/**
* Create config here.
* @return 0 for success. -1 for failure.
*/
int create()
{
if (util::is_dir_exists(ctx.config_dir))
{
if (util::is_file_exists(ctx.config_file))
{
std::cerr << "Config file already exists. Cannot create config at the same location.\n";
return -1;
}
}
else
{
// Recursivly create contract directory. Return an error if unable to create
if (util::create_dir_tree_recursive(ctx.config_dir) == -1 ||
util::create_dir_tree_recursive(ctx.log_dir) == -1)
{
std::cerr << "ERROR: unable to create directories.\n";
return -1;
}
}
//Create config file with default settings.
//We populate the in-memory struct with default settings and then save it to the file.
{
sa_config cfg = {};
cfg.version = "0.0.1";
cfg.log.max_file_count = 50;
cfg.log.max_mbytes_per_file = 10;
cfg.log.log_level = "inf";
cfg.log.loggers.emplace("console");
cfg.log.loggers.emplace("file");
//Save the default settings into the config file.
if (write_config(cfg) != 0)
return -1;
}
std::cout << "Config file created at " << ctx.config_file << std::endl;
return 0;
}
/**
* Updates the context with directory paths based on provided base directory.
* This is called after parsing SA command line arg in order to populate the ctx.
* @param basedir Path to base directory.
*/
void set_dir_paths(std::string basedir)
{
if (basedir.empty())
{
// This code branch will never execute the way main is currently coded, but it might change in future
std::cerr << "Base directory must be specified\n";
exit(1);
}
// Resolving the path through realpath will remove any trailing slash if present
// Set config directory to the parent of the exec binary.
basedir = dirname((char *)util::realpath(basedir).data());
ctx.config_dir = basedir + "/cfg";
ctx.config_file = ctx.config_dir + "/sa.cfg";
ctx.log_dir = basedir + "/log";
}
/**
* Checks for the existence of all contract sub directories.
* @return 0 for successful validation. -1 for failure.
*/
int validate_dir_paths()
{
const std::string paths[2] = {
ctx.config_file,
ctx.log_dir};
for (const std::string &path : paths)
{
if (!util::is_file_exists(path) && !util::is_dir_exists(path))
{
std::cerr << path << " does not exist.\n";
return -1;
}
}
return 0;
}
/**
* Reads the config file on disk and populates the in-memory 'cfg' struct.
* @param cfg Config to populate.
* @return 0 for successful loading of config. -1 for failure.
*/
int read_config(sa_config &cfg)
{
int fd = open(ctx.config_file.data(), O_RDWR, 444);
if (fd == -1)
return -1;
// Read the config file into json document object.
std::string buf;
if (util::read_from_fd(fd, buf) == -1)
{
std::cerr << "Error reading from the config file. " << errno << '\n';
return -1;
}
jsoncons::ojson d;
try
{
d = jsoncons::ojson::parse(buf, jsoncons::strict_json_parsing());
}
catch (const std::exception &e)
{
std::cerr << "Invalid config file format. " << e.what() << '\n';
return -1;
}
buf.clear();
try
{
// Check whether the version is specified.
cfg.version = d["version"].as<std::string>();
if (cfg.version.empty())
{
std::cerr << "Config version missing.\n";
return -1;
}
}
catch (const std::exception &e)
{
std::cerr << "Required config field version missing at " << ctx.config_file << std::endl;
return -1;
}
std::string jpath;
// log
{
jpath = "log";
try
{
const jsoncons::ojson &log = d["log"];
cfg.log.log_level = log["log_level"].as<std::string>();
cfg.log.log_level_type = get_loglevel_type(cfg.log.log_level);
cfg.log.max_mbytes_per_file = log["max_mbytes_per_file"].as<size_t>();
cfg.log.max_file_count = log["max_file_count"].as<size_t>();
cfg.log.loggers.clear();
for (auto &v : log["loggers"].array_range())
cfg.log.loggers.emplace(v.as<std::string>());
}
catch (const std::exception &e)
{
print_missing_field_error(jpath, e);
return -1;
}
}
return 0;
}
/**
* Saves the provided 'cfg' struct into the config file.
* @param cfg Config to write.
* @return 0 for successful save. -1 for failure.
*/
int write_config(const sa_config &cfg)
{
// Popualte json document with 'cfg' values.
// ojson is used instead of json to preserve insertion order.
jsoncons::ojson d;
d.insert_or_assign("version", cfg.version);
// Log configs.
{
jsoncons::ojson log_config;
log_config.insert_or_assign("log_level", cfg.log.log_level);
log_config.insert_or_assign("max_mbytes_per_file", cfg.log.max_mbytes_per_file);
log_config.insert_or_assign("max_file_count", cfg.log.max_file_count);
jsoncons::ojson loggers(jsoncons::json_array_arg);
for (std::string_view logger : cfg.log.loggers)
{
loggers.push_back(logger);
}
log_config.insert_or_assign("loggers", loggers);
d.insert_or_assign("log", log_config);
}
return write_json_file(ctx.config_file, d);
}
/**
* Writes the given json doc to a file.
* @param file_path Path to the file.
* @param d Json object.
* @return 0 on success. -1 on failure.
*/
int write_json_file(const std::string &file_path, const jsoncons::ojson &d)
{
std::string json;
// Convert json object to a string.
try
{
jsoncons::json_options options;
options.object_array_line_splits(jsoncons::line_split_kind::multi_line);
options.spaces_around_comma(jsoncons::spaces_option::no_spaces);
std::ostringstream os;
os << jsoncons::pretty_print(d, options);
json = os.str();
os.clear();
}
catch (const std::exception &e)
{
std::cerr << "Converting json to string failed. " << file_path << std::endl;
return -1;
}
// O_TRUNC flag is used to trucate existing content from the file.
const int fd = open(file_path.data(), O_CREAT | O_RDWR | O_TRUNC, FILE_PERMS);
if (fd == -1 || write(fd, json.data(), json.size()) == -1)
{
std::cerr << "Writing file failed. " << file_path << std::endl;
if (fd != -1)
close(fd);
return -1;
}
close(fd);
return 0;
}
/**
* Convert string to Log Severity enum type.
* @param severity log severity code.
* @return log severity type.
*/
LOG_SEVERITY get_loglevel_type(std::string_view severity)
{
if (severity == "dbg")
return LOG_SEVERITY::DEBUG;
else if (severity == "wrn")
return LOG_SEVERITY::WARN;
else if (severity == "inf")
return LOG_SEVERITY::INFO;
else
return LOG_SEVERITY::ERROR;
}
/**
* Prints the config json parsing field missing error.
* @param jpath Json path of the feild.
* @param e Exception.
*/
void print_missing_field_error(std::string_view jpath, const std::exception &e)
{
// Extract field name from jsoncons exception message.
std::cerr << "Config validation error: " << e.what() << " in '" << jpath << "' section at " << ctx.config_file << std::endl;
}
/**
* Validates the 'cfg' struct for invalid values.
* @param cfg Config to validate.
* @return 0 for successful validation. -1 for failure.
*/
int validate_config(const sa_config &cfg)
{
// Other required fields.
bool fields_invalid = false;
fields_invalid |= cfg.log.log_level.empty() && std::cerr << "Invalid value for loglevel.\n";
if (fields_invalid)
{
std::cerr << "Invalid configuration values at " << ctx.config_file << std::endl;
return -1;
}
const std::unordered_set<std::string> valid_loglevels({"dbg", "inf", "wrn", "err"});
if (valid_loglevels.count(cfg.log.log_level) != 1)
{
std::cerr << "Invalid loglevel configured. Valid values: dbg|inf|wrn|err\n";
return -1;
}
return 0;
}
}

73
src/conf.hpp Normal file
View File

@@ -0,0 +1,73 @@
#ifndef _SA_CONF_
#define _SA_CONF_
#include "pchheader.hpp"
namespace conf
{
// Log severity levels used in Sashimono agent.
enum LOG_SEVERITY
{
DEBUG,
INFO,
WARN,
ERROR
};
struct log_config
{
std::string log_level; // Log severity level (dbg, inf, wrn, wrr)
LOG_SEVERITY log_level_type; // Log severity level enum (debug, info, warn, error)
std::unordered_set<std::string> loggers; // List of enabled loggers (console, file)
size_t max_mbytes_per_file = 0; // Max MB size of a single log file.
size_t max_file_count = 0; // Max no. of log files to keep.
};
struct sa_config
{
std::string version;
log_config log;
};
struct sa_context
{
std::string command; // The CLI command issued to launch Sashimono agent
std::string config_dir; // Config dir full path.
std::string config_file; // Full path to the config file.
std::string log_dir; // Log directory full path.
};
// Global context struct exposed to the application.
// Other modules will access context values via this.
extern sa_context ctx;
// Global configuration struct exposed to the application.
// Other modules will access config values via this.
extern sa_config cfg;
int init();
void deinit();
int create();
void set_dir_paths(std::string basedir);
int validate_dir_paths();
int read_config(sa_config &cfg);
int write_config(const sa_config &cfg);
int write_json_file(const std::string &file_path, const jsoncons::ojson &d);
LOG_SEVERITY get_loglevel_type(std::string_view severity);
void print_missing_field_error(std::string_view jpath, const std::exception &e);
int validate_config(const sa_config &cfg);
}
#endif

104
src/main.cpp Normal file
View File

@@ -0,0 +1,104 @@
/**
Entry point for Sashimono
**/
#include "pchheader.hpp"
#include "conf.hpp"
#include "sqlite.hpp"
#include "salog.hpp"
/**
* Parses CLI args and extracts sashimono agent command and parameters given.
* @param argc Argument count.
* @param argv Arguments.
* @returns 0 on success, -1 on error.
*/
int parse_cmd(int argc, char **argv)
{
conf::ctx.command = argv[1];
if (argc == 2 && //We get working dir as an arg anyway. So we need to check for ==2 args.
(conf::ctx.command == "new" || conf::ctx.command == "run" || conf::ctx.command == "version"))
{
// We populate the global contract ctx with the detected command.
conf::set_dir_paths(argv[0]);
return 0;
}
// If all extractions fail display help message.
std::cerr << "Arguments mismatch.\n";
std::cout << "Usage:\n";
std::cout << "sagent version\n";
std::cout << "sagent <command> (command = run | new | rekey)\n";
std::cout << "Example: sagent run\n";
return -1;
}
/**
* Performs any cleanup on graceful application termination.
*/
void deinit()
{
conf::deinit();
}
int main(int argc, char **argv)
{
// Extract the CLI args
// This call will populate conf::ctx
if (parse_cmd(argc, argv) != 0)
return -1;
if (conf::ctx.command == "new")
{
// This will create a new config.
if (conf::create() != 0)
return -1;
}
else
{
if (conf::init() != 0)
return -1;
salog::init(); // Initialize logger for SA.
if (conf::ctx.command == "run")
{
LOG_INFO << "Sashimono agent started. Version : " << conf::cfg.version << " Log level : " << conf::cfg.log.log_level;
// Run the program.
sqlite3 *db = NULL;
const char *path = "db.sqlite";
if (sqlite::open_db(path, &db, true) == -1)
{
LOG_ERROR << "Error opening database";
return -1;
}
LOG_INFO << "Database " << path << " opened successfully";
const std::vector<sqlite::table_column_info> column_info{
sqlite::table_column_info("VERSION", sqlite::COLUMN_DATA_TYPE::TEXT)};
if (create_table(db, "SA_VERSION", column_info) == -1)
return -1;
if (sqlite::insert_row(db, "SA_VERSION", "VERSION", "\"0.0.0\"") == -1)
return -1;
if (sqlite::close_db(&db) == -1)
{
LOG_ERROR << "Error closing database";
return -1;
}
}
else if (conf::ctx.command == "version")
// Print the version
LOG_INFO << "Sashimono Agent " << conf::cfg.version;
deinit();
}
LOG_INFO << "sashimono agent exited normally.";
return 0;
}

19
src/pchheader.hpp Normal file
View File

@@ -0,0 +1,19 @@
#ifndef _SA_PCHHEADER_
#define _SA_PCHHEADER_
#include <algorithm>
#include <fcntl.h>
#include <iostream>
#include <jsoncons/json.hpp>
#include <libgen.h>
#include <string>
#include <string_view>
#include <sqlite3.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unordered_set>
#include <vector>
#include <plog/Log.h>
#include <plog/Appenders/ColorConsoleAppender.h>
#endif

90
src/salog.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include "pchheader.hpp"
#include "conf.hpp"
#include "salog.hpp"
namespace salog
{
class plog_formatter;
// Custom formatter adopted from:
// https://github.com/SergiusTheBest/plog/blob/master/include/plog/Formatters/TxtFormatter.h
class plog_formatter
{
public:
static plog::util::nstring header()
{
return plog::util::nstring();
}
static inline const char *severity_to_string(plog::Severity severity)
{
switch (severity)
{
case plog::Severity::fatal:
return "fat";
case plog::Severity::error:
return "err";
case plog::Severity::warning:
return "wrn";
case plog::Severity::info:
return "inf";
case plog::Severity::debug:
return "dbg";
case plog::Severity::verbose:
return "ver";
default:
return "def";
}
}
static plog::util::nstring format(const plog::Record &record)
{
tm t;
plog::util::localtime_s(&t, &record.getTime().time); // local time
plog::util::nostringstream ss;
ss << t.tm_year + 1900 << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_mon + 1 << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_mday << PLOG_NSTR(" ");
ss << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_hour << PLOG_NSTR(":")
<< std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_min << PLOG_NSTR(":")
<< std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_sec << PLOG_NSTR(" ");
// Uncomment for millseconds.
// << std::setfill(PLOG_NSTR('0')) << std::setw(3) << record.getTime().millitm << PLOG_NSTR(" ");
ss << PLOG_NSTR("[") << severity_to_string(record.getSeverity()) << PLOG_NSTR("][sa] ");
ss << record.getMessage() << PLOG_NSTR("\n");
return ss.str();
}
};
void init()
{
plog::Severity level;
if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::DEBUG)
level = plog::Severity::debug;
else if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::INFO)
level = plog::Severity::info;
else if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::WARN)
level = plog::Severity::warning;
else
level = plog::Severity::error;
const std::string trace_file = conf::ctx.log_dir + "/sa.log";
static plog::RollingFileAppender<plog_formatter> fileAppender(trace_file.c_str(), conf::cfg.log.max_mbytes_per_file * 1024 * 1024, conf::cfg.log.max_file_count);
static plog::ColorConsoleAppender<plog_formatter> consoleAppender;
plog::Logger<0> &logger = plog::init(level);
// Take decision to append logger for file / console or both.
if (conf::cfg.log.loggers.count("console") == 1)
{
logger.addAppender(&consoleAppender);
}
if (conf::cfg.log.loggers.count("file") == 1)
{
logger.addAppender(&fileAppender);
}
}
} // namespace salog

9
src/salog.hpp Normal file
View File

@@ -0,0 +1,9 @@
#ifndef _SA_SALOG_
#define _SA_SALOG_
namespace salog
{
void init();
} // namespace salog
#endif

257
src/sqlite.cpp Normal file
View File

@@ -0,0 +1,257 @@
#include "sqlite.hpp"
#include "salog.hpp"
namespace sqlite
{
constexpr const char *COLUMN_DATA_TYPES[]{"INT", "TEXT", "BLOB"};
constexpr const char *CREATE_TABLE = "CREATE TABLE IF NOT EXISTS ";
constexpr const char *CREATE_INDEX = "CREATE INDEX ";
constexpr const char *CREATE_UNIQUE_INDEX = "CREATE UNIQUE INDEX ";
constexpr const char *BEGIN_TRANSACTION = "BEGIN TRANSACTION;";
constexpr const char *COMMIT_TRANSACTION = "COMMIT;";
constexpr const char *ROLLBACK_TRANSACTION = "ROLLBACK;";
constexpr const char *INSERT_INTO = "INSERT INTO ";
constexpr const char *PRIMARY_KEY = "PRIMARY KEY";
constexpr const char *NOT_NULL = "NOT NULL";
constexpr const char *VALUES = "VALUES";
constexpr const char *SELECT_ALL = "SELECT * FROM ";
constexpr const char *SQLITE_MASTER = "sqlite_master";
constexpr const char *WHERE = " WHERE ";
constexpr const char *AND = " AND ";
/**
* Opens a connection to a given databse and give the db pointer.
* @param db_name Database name to be connected.
* @param db Pointer to the db pointer which is to be connected and pointed.
* @param writable Whether the database must be opened in a writable mode or not.
* @returns returns 0 on success, or -1 on error.
*/
int open_db(std::string_view db_name, sqlite3 **db, const bool writable)
{
int ret;
const int flags = writable ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY;
if ((ret = sqlite3_open_v2(db_name.data(), db, flags, 0)) != SQLITE_OK)
{
LOG_ERROR << ret << ": Sqlite error when opening database " << db_name;
*db = NULL;
return -1;
}
return 0;
}
/**
* Executes given sql query.
* @param db Pointer to the db.
* @param sql Sql query to be executed.
* @param callback Callback funcion which is called for each result row.
* @param callback_first_arg First data argumat to be parced to the callback (void pointer).
* @returns returns 0 on success, or -1 on error.
*/
int exec_sql(sqlite3 *db, std::string_view sql, int (*callback)(void *, int, char **, char **), void *callback_first_arg)
{
char *err_msg;
if (sqlite3_exec(db, sql.data(), callback, (callback != NULL ? (void *)callback_first_arg : NULL), &err_msg) != SQLITE_OK)
{
LOG_ERROR << "SQL error occured: " << err_msg;
sqlite3_free(err_msg);
return -1;
}
return 0;
}
int begin_transaction(sqlite3 *db)
{
return sqlite::exec_sql(db, BEGIN_TRANSACTION);
}
int commit_transaction(sqlite3 *db)
{
return sqlite::exec_sql(db, COMMIT_TRANSACTION);
}
int rollback_transaction(sqlite3 *db)
{
return sqlite::exec_sql(db, ROLLBACK_TRANSACTION);
}
/**
* Create a table with given table info.
* @param db Pointer to the db.
* @param table_name Table name to be created.
* @param column_info Column info of the table.
* @returns returns 0 on success, or -1 on error.
*/
int create_table(sqlite3 *db, std::string_view table_name, const std::vector<table_column_info> &column_info)
{
std::string sql;
sql.append(CREATE_TABLE).append(table_name).append(" (");
for (auto itr = column_info.begin(); itr != column_info.end(); ++itr)
{
sql.append(itr->name);
sql.append(" ");
sql.append(COLUMN_DATA_TYPES[itr->column_type]);
if (itr->is_key)
{
sql.append(" ");
sql.append(PRIMARY_KEY);
}
if (!itr->is_null)
{
sql.append(" ");
sql.append(NOT_NULL);
}
if (itr != column_info.end() - 1)
sql.append(",");
}
sql.append(")");
const int ret = exec_sql(db, sql);
if (ret == -1)
LOG_ERROR << "Error when creating sqlite table " << table_name;
return ret;
}
int create_index(sqlite3 *db, std::string_view table_name, std::string_view column_names, const bool is_unique)
{
std::string index_name = std::string("idx_").append(table_name).append("_").append(column_names);
std::replace(index_name.begin(), index_name.end(), ',', '_');
std::string sql;
sql.append(is_unique ? CREATE_UNIQUE_INDEX : CREATE_INDEX)
.append(index_name)
.append(" ON ")
.append(table_name)
.append("(")
.append(column_names)
.append(")");
const int ret = exec_sql(db, sql);
if (ret == -1)
LOG_ERROR << "Error when creating sqlite index '" << index_name << "' in table " << table_name;
return ret;
}
/**
* Inserts mulitple rows to a table.
* @param db Pointer to the db.
* @param table_name Table name to be populated.
* @param column_names_string Comma seperated string of colums (eg: "col_1,col_2,...").
* @param value_strings Vector of comma seperated values (wrap in single quotes for TEXT type) (eg: ["r1val1,'r1val2',...", "r2val1,'r2val2',..."]).
* @returns returns 0 on success, or -1 on error.
*/
int insert_rows(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, const std::vector<std::string> &value_strings)
{
std::string sql;
sql.append(INSERT_INTO);
sql.append(table_name);
sql.append("(");
sql.append(column_names_string);
sql.append(") ");
sql.append(VALUES);
for (auto itr = value_strings.begin(); itr != value_strings.end(); ++itr)
{
sql.append("(");
sql.append(*itr);
sql.append(")");
if (itr != value_strings.end() - 1)
sql.append(",");
}
/* Execute SQL statement */
return exec_sql(db, sql);
}
/**
* Inserts a row to a table.
* @param db Pointer to the db.
* @param table_name Table name to be populated.
* @param column_names_string Comma seperated string of colums (eg: "col_1,col_2,...").
* @param value_string comma seperated values as per column order (wrap in single quotes for TEXT type) (eg: "r1val1,'r1val2',...").
* @returns returns 0 on success, or -1 on error.
*/
int insert_row(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, std::string_view value_string)
{
std::string sql;
// Reserving the space for the query before construction.
sql.reserve(sizeof(INSERT_INTO) + table_name.size() + column_names_string.size() + sizeof(VALUES) + value_string.size() + 5);
sql.append(INSERT_INTO);
sql.append(table_name);
sql.append("(");
sql.append(column_names_string);
sql.append(") ");
sql.append(VALUES);
sql.append("(");
sql.append(value_string);
sql.append(")");
/* Execute SQL statement */
return exec_sql(db, sql);
}
/**
* Checks whether table exist in the database.
* @param db Pointer to the db.
* @param table_name Table name to be checked.
* @returns returns true is exist, otherwise false.
*/
bool is_table_exists(sqlite3 *db, std::string_view table_name)
{
std::string sql;
// Reserving the space for the query before construction.
sql.reserve(sizeof(SELECT_ALL) + sizeof(SQLITE_MASTER) + sizeof(WHERE) + sizeof(AND) + table_name.size() + 19);
sql.append(SELECT_ALL);
sql.append(SQLITE_MASTER);
sql.append(WHERE);
sql.append("type='table'");
sql.append(AND);
sql.append("name='");
sql.append(table_name);
sql.append("'");
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql.data(), -1, &stmt, 0) == SQLITE_OK &&
stmt != NULL && sqlite3_step(stmt) == SQLITE_ROW)
{
// Finalize and distroys the statement.
sqlite3_finalize(stmt);
return true;
}
// Finalize and distroys the statement.
sqlite3_finalize(stmt);
return false;
}
/**
* Closes a connection to a given databse.
* @param db Pointer to the db.
* @returns returns 0 on success, or -1 on error.
*/
int close_db(sqlite3 **db)
{
if (*db == NULL)
return 0;
if (sqlite3_close(*db) != SQLITE_OK)
{
LOG_ERROR << "Can't close database: " << sqlite3_errmsg(*db);
return -1;
}
*db = NULL;
return 0;
}
}

65
src/sqlite.hpp Normal file
View File

@@ -0,0 +1,65 @@
#ifndef _SA_SQLITE_
#define _SA_SQLITE_
#include "pchheader.hpp"
namespace sqlite
{
/**
* Define an enum and a string array for the column data types.
* Any column data type that needs to be supportes should be added to both the 'COLUMN_DATA_TYPE' enum and the 'column_data_type' array in its respective order.
*/
enum COLUMN_DATA_TYPE
{
INT,
TEXT,
BLOB
};
/**
* Struct of table column information.
* {
* string name Name of the column.
* column_type Data type of the column.
* is_key Whether column is a key.
* is_null Whether column is nullable.
* }
*/
struct table_column_info
{
std::string name;
COLUMN_DATA_TYPE column_type;
bool is_key;
bool is_null;
table_column_info(std::string_view name, const COLUMN_DATA_TYPE &column_type, const bool is_key = false, const bool is_null = true)
: name(name), column_type(column_type), is_key(is_key), is_null(is_null)
{
}
};
int open_db(std::string_view db_name, sqlite3 **db, const bool writable = false);
int exec_sql(sqlite3 *db, std::string_view sql, int (*callback)(void *, int, char **, char **) = NULL, void *callback_first_arg = NULL);
int begin_transaction(sqlite3 *db);
int commit_transaction(sqlite3 *db);
int rollback_transaction(sqlite3 *db);
int create_table(sqlite3 *db, std::string_view table_name, const std::vector<table_column_info> &column_info);
int create_index(sqlite3 *db, std::string_view table_name, std::string_view column_names, const bool is_unique);
int insert_rows(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, const std::vector<std::string> &value_strings);
int insert_row(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, std::string_view value_string);
bool is_table_exists(sqlite3 *db, std::string_view table_name);
int close_db(sqlite3 **db);
}
#endif

100
src/util/util.cpp Normal file
View File

@@ -0,0 +1,100 @@
#include "../pchheader.hpp"
#include "util.hpp"
namespace util
{
/**
* Check whether given directory exists.
* @param path Directory path.
* @return Returns true if given directory exists otherwise false.
*/
bool is_dir_exists(std::string_view path)
{
struct stat st;
return (stat(path.data(), &st) == 0 && S_ISDIR(st.st_mode));
}
/**
* Check whether given file exists.
* @param path File path.
* @return Returns true if give file exists otherwise false.
*/
bool is_file_exists(std::string_view path)
{
struct stat st;
return (stat(path.data(), &st) == 0 && S_ISREG(st.st_mode));
}
/**
* Recursively creates directories and sub-directories if not exist.
* @param path Directory path.
* @return Returns 0 operations succeeded otherwise -1.
*/
int create_dir_tree_recursive(std::string_view path)
{
if (strcmp(path.data(), "/") == 0) // No need of checking if we are at root.
return 0;
// Check whether this dir exists or not.
struct stat st;
if (stat(path.data(), &st) != 0 || !S_ISDIR(st.st_mode))
{
// Check and create parent dir tree first.
char *path2 = strdup(path.data());
char *parent_dir_path = dirname(path2);
bool error_thrown = false;
if (create_dir_tree_recursive(parent_dir_path) == -1)
error_thrown = true;
free(path2);
// Create this dir.
if (!error_thrown && mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH) == -1)
{
std::cerr << errno << ": Error in recursive dir creation. " << path << std::endl;
error_thrown = true;
}
if (error_thrown)
return -1;
}
return 0;
}
/**
* Reads the entire file from given file discriptor.
* @param fd File descriptor to be read.
* @param buf String buffer to be populated.
* @param offset Begin offset of the file to read.
* @return Returns number of bytes read in a successful read and -1 on error.
*/
int read_from_fd(const int fd, std::string &buf, const off_t offset)
{
struct stat st;
if (fstat(fd, &st) == -1)
{
std::cerr << errno << ": Error in stat for reading entire file." << std::endl;
return -1;
}
buf.resize(st.st_size - offset);
return pread(fd, buf.data(), buf.size(), offset);
}
/**
* Provide a safe std::string overload for realpath.
* @param path Path.
* @returns Returns the realpath as string.
*/
const std::string realpath(std::string_view path)
{
std::array<char, PATH_MAX> buffer;
::realpath(path.data(), buffer.data());
buffer[PATH_MAX] = '\0';
return buffer.data();
}
} // namespace util

23
src/util/util.hpp Normal file
View File

@@ -0,0 +1,23 @@
#ifndef _HP_UTIL_UTIL_
#define _HP_UTIL_UTIL_
#include "../pchheader.hpp"
/**
* Contains helper functions and data structures used by multiple other subsystems.
*/
namespace util
{
bool is_dir_exists(std::string_view path);
bool is_file_exists(std::string_view path);
int create_dir_tree_recursive(std::string_view path);
int read_from_fd(const int fd, std::string &buf, const off_t offset = 0);
const std::string realpath(std::string_view path);
} // namespace util
#endif