Initial commit
This commit is contained in:
commit
f3bb6ac62b
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -0,0 +1,34 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
project(mf C CXX)
|
||||||
|
|
||||||
|
|
||||||
|
find_package(SQLite3 REQUIRED)
|
||||||
|
|
||||||
|
|
||||||
|
if (CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
if (NOT FLAGS)
|
||||||
|
list(APPEND FLAGS -fsanitize=undefined)
|
||||||
|
endif()
|
||||||
|
# https://sourceforge.net/p/valgrind/mailman/valgrind-users/thread/Ygze8PzaQAYWlKDj%40wildebeest.org/
|
||||||
|
list(APPEND FLAGS -gdwarf-4)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# https://t.me/NightShadowsHangout/670691
|
||||||
|
list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-struct -fPIC -fno-rtti -Wconversion -Wno-unused-parameter -Wimplicit-fallthrough)
|
||||||
|
|
||||||
|
# i have no idea why this hack wasn't needed before but it's needed if sanitizers are used
|
||||||
|
add_link_options(${FLAGS})
|
||||||
|
|
||||||
|
|
||||||
|
add_executable(${PROJECT_NAME} main.cpp sqlite_wrapper.cpp database.cpp utils.cpp blankie/cliparse.cpp
|
||||||
|
subcommand_create.cpp subcommand_search.cpp subcommand_prune.cpp
|
||||||
|
subcommand_info.cpp subcommand_delete.cpp subcommand_edit.cpp subcommand_get.cpp subcommand_set.cpp)
|
||||||
|
set_target_properties(${PROJECT_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
CXX_STANDARD 20
|
||||||
|
CXX_STANDARD_REQUIRED YES
|
||||||
|
CXX_EXTENSIONS NO
|
||||||
|
)
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE SQLite::SQLite3)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS})
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2023 blankie
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,123 @@
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "cliparse.h"
|
||||||
|
|
||||||
|
namespace blankie {
|
||||||
|
namespace cliparse {
|
||||||
|
|
||||||
|
void Parser::parse(int argc, char** argv) {
|
||||||
|
for (Flag& flag : this->flags) {
|
||||||
|
flag.used = false;
|
||||||
|
flag.argument = nullptr;
|
||||||
|
}
|
||||||
|
this->arguments.clear();
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
bool parse_flags = true;
|
||||||
|
|
||||||
|
if (offset == 0 && argc) {
|
||||||
|
this->program_name = argv[0];
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (argc > offset) {
|
||||||
|
if (argv[offset][0] != '-' || argv[offset][1] == '\0' || !parse_flags) {
|
||||||
|
this->arguments.push_back(argv[offset]);
|
||||||
|
} else if (argv[offset][1] == '-') {
|
||||||
|
this->_handle_long_flag(argc, argv, offset, parse_flags);
|
||||||
|
} else {
|
||||||
|
this->_handle_short_flags(argc, argv, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto unused_flag = std::find_if(this->flags.begin(), this->flags.end(), [](Flag& flag) {
|
||||||
|
return flag.required && !flag.used;
|
||||||
|
});
|
||||||
|
if (unused_flag != this->flags.end()) {
|
||||||
|
throw missing_flag(unused_flag->long_flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->arguments_min > this->arguments.size() || this->arguments_max < this->arguments.size()) {
|
||||||
|
throw invalid_argument_count(this->arguments_min, this->arguments_max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::_handle_long_flag(int argc, char** argv, int& offset, bool& parse_flags) {
|
||||||
|
if (argv[offset][2] == '\0') {
|
||||||
|
parse_flags = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* long_flag = &argv[offset][2];
|
||||||
|
const char* equal_offset = strchr(long_flag, '=');
|
||||||
|
size_t long_flag_length = equal_offset ? static_cast<size_t>(equal_offset - long_flag) : strlen(long_flag);
|
||||||
|
auto flag = std::find_if(this->flags.begin(), this->flags.end(), [=](Flag& flag) {
|
||||||
|
return strncmp(flag.long_flag, long_flag, long_flag_length) == 0 && flag.long_flag[long_flag_length] == '\0';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flag == this->flags.end()) {
|
||||||
|
throw unknown_flag(std::string(long_flag, long_flag_length));
|
||||||
|
}
|
||||||
|
flag->used = true;
|
||||||
|
|
||||||
|
if (flag->argument_required == ArgumentRequired::No) {
|
||||||
|
if (equal_offset) {
|
||||||
|
throw unnecessary_argument(flag->long_flag);
|
||||||
|
}
|
||||||
|
} else if (equal_offset) {
|
||||||
|
flag->argument = &equal_offset[1];
|
||||||
|
} else if (flag->argument_required == ArgumentRequired::Yes) {
|
||||||
|
if (offset < argc - 1) {
|
||||||
|
flag->argument = argv[++offset];
|
||||||
|
} else {
|
||||||
|
throw missing_argument(flag->long_flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::_handle_short_flags(int argc, char** argv, int& offset) {
|
||||||
|
const char* options = &argv[offset][1];
|
||||||
|
|
||||||
|
while (options[0] != '\0') {
|
||||||
|
Flag* flag = this->flag(options[0]);
|
||||||
|
if (!flag) {
|
||||||
|
throw unknown_flag(options[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
options++;
|
||||||
|
flag->used = true;
|
||||||
|
if (flag->argument_required == ArgumentRequired::No) {
|
||||||
|
continue;
|
||||||
|
} else if (options[0] != '\0') {
|
||||||
|
flag->argument = options[0] == '=' ? &options[1] : options;
|
||||||
|
break;
|
||||||
|
} else if (flag->argument_required == ArgumentRequired::Yes) {
|
||||||
|
if (offset < argc - 1) {
|
||||||
|
flag->argument = argv[++offset];
|
||||||
|
} else {
|
||||||
|
throw missing_argument(flag->long_flag);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Flag* Parser::flag(const char* long_flag) {
|
||||||
|
auto flag = std::find_if(this->flags.begin(), this->flags.end(), [=](Flag& flag) {
|
||||||
|
return strcmp(flag.long_flag, long_flag) == 0;
|
||||||
|
});
|
||||||
|
return flag != this->flags.end() ? &*flag : nullptr;
|
||||||
|
}
|
||||||
|
Flag* Parser::flag(char short_flag) {
|
||||||
|
auto flag = std::find_if(this->flags.begin(), this->flags.end(), [=](Flag& flag) {
|
||||||
|
return flag.short_flag == short_flag;
|
||||||
|
});
|
||||||
|
return flag != this->flags.end() ? &*flag : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cliparse
|
||||||
|
} // namespace blankie
|
|
@ -0,0 +1,142 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace blankie {
|
||||||
|
namespace cliparse {
|
||||||
|
|
||||||
|
enum ArgumentRequired {
|
||||||
|
Yes,
|
||||||
|
Maybe,
|
||||||
|
No,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Flag {
|
||||||
|
const char* long_flag; // must not be null
|
||||||
|
char short_flag; // may be '\0'
|
||||||
|
bool required;
|
||||||
|
ArgumentRequired argument_required;
|
||||||
|
|
||||||
|
bool used;
|
||||||
|
const char* argument; // may be null
|
||||||
|
|
||||||
|
Flag(const char* long_flag_, char short_flag_, bool required_ = false, ArgumentRequired argument_required_ = ArgumentRequired::No)
|
||||||
|
: long_flag(long_flag_), short_flag(short_flag_), required(required_), argument_required(argument_required_) {
|
||||||
|
assert(this->long_flag != nullptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Parser {
|
||||||
|
const char* program_name; // may be null
|
||||||
|
std::vector<Flag> flags;
|
||||||
|
size_t arguments_min;
|
||||||
|
size_t arguments_max;
|
||||||
|
std::vector<const char*> arguments;
|
||||||
|
|
||||||
|
void parse(int argc, char** argv);
|
||||||
|
|
||||||
|
Flag* flag(const char* long_flag);
|
||||||
|
Flag* flag(char short_flag);
|
||||||
|
|
||||||
|
Parser(const char* program_name_, std::vector<Flag> flags_, size_t arguments_min_ = 0, size_t arguments_max_ = SIZE_MAX)
|
||||||
|
: program_name(program_name_), flags(std::move(flags_)), arguments_min(arguments_min_), arguments_max(arguments_max_) {
|
||||||
|
assert(this->arguments_max >= this->arguments_min);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void _handle_long_flag(int argc, char** argv, int& offset, bool& parse_flags);
|
||||||
|
void _handle_short_flags(int argc, char** argv, int& offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
class exception : public std::exception {};
|
||||||
|
|
||||||
|
class missing_flag : public exception {
|
||||||
|
public:
|
||||||
|
missing_flag(const char* long_flag_) : long_flag(long_flag_) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
this->_msg = "flag --"s + this->long_flag + " required, but not used";
|
||||||
|
}
|
||||||
|
const char* what() const noexcept {
|
||||||
|
return this->_msg.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* long_flag;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
class missing_argument : public exception {
|
||||||
|
public:
|
||||||
|
missing_argument(const char* long_flag_) : long_flag(long_flag_) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
this->_msg = "flag --"s + this->long_flag + " requires an argument, but one is not passed";
|
||||||
|
}
|
||||||
|
const char* what() const noexcept {
|
||||||
|
return this->_msg.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* long_flag;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
class invalid_argument_count : public exception {
|
||||||
|
public:
|
||||||
|
invalid_argument_count(size_t arguments_min_, size_t arguments_max_) : arguments_min(arguments_min_), arguments_max(arguments_max_) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
this->_msg = "too little or too many arguments provided, there must be "s
|
||||||
|
+ std::to_string(this->arguments_min) + " to " + std::to_string(this->arguments_max) + " arguments";
|
||||||
|
}
|
||||||
|
const char* what() const noexcept {
|
||||||
|
return this->_msg.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t arguments_min;
|
||||||
|
size_t arguments_max;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
class unknown_flag : public exception {
|
||||||
|
public:
|
||||||
|
unknown_flag(std::string long_flag) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
this->_msg = "unknown flag --"s + std::move(long_flag);
|
||||||
|
}
|
||||||
|
unknown_flag(char short_flag) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
this->_msg = "unknown flag -"s + short_flag;
|
||||||
|
}
|
||||||
|
const char* what() const noexcept {
|
||||||
|
return this->_msg.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
class unnecessary_argument : public exception {
|
||||||
|
public:
|
||||||
|
unnecessary_argument(const char* long_flag_) : long_flag(long_flag_) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
this->_msg = "flag --"s + this->long_flag + " does not take in an argument, yet one is passed";
|
||||||
|
}
|
||||||
|
const char* what() const noexcept {
|
||||||
|
return this->_msg.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* long_flag;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cliparse
|
||||||
|
} // namespace blankie
|
|
@ -0,0 +1,17 @@
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
Database Database::find(bool readonly) {
|
||||||
|
std::filesystem::path directory = std::filesystem::current_path();
|
||||||
|
do {
|
||||||
|
std::filesystem::path path = directory / "mf.db";
|
||||||
|
if (std::filesystem::is_regular_file(path)) {
|
||||||
|
return Database(path, readonly, false);
|
||||||
|
}
|
||||||
|
directory = directory.parent_path();
|
||||||
|
} while (directory != directory.root_path());
|
||||||
|
|
||||||
|
fprintf(stderr, "Failed to find a database; have you created one and are in a directory with it?\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "sqlite_wrapper.h"
|
||||||
|
|
||||||
|
struct Database {
|
||||||
|
Database(std::filesystem::path path_, bool readonly, bool create) : path(std::move(path_)), db(this->path.c_str(), readonly, create) {}
|
||||||
|
|
||||||
|
static Database find(bool readonly);
|
||||||
|
static Database create_in_cwd() {
|
||||||
|
return Database(std::filesystem::current_path() / "mf.db", false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path path;
|
||||||
|
Sqlite3 db;
|
||||||
|
};
|
|
@ -0,0 +1,91 @@
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "database.h"
|
||||||
|
#include "subcommands.h"
|
||||||
|
#include "blankie/cliparse.h"
|
||||||
|
|
||||||
|
void sqlite_error_logger(void*, int error_code, const char* message) {
|
||||||
|
// For some reason, Sqlite3Statement::bind_text causes this
|
||||||
|
if (error_code == SQLITE_SCHEMA) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "sqlite: (%d) %s\n", error_code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void exit_handler() {
|
||||||
|
sqlite3_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void real_main(int argc, char** argv);
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
sqlite3_config(SQLITE_CONFIG_LOG, sqlite_error_logger, nullptr);
|
||||||
|
|
||||||
|
int error_code = sqlite3_initialize();
|
||||||
|
if (error_code != SQLITE_OK) {
|
||||||
|
throw Sqlite3Exception(error_code, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
atexit(exit_handler);
|
||||||
|
|
||||||
|
try {
|
||||||
|
real_main(argc, argv);
|
||||||
|
} catch (...) {
|
||||||
|
sqlite3_shutdown();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void real_main(int argc, char** argv) {
|
||||||
|
Parser parser("mf", {
|
||||||
|
{"directory", 'C', false, blankie::cliparse::ArgumentRequired::Yes},
|
||||||
|
}, 1, ~static_cast<size_t>(0));
|
||||||
|
|
||||||
|
try {
|
||||||
|
parser.parse(argc, argv);
|
||||||
|
} catch (const blankie::cliparse::invalid_argument_count& e) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
} catch (const blankie::cliparse::exception& e) {
|
||||||
|
fprintf(stderr, "failed to parse arguments: %s\n", e.what());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parser.flag('C')->used) {
|
||||||
|
if (chdir(parser.flag('C')->argument)) {
|
||||||
|
perror("failed to chdir");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto is = [&](const char* subcommand) {
|
||||||
|
return strcmp(parser.arguments[0], subcommand) == 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (is("create")) {
|
||||||
|
subcommand_create(parser);
|
||||||
|
} else if (is("search")) {
|
||||||
|
subcommand_search(parser);
|
||||||
|
} else if (is("prune")) {
|
||||||
|
subcommand_prune(parser);
|
||||||
|
} else if (is("info") || is("show")) {
|
||||||
|
subcommand_info(parser);
|
||||||
|
} else if (is("delete")) {
|
||||||
|
subcommand_delete(parser);
|
||||||
|
} else if (is("edit")) {
|
||||||
|
subcommand_edit(parser);
|
||||||
|
} else if (is("get")) {
|
||||||
|
subcommand_get(parser);
|
||||||
|
} else if (is("set")) {
|
||||||
|
subcommand_set(parser);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "sqlite_wrapper.h"
|
||||||
|
|
||||||
|
Sqlite3::Sqlite3(const char* filename, bool readonly, bool create) {
|
||||||
|
int flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE;
|
||||||
|
if (create) {
|
||||||
|
flags |= SQLITE_OPEN_CREATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int error_code = sqlite3_open_v2(filename, &this->_db, flags, nullptr);
|
||||||
|
if (error_code != SQLITE_OK) {
|
||||||
|
Sqlite3Exception e(error_code, this);
|
||||||
|
sqlite3_close_v2(this->_db);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_extended_result_codes(this->_db, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sqlite3Statement::Sqlite3Statement(Sqlite3& db, const char* sql) {
|
||||||
|
int error_code = sqlite3_prepare_v2(db.get(), sql, -1, &this->_stmt, nullptr);
|
||||||
|
if (error_code != SQLITE_OK) {
|
||||||
|
throw Sqlite3Exception(error_code, &db);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <exception>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
class Sqlite3Statement;
|
||||||
|
|
||||||
|
class Sqlite3 {
|
||||||
|
public:
|
||||||
|
Sqlite3(const Sqlite3&) = delete;
|
||||||
|
Sqlite3& operator=(const Sqlite3&) = delete;
|
||||||
|
|
||||||
|
Sqlite3(const char* filename, bool readonly, bool create);
|
||||||
|
~Sqlite3() {
|
||||||
|
sqlite3_close_v2(this->_db);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/6458689
|
||||||
|
template<typename F>
|
||||||
|
constexpr void exec(Sqlite3Statement& stmt, F callback, size_t iterations = ~static_cast<size_t>(0));
|
||||||
|
constexpr void exec(Sqlite3Statement& stmt) {
|
||||||
|
this->exec(stmt, []() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int64_t changes() const noexcept {
|
||||||
|
return sqlite3_changes64(this->_db);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr sqlite3* get() const noexcept {
|
||||||
|
return this->_db;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
sqlite3* _db;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Sqlite3Statement {
|
||||||
|
public:
|
||||||
|
Sqlite3Statement(const Sqlite3Statement&) = delete;
|
||||||
|
Sqlite3Statement& operator=(const Sqlite3Statement&) = delete;
|
||||||
|
|
||||||
|
Sqlite3Statement(Sqlite3& db, const char* sql);
|
||||||
|
~Sqlite3Statement() {
|
||||||
|
sqlite3_finalize(this->_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void reset() noexcept {
|
||||||
|
sqlite3_reset(this->_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void bind_text(int column, const char* text, int bytes, void (*lifetime)(void*));
|
||||||
|
constexpr void bind_text(int column, const char* text, void (*lifetime)(void*)) {
|
||||||
|
this->bind_text(column, text, -1, lifetime);
|
||||||
|
}
|
||||||
|
constexpr void bind_text(int column, const std::string& text, void (*lifetime)(void*)) {
|
||||||
|
this->bind_text(column, text.data(), static_cast<int>(text.size()), lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void bind_null(int column);
|
||||||
|
|
||||||
|
const char* column_text(int column) const noexcept {
|
||||||
|
return reinterpret_cast<const char*>(sqlite3_column_text(this->_stmt, column));
|
||||||
|
}
|
||||||
|
constexpr int64_t column_int64(int column) const noexcept {
|
||||||
|
return sqlite3_column_int64(this->_stmt, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend Sqlite3;
|
||||||
|
|
||||||
|
private:
|
||||||
|
sqlite3_stmt* _stmt;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Sqlite3Exception : public std::exception {
|
||||||
|
public:
|
||||||
|
Sqlite3Exception(int error_code, const Sqlite3* db) {
|
||||||
|
this->_msg = db && db->get()
|
||||||
|
? sqlite3_errmsg(db->get())
|
||||||
|
: sqlite3_errstr(error_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* what() const noexcept {
|
||||||
|
return this->_msg.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
template<typename F>
|
||||||
|
constexpr void Sqlite3::exec(Sqlite3Statement& stmt, F callback, size_t iterations) {
|
||||||
|
while (iterations--) {
|
||||||
|
int error_code = sqlite3_step(stmt._stmt);
|
||||||
|
if (error_code == SQLITE_DONE) {
|
||||||
|
break;
|
||||||
|
} else if (error_code == SQLITE_ROW) {
|
||||||
|
callback();
|
||||||
|
} else if (error_code != SQLITE_OK) {
|
||||||
|
throw Sqlite3Exception(error_code, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void Sqlite3Statement::bind_text(int column, const char* text, int bytes, void (*lifetime)(void*)) {
|
||||||
|
int error_code = sqlite3_bind_text(this->_stmt, column, text, bytes, lifetime);
|
||||||
|
if (error_code != SQLITE_OK) {
|
||||||
|
throw Sqlite3Exception(error_code, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void Sqlite3Statement::bind_null(int column) {
|
||||||
|
int error_code = sqlite3_bind_null(this->_stmt, column);
|
||||||
|
if (error_code != SQLITE_OK) {
|
||||||
|
throw Sqlite3Exception(error_code, nullptr);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
void subcommand_create(const Parser& parser) {
|
||||||
|
if (parser.arguments.size() != 1) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Database db = Database::create_in_cwd();
|
||||||
|
|
||||||
|
Sqlite3Statement stmt(db.db, R"EOF(CREATE VIRTUAL TABLE IF NOT EXISTS memes USING fts4(
|
||||||
|
path TEXT PRIMARY KEY NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
miscinfo TEXT NOT NULL,
|
||||||
|
matchinfo=fts3
|
||||||
|
))EOF");
|
||||||
|
db.db.exec(stmt);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
#include "utils.h"
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
void subcommand_delete(const Parser& parser) {
|
||||||
|
if (parser.arguments.size() < 2) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = 0;
|
||||||
|
Database db = Database::find(false);
|
||||||
|
std::vector<std::filesystem::path> paths = get_paths_relative_to_database(parser, db, &rc, false);
|
||||||
|
Sqlite3Statement stmt(db.db, "DELETE FROM memes WHERE path = ?");
|
||||||
|
|
||||||
|
for (const std::filesystem::path& i : paths) {
|
||||||
|
stmt.reset();
|
||||||
|
stmt.bind_text(1, i.native(), SQLITE_STATIC);
|
||||||
|
db.db.exec(stmt);
|
||||||
|
|
||||||
|
if (db.db.changes() <= 0) {
|
||||||
|
fprintf(stderr, "failed to delete %s: no such file\n", i.c_str());
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(rc);
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
#include <regex>
|
||||||
|
#include <system_error>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
#include "utils.h"
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
using namespace std::string_literals;
|
||||||
|
|
||||||
|
const std::regex contents_regex("# Path \\(immutable\\):\n\\s*[^\0]*?\\s*\0\n# Source:\n\\s*([^\0]*?)\\s*\0\n# Description:\n\\s*([^\0]*?)\\s*\0\n# Miscellaneous information:\n\\s*([^\0]*?)\\s*"s);
|
||||||
|
static inline const char* editor();
|
||||||
|
static inline std::string read_file(int fd);
|
||||||
|
|
||||||
|
struct TemporaryFile {
|
||||||
|
int fd;
|
||||||
|
char name[14] = "/tmp/mfXXXXXX";
|
||||||
|
bool unlinked = false;
|
||||||
|
|
||||||
|
TemporaryFile() {
|
||||||
|
this->fd = mkstemp(this->name);
|
||||||
|
if (this->fd == -1) {
|
||||||
|
throw std::system_error(errno, std::generic_category(), "mkstemp()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~TemporaryFile() {
|
||||||
|
if (close(this->fd)) {
|
||||||
|
perror("close(temp_fd)");
|
||||||
|
}
|
||||||
|
this->unlink();
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlink() {
|
||||||
|
if (!this->unlinked && ::unlink(this->name)) {
|
||||||
|
perror("unlink(temp_name)");
|
||||||
|
}
|
||||||
|
this->unlinked = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void subcommand_edit(const Parser& parser) {
|
||||||
|
// Sanity check the arguments passed
|
||||||
|
if (parser.arguments.size() != 2) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the database and find the meme's path, relative to the database
|
||||||
|
Database db = Database::find(false);
|
||||||
|
std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db);
|
||||||
|
|
||||||
|
// Select the meme, if any
|
||||||
|
bool meme_found = false;
|
||||||
|
Sqlite3Statement select_stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?");
|
||||||
|
select_stmt.bind_text(1, path.native(), SQLITE_STATIC);
|
||||||
|
db.db.exec(select_stmt, [&]() {
|
||||||
|
meme_found = true;
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// Create a temporary file and write the meme's metadata to it
|
||||||
|
TemporaryFile tempfile;
|
||||||
|
output_meme(
|
||||||
|
path.c_str(),
|
||||||
|
meme_found ? select_stmt.column_text(0) : "",
|
||||||
|
meme_found ? select_stmt.column_text(1) : "",
|
||||||
|
meme_found ? select_stmt.column_text(2) : "",
|
||||||
|
tempfile.fd,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
select_stmt.reset();
|
||||||
|
select_stmt.bind_null(1);
|
||||||
|
if (lseek(tempfile.fd, 0, SEEK_SET)) {
|
||||||
|
perror("lseek(temp_fd)");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn $VISUAL/$EDITOR/vi
|
||||||
|
pid_t editor_pid = fork();
|
||||||
|
if (editor_pid == -1) {
|
||||||
|
perror("fork()");
|
||||||
|
exit(1);
|
||||||
|
} else if (editor_pid == 0) {
|
||||||
|
execlp(editor(), editor(), tempfile.name, nullptr);
|
||||||
|
perror("execlp()");
|
||||||
|
_exit(1);
|
||||||
|
} else if (waitpid(editor_pid, nullptr, 0) != editor_pid) {
|
||||||
|
perror("waitpid()");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the temporary file and read its contents
|
||||||
|
tempfile.unlink();
|
||||||
|
std::string contents = read_file(tempfile.fd);
|
||||||
|
std::smatch sm;
|
||||||
|
if (!std::regex_match(contents, sm, contents_regex)) {
|
||||||
|
fprintf(stderr, "failed to parse file contents\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write new meme metadata to the database
|
||||||
|
std::string source = sm.str(1);
|
||||||
|
std::string description = sm.str(2);
|
||||||
|
std::string miscinfo = sm.str(3);
|
||||||
|
if (meme_found) {
|
||||||
|
Sqlite3Statement update_stmt(db.db, "UPDATE memes SET source = ?, description = ?, miscinfo = ? WHERE path = ?");
|
||||||
|
update_stmt.bind_text(1, source, SQLITE_STATIC);
|
||||||
|
update_stmt.bind_text(2, description, SQLITE_STATIC);
|
||||||
|
update_stmt.bind_text(3, miscinfo, SQLITE_STATIC);
|
||||||
|
update_stmt.bind_text(4, path.native(), SQLITE_STATIC);
|
||||||
|
db.db.exec(update_stmt);
|
||||||
|
} else {
|
||||||
|
Sqlite3Statement insert_stmt(db.db, "INSERT INTO memes (path, source, description, miscinfo) VALUES (?, ?, ?, ?)");
|
||||||
|
insert_stmt.bind_text(1, path.native(), SQLITE_STATIC);
|
||||||
|
insert_stmt.bind_text(2, source, SQLITE_STATIC);
|
||||||
|
insert_stmt.bind_text(3, description, SQLITE_STATIC);
|
||||||
|
insert_stmt.bind_text(4, miscinfo, SQLITE_STATIC);
|
||||||
|
db.db.exec(insert_stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static inline const char* editor() {
|
||||||
|
if (getenv("VISUAL")) {
|
||||||
|
return getenv("VISUAL");
|
||||||
|
}
|
||||||
|
if (getenv("EDITOR")) {
|
||||||
|
return getenv("EDITOR");
|
||||||
|
}
|
||||||
|
return "vi";
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::string read_file(int fd) {
|
||||||
|
std::string res;
|
||||||
|
char buf[4096];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ssize_t read_size = read(fd, buf, 4096);
|
||||||
|
if (read_size == -1) {
|
||||||
|
throw std::system_error(errno, std::generic_category(), "read(temp_fd)");
|
||||||
|
} else if (read_size == 0) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
res.insert(res.end(), buf, &buf[read_size]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
#include "utils.h"
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
void subcommand_get(const Parser& parser) {
|
||||||
|
// Sanity check the arguments passed
|
||||||
|
if (parser.arguments.size() != 2) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the database and find the meme's path, relative to the database
|
||||||
|
Database db = Database::find(true);
|
||||||
|
std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db);
|
||||||
|
|
||||||
|
// Select the meme, if any
|
||||||
|
bool meme_found = false;
|
||||||
|
Sqlite3Statement select_stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?");
|
||||||
|
select_stmt.bind_text(1, path.native(), SQLITE_STATIC);
|
||||||
|
db.db.exec(select_stmt, [&]() {
|
||||||
|
meme_found = true;
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// Output the meme's metadata to stdout
|
||||||
|
printf("%s%c%s%c%s%c%s",
|
||||||
|
path.c_str(), 0,
|
||||||
|
meme_found ? select_stmt.column_text(0) : "", 0,
|
||||||
|
meme_found ? select_stmt.column_text(1) : "", 0,
|
||||||
|
meme_found ? select_stmt.column_text(2) : "");
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "utils.h"
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
void subcommand_info(const Parser& parser) {
|
||||||
|
if (parser.arguments.size() < 2) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = 0;
|
||||||
|
bool first_item = true;
|
||||||
|
Database db = Database::find(true);
|
||||||
|
std::vector<std::filesystem::path> paths = get_paths_relative_to_database(parser, db, &rc);
|
||||||
|
Sqlite3Statement stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?");
|
||||||
|
|
||||||
|
for (const std::filesystem::path& i : paths) {
|
||||||
|
bool item_found = false;
|
||||||
|
|
||||||
|
stmt.reset();
|
||||||
|
stmt.bind_text(1, i.native(), SQLITE_STATIC);
|
||||||
|
db.db.exec(stmt, [&]() {
|
||||||
|
if (!first_item) {
|
||||||
|
write(1, "================================================================================\n", 81);
|
||||||
|
}
|
||||||
|
first_item = false;
|
||||||
|
item_found = true;
|
||||||
|
|
||||||
|
output_meme(i.c_str(), stmt.column_text(0), stmt.column_text(1), stmt.column_text(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item_found) {
|
||||||
|
fprintf(stderr, "failed to find %s\n", i.c_str());
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(first_item ? 1 : rc);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
static inline void is_file(sqlite3_context* context, int argc, sqlite3_value** argv);
|
||||||
|
|
||||||
|
void subcommand_prune(const Parser& parser) {
|
||||||
|
if (parser.arguments.size() != 1) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Database db = Database::find(false);
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/4929486
|
||||||
|
int error_code = sqlite3_create_function(db.db.get(), "is_file", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_DIRECTONLY, &db.path, is_file, nullptr, nullptr);
|
||||||
|
if (error_code != SQLITE_OK) {
|
||||||
|
throw Sqlite3Exception(error_code, &db.db);
|
||||||
|
}
|
||||||
|
Sqlite3Statement delete_stmt(db.db, "DELETE FROM memes WHERE NOT is_file(path)");
|
||||||
|
db.db.exec(delete_stmt);
|
||||||
|
sqlite3_create_function(db.db.get(), "is_file", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_DIRECTONLY, &db.path, nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
Sqlite3Statement vacuum_stmt(db.db, "VACUUM");
|
||||||
|
db.db.exec(vacuum_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void is_file(sqlite3_context* context, int argc, sqlite3_value** argv) {
|
||||||
|
if (argc != 1) {
|
||||||
|
sqlite3_result_error(context, "is_file() expects one argument", -1);
|
||||||
|
sqlite3_result_error_code(context, SQLITE_MISUSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path* db_path = static_cast<const std::filesystem::path*>(sqlite3_user_data(context));
|
||||||
|
std::filesystem::path file_path = db_path->parent_path() / reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
|
||||||
|
sqlite3_result_int(context, std::filesystem::is_regular_file(file_path));
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "utils.h"
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
void subcommand_search(const Parser& parser) {
|
||||||
|
if (parser.arguments.size() != 2) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Database db = Database::find(true);
|
||||||
|
Sqlite3Statement stmt(db.db, "SELECT path, source, description, miscinfo FROM memes WHERE memes MATCH ?");
|
||||||
|
bool first_item = true;
|
||||||
|
|
||||||
|
stmt.bind_text(1, parser.arguments[1], SQLITE_STATIC);
|
||||||
|
try {
|
||||||
|
db.db.exec(stmt, [&]() {
|
||||||
|
if (!first_item) {
|
||||||
|
write(1, "================================================================================\n", 81);
|
||||||
|
}
|
||||||
|
first_item = false;
|
||||||
|
|
||||||
|
output_meme(stmt.column_text(0), stmt.column_text(1), stmt.column_text(2), stmt.column_text(3));
|
||||||
|
});
|
||||||
|
} catch (const Sqlite3Exception& e) {
|
||||||
|
// it'll be printed to the error log, so do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first_item) {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
#include "utils.h"
|
||||||
|
#include "database.h"
|
||||||
|
|
||||||
|
#include "subcommands.h"
|
||||||
|
|
||||||
|
void subcommand_set(const Parser& parser) {
|
||||||
|
// Sanity check the arguments passed
|
||||||
|
if (parser.arguments.size() != 5) {
|
||||||
|
fprintf(stderr, HELP_TEXT, parser.program_name);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the database and find the meme's path, relative to the database
|
||||||
|
Database db = Database::find(false);
|
||||||
|
std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db);
|
||||||
|
|
||||||
|
// Update an existing meme, if any
|
||||||
|
Sqlite3Statement update_stmt(db.db, "UPDATE memes SET source = ?, description = ?, miscinfo = ? WHERE path = ?");
|
||||||
|
update_stmt.bind_text(1, parser.arguments[2], SQLITE_STATIC);
|
||||||
|
update_stmt.bind_text(2, parser.arguments[3], SQLITE_STATIC);
|
||||||
|
update_stmt.bind_text(3, parser.arguments[4], SQLITE_STATIC);
|
||||||
|
update_stmt.bind_text(4, path.native(), SQLITE_STATIC);
|
||||||
|
db.db.exec(update_stmt);
|
||||||
|
|
||||||
|
// If there is not one, then create one
|
||||||
|
if (db.db.changes() <= 0) {
|
||||||
|
Sqlite3Statement insert_stmt(db.db, "INSERT INTO memes (path, source, description, miscinfo) VALUES (?, ?, ?, ?)");
|
||||||
|
insert_stmt.bind_text(1, path.native(), SQLITE_STATIC);
|
||||||
|
insert_stmt.bind_text(2, parser.arguments[2], SQLITE_STATIC);
|
||||||
|
insert_stmt.bind_text(3, parser.arguments[3], SQLITE_STATIC);
|
||||||
|
insert_stmt.bind_text(4, parser.arguments[4], SQLITE_STATIC);
|
||||||
|
db.db.exec(insert_stmt);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "blankie/cliparse.h"
|
||||||
|
|
||||||
|
using Parser = blankie::cliparse::Parser;
|
||||||
|
|
||||||
|
#define HELP_TEXT &R"EOF(
|
||||||
|
Usage: %1$s [-C=<directory>] create
|
||||||
|
or %1$s [-C=<directory>] search <search query>
|
||||||
|
or %1$s [-C=<directory>] prune
|
||||||
|
|
||||||
|
or %1$s [-C=<directory>] {info|show} <file>...
|
||||||
|
or %1$s [-C=<directory>] delete <file>...
|
||||||
|
or %1$s [-C=<directory>] edit <file>
|
||||||
|
or %1$s [-C=<directory>] get <file>
|
||||||
|
or %1$s [-C=<directory>] set <file> <source> <description> <misc info>
|
||||||
|
|
||||||
|
-C=<directory> changes the current working directory to <directory> before a
|
||||||
|
subcommand is executed. This causes the database to be looked up in
|
||||||
|
<directory>, all file paths are relative to <directory> and database creation
|
||||||
|
to be inside of <directory>
|
||||||
|
|
||||||
|
create creates a database in the current directory (or <directory>)
|
||||||
|
|
||||||
|
prune removes all metadata entries that correspond to non-existent files, then
|
||||||
|
vaccums the database
|
||||||
|
|
||||||
|
delete only deletes the metadata for the files specified, not the actual files
|
||||||
|
themselves
|
||||||
|
|
||||||
|
get outputs the file's path, source, description, and miscellaneous information
|
||||||
|
delimited by a null-byte
|
||||||
|
)EOF"[1]
|
||||||
|
|
||||||
|
void subcommand_create(const Parser& parser);
|
||||||
|
void subcommand_search(const Parser& parser);
|
||||||
|
void subcommand_prune(const Parser& parser);
|
||||||
|
|
||||||
|
void subcommand_info(const Parser& parser);
|
||||||
|
void subcommand_delete(const Parser& parser);
|
||||||
|
void subcommand_edit(const Parser& parser);
|
||||||
|
void subcommand_get(const Parser& parser);
|
||||||
|
void subcommand_set(const Parser& parser);
|
|
@ -0,0 +1,103 @@
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "database.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
static std::filesystem::path normalize(std::filesystem::path path);
|
||||||
|
static std::optional<std::filesystem::path> relative_path_from(const std::filesystem::path& from, const std::filesystem::path& path);
|
||||||
|
|
||||||
|
void output_meme(const char* path, const char* source, const char* description, const char* miscinfo, int fd, bool edit) {
|
||||||
|
auto output = [=](const char* header, const char* contents, bool end = false) {
|
||||||
|
write(fd, "# ", 2);
|
||||||
|
write(fd, header, strlen(header));
|
||||||
|
write(fd, ":\n", 2);
|
||||||
|
write(fd, contents, strlen(contents));
|
||||||
|
write(fd, "\n", 1);
|
||||||
|
if (!end) {
|
||||||
|
write(fd, edit ? "\n\0\n" : "\n", edit ? 3 : 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
output(edit ? "Path (immutable)" : "Path", path);
|
||||||
|
output("Source", source);
|
||||||
|
output("Description", description);
|
||||||
|
output("Miscellaneous information", miscinfo, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> get_paths_relative_to_database(const blankie::cliparse::Parser& parser, const Database& db, int* rc, bool needs_exist) {
|
||||||
|
std::vector<std::filesystem::path> paths;
|
||||||
|
|
||||||
|
paths.reserve(parser.arguments.size() - 1);
|
||||||
|
for (size_t i = 1; i < parser.arguments.size(); i++) {
|
||||||
|
std::filesystem::path normalized_path = normalize(parser.arguments[i]);
|
||||||
|
std::optional<std::filesystem::path> relative_path = relative_path_from(db.path.parent_path(), normalized_path);
|
||||||
|
if (!relative_path) {
|
||||||
|
fprintf(stderr, "%s is not in %s\n", normalized_path.c_str(), db.path.parent_path().c_str());
|
||||||
|
*rc = 1;
|
||||||
|
} else if (needs_exist && !std::filesystem::is_regular_file(normalized_path)) {
|
||||||
|
fprintf(stderr, "%s is not a file\n", normalized_path.c_str());
|
||||||
|
*rc = 1;
|
||||||
|
} else {
|
||||||
|
paths.push_back(*relative_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path get_path_relative_to_database(const char* path, const Database& db) {
|
||||||
|
std::filesystem::path normalized_path = normalize(path);
|
||||||
|
std::optional<std::filesystem::path> relative_path = relative_path_from(db.path.parent_path(), normalized_path);
|
||||||
|
|
||||||
|
if (!relative_path) {
|
||||||
|
fprintf(stderr, "%s is not in %s\n", normalized_path.c_str(), db.path.parent_path().c_str());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (!std::filesystem::is_regular_file(normalized_path)) {
|
||||||
|
fprintf(stderr, "%s is not a file\n", normalized_path.c_str());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
return *relative_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
std::filesystem::path normalize(std::filesystem::path path) {
|
||||||
|
if (path.is_relative()) {
|
||||||
|
path = std::filesystem::current_path() / path;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path normalized = path.root_path();
|
||||||
|
for (auto it = path.begin(); it != path.end(); it++) {
|
||||||
|
if (*it == ".") {
|
||||||
|
// do nothing
|
||||||
|
} else if (*it == "..") {
|
||||||
|
normalized = normalized.parent_path();
|
||||||
|
} else {
|
||||||
|
normalized /= *it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::filesystem::path> relative_path_from(const std::filesystem::path& from, const std::filesystem::path& path) {
|
||||||
|
auto from_it = from.begin();
|
||||||
|
auto path_it = path.begin();
|
||||||
|
|
||||||
|
for (; from_it != from.end() && path_it != path.end(); from_it++, path_it++) {
|
||||||
|
if (*from_it != *path_it) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (from_it != from.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path name;
|
||||||
|
for (; path_it != path.end(); path_it++) {
|
||||||
|
name /= *path_it;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
struct Database; // forward declaration from database.h
|
||||||
|
#include "blankie/cliparse.h"
|
||||||
|
|
||||||
|
void output_meme(const char* path, const char* source, const char* description, const char* miscinfo, int fd = 1, bool edit = false);
|
||||||
|
std::vector<std::filesystem::path> get_paths_relative_to_database(const blankie::cliparse::Parser& parser, const Database& db, int* rc, bool needs_exist = true);
|
||||||
|
std::filesystem::path get_path_relative_to_database(const char* path, const Database& db);
|
Loading…
Reference in New Issue