151 lines
4.5 KiB
C++
151 lines
4.5 KiB
C++
#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;
|
|
}
|