thalassa/cms/main_upd.cpp

824 lines
25 KiB
C++
Raw Permalink Normal View History

2026-03-19 01:23:52 +00:00
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <scriptpp/scrvar.hpp>
#include <scriptpp/scrvect.hpp>
#include <scriptpp/scrmsg.hpp>
#include <scriptpp/cmd.hpp>
#include "database.hpp"
#include "main_all.hpp"
#include "main_upd.hpp"
#define FILESPEC_TEXT\
"The <filespec> may be either a file path or a Thalassa object\n"\
"specification. In case the argument contains at least one ``/'',\n"\
"it is taken as a path to an existing file; the Thalassa database\n"\
"is not even loaded in this case. If the argument contains at\n"\
"least one ``='', it is considered to be a Thalassa object\n"\
"specification, which must either specify a pageset page ID,\n"\
"or a comment ID for either a list item page, or a pageset page.\n"\
"In case there's neither a ``/'' nor a ``='' in the argument\n"\
"(which is not recommended, it is better to use ./name for files\n"\
"and setID=pgID for pageset pages), Thalassa will first try a\n"\
"file in the current working directory, and if there's no such\n"\
"file or it is not a regular file, will try to load the database\n"\
"and use the given name as a pageset page ID (which only works if\n"\
"you've got exactly one pageset).\n"\
"\n"\
"Thalassa object specifications have the form\n"\
"\n"\
" [<type>=[<realmID>=]]<pageID>[=<commentID>]\n"\
"\n"\
"where <type> is one of ``list'', ``set'' or ``pageset'' (the\n"\
"latter two are equal), but is in most cases simply omitted;\n"\
"<realmID> is an ID of either a list or a pageset;\n"\
"<pageID> is either a pageset page ID, or an item ID within\n"\
"a list with item pages;\n"\
"<commentID> is, well, the comment ID.\n"\
"Please note that in case the <commentID> is omitted, then the\n"\
"type must not be ``list'' and the realmID must not be a list ID\n"\
"because list item pages don't have source files, and this command\n"\
"is only intended to work with headed text files. The <realmID> may\n"\
"only be omitted in case you only have one realm: either exactly one\n"\
"pageset and no lists, or no pagesets and exactly one list. If you\n"\
"specify a pair like XXX=YYY, it is considered to be pageID=commentID\n"\
"if and only if you have exactly one realm, otherwise it is\n"\
"interpreted as setID=pageID.\n"
void help_update(FILE *stream)
{
fprintf(stream,
"The ``update'' command updates a ``headed text'' (page/comment) file."
"\n"
"Usage:\n"
"\n"
" thalassa [...] update <filespec> [<options>]\n"
"\n"
FILESPEC_TEXT
"\n"
"Valid options are:\n"
"\n"
" -n dry run: show what would be done but don't do\n"
" +T <tag> add the tag\n"
" -T <tag> remove the tag\n"
" +F <flag> add the flag\n"
" -F <flag> remove the flag\n"
" +L <label> add the label\n"
" -L <label> remove the label\n"
" -D <unixtime> use the <unixtime> instead of the current time\n"
" -d force updating the ``unixtime:'' field:\n"
" update even if it is already there\n"
" -t update the ``teaser_len:'' field: if there's no\n"
" ``descr:'' field, search the body for the\n"
" ``<!--break-->'' string and put its position\n"
" into the field, or put the length body if the\n"
" string isn't found; if the ``descr:'' field\n"
" is there, just remove the ``teaser_len:''\n"
" -i suppress updating of the ``id:'' field\n"
" by default, it is set accordingly to the\n"
" object specification or file path\n"
" -b <suffix> set the backup file suffix (default: ``~'')\n"
" -B don't create backup files\n"
" -v verbose messages\n"
"\n"
);
}
enum file_types { ft_guess, ft_page, ft_comment };
struct file_choice {
ScriptVariableInv filename, id;
int file_type;
file_choice() : file_type(ft_guess) {}
};
enum task_categories { ct_label, ct_flag, ct_tag };
struct Task {
int category;
bool add; // false to remove
ScriptVariable name;
Task *next;
Task(int c, bool a, const ScriptVariable &n)
: category(c), add(a), name(n), next(0) {}
};
struct cmdline_update : public file_choice {
Task *first, *last;
bool dry_run;
ScriptVariable baksuffix;
bool backup;
long long date;
bool force_date_update;
bool teaser_update;
bool dont_touch_id;
bool verbose;
cmdline_update()
: first(0), last(0), dry_run(false),
baksuffix("~"), backup(true), date(0), force_date_update(false),
teaser_update(false), dont_touch_id(false), verbose(false)
{}
~cmdline_update();
void AddTask(int c, bool a, const ScriptVariable &n);
};
cmdline_update::~cmdline_update()
{
while(first) {
Task *tmp = first;
first = first->next;
delete tmp;
}
}
void cmdline_update::AddTask(int c, bool a, const ScriptVariable &n)
{
Task *p = new Task(c, a, n);
if(last)
last->next = p;
else
first = p;
last = p;
}
const char *headername(int c)
{
switch(c) {
case ct_label: return "label";
case ct_flag: return "flags";
case ct_tag: return "tags";
}
// we shouln't have reached this point!
fprintf(stderr, "BUG: unknown header code\n");
exit(5);
}
static int action_by_char(char c)
{
switch(c) {
case 'L': return ct_label;
case 'F': return ct_flag;
case 'T': return ct_tag;
default: return -1; /* shouldn't happen */
}
}
// the file name (or object spec) is already not there
static bool
parse_update_cmdl(int argc, const char *const *argv, cmdline_update &options)
{
const char *const *p = argv;
while(*p) {
if(((*p)[0] == '+' || (*p)[0] == '-') &&
((*p)[1] == 'T' || (*p)[1] == 'F' || (*p)[1] == 'L') &&
((*p)[2] == 0))
{
if(!p[1]) {
fprintf(stderr, "action requires a parameter\n");
return false;
}
options.AddTask(action_by_char((*p)[1]), (*p)[0] == '+', p[1]);
p += 2;
continue;
}
if((*p)[0] == '-') {
if((*p)[1] == 0 || (*p)[2] != 0) {
fprintf(stderr, "option %s unknown\n", *p);
return false;
}
switch((*p)[1]) {
case 'b':
if(!p[1]) {
fprintf(stderr, "-b requires a parameter\n");
return false;
}
options.baksuffix = p[1];
p += 2;
break;
case 'D':
if(!p[1]) {
fprintf(stderr, "-D requires a parameter\n");
return false;
}
{ // to localize vars
long long d;
bool ok = ScriptVariable(p[1]).GetLongLong(d, 10);
if(!ok || d < 1) {
fprintf(stderr, "invalid date ``%s''\n", p[1]);
return false;
}
options.date = d;
}
p += 2;
break;
case 'B':
options.backup = false;
p++;
break;
case 'n':
options.dry_run = true;
p++;
break;
case 'd':
options.force_date_update = true;
p++;
break;
case 't':
options.teaser_update = true;
p++;
break;
case 'i':
options.dont_touch_id = true;
p++;
break;
case 'v':
options.verbose = true;
p++;
break;
default:
fprintf(stderr, "option %s unknown\n", *p);
return false;
}
continue;
}
if((*p)[0] == '+') {
fprintf(stderr, "action %s unknown\n", *p);
return false;
}
fprintf(stderr, "extra argument(s) starting with %s\n", *p);
return false;
}
return true;
}
//////////////////////////////////////////////////////////////////////
static bool svec_has_elem(const ScriptVector &v, const ScriptVariable &el)
{
int i;
for(i = 0; i < v.Length(); i++) {
if(v[i] == el)
return true;
}
return false;
}
static bool get_fname_setitem(Database &dbs,
const ScriptVariable &set_id, const ScriptVariable &page_id,
file_choice &choice)
{
const char *sid = set_id.c_str();
bool ok;
PageSetData sd;
ok = dbs.GetSetData(set_id, sd);
if(!ok) {
fprintf(stderr, "%s: no such pageset or can't load\n", sid);
return false;
}
ScriptVariable srcd, srcf;
bool separdir;
ok = dbs.GetSetItemSource(set_id, page_id, srcd, srcf, separdir);
if(!ok) {
fprintf(stderr, "%s=%s: can't find pageset item source file\n",
sid, page_id.c_str());
return false;
}
choice.filename = srcf;
choice.file_type = ft_page;
choice.id = page_id;
return true;
}
static bool get_fname_listitem(Database &dbs,
const ScriptVariable &list_id, const ScriptVariable &page_id,
file_choice &choice)
{
ListData ld;
const char *lid = list_id.c_str();
bool ok = dbs.GetListData(list_id, ld);
if(!ok) {
fprintf(stderr, "%s: no such list or can't load\n", lid);
return false;
}
if(ld.srctype != listsrc_set) {
fprintf(stderr,
"%s: list is not set-based, can't update its items\n", lid);
return false;
}
return get_fname_setitem(dbs, ld.srcname, page_id, choice);
}
static bool get_fname_li(Database &dbs, const ScriptVariable &comconf,
ListItemData &li, ScriptVariable cmt_id,
file_choice &choice)
{
dbs.SetMacroData(0, &li);
ScriptVariable path = dbs.GetCommentsPath(comconf);
dbs.ForgetMacroData();
if(path.IsInvalid())
return false;
path.Trim();
while(cmt_id.Length() < 4)
cmt_id.Range(0, 0).Replace("0");
if(path.Range(-1, 1)[0] != '/')
path += "/";
choice.filename = path + cmt_id;
choice.file_type = ft_comment;
choice.id = cmt_id;
return true;
}
static bool get_fname_setcmt(Database &dbs, const ScriptVariable &set_id,
const ScriptVariable &page_id, const ScriptVariable &cmt_id,
file_choice &choice)
{
const char *sid = set_id.c_str();
bool ok;
PageSetData sd;
ok = dbs.GetSetData(set_id, sd);
if(!ok) {
fprintf(stderr, "%s: no such pageset or can't load\n", sid);
return false;
}
if(sd.comments_conf.IsInvalid() || sd.comments_conf == "") {
fprintf(stderr, "%s: comments not configured for the set\n", sid);
return false;
}
ListItemData li;
ok = dbs.GetSetItemDataById(set_id, page_id, &li);
if(!ok) {
fprintf(stderr, "%s: no such item in the pageset ``%s''\n",
page_id.c_str(), sid);
return false;
}
return get_fname_li(dbs, sd.comments_conf, li, cmt_id, choice);
}
static bool get_fname_listcmt(Database &dbs, const ScriptVariable&list_id,
const ScriptVariable &page_id, const ScriptVariable &cmt_id,
file_choice &choice)
{
const char *lid = list_id.c_str();
bool ok;
ListData ld;
ok = dbs.GetListData(list_id, ld);
if(!ok) {
fprintf(stderr, "%s: no such list or can't load\n", lid);
return false;
}
if(ld.srctype == listsrc_set)
return get_fname_setcmt(dbs, ld.srcname, page_id, cmt_id, choice);
if(!ld.pages) {
fprintf(stderr, "%s: list has no item pages\n", lid);
return false;
}
if(ld.comments_conf.IsInvalid() || ld.comments_conf == "") {
fprintf(stderr, "%s: comments not configured for the list\n", lid);
return false;
}
int idx = -1;
int max = ld.items.Length();
int i;
for(i = 0; i < max; i++) {
if(ld.items[i] == page_id) {
idx = i;
break;
}
}
if(idx == -1) {
fprintf(stderr, "%s: no such item in the list ``%s''\n",
page_id.c_str(), lid);
return false;
}
ListItemData li;
ok = dbs.GetListItemData(ld, idx, li);
if(!ok) {
fprintf(stderr, "%s=%s: couldn't load the list item data\n",
lid, page_id.c_str());
return false;
}
return get_fname_li(dbs, ld.comments_conf, li, cmt_id, choice);
}
static ScriptVariable guess_id_from_fname(const ScriptVariable &fname)
{
ScriptVector pparts(fname, "/", "");
ScriptVariable last = pparts[pparts.Length()-1];
if(last == PAGESET_MAIN_FILE_NAME)
return pparts[pparts.Length()-2];
if(last.Length() == 4) {
long n;
bool num = last.GetLong(n, 10);
if(num)
return ScriptNumber(num);
}
if(last == "" || last == "." || last == "..")
return ScriptVariableInv();
return last;
}
static bool choose_filename(cmdline_common &cmd_com, const char *name_str,
file_choice &choice)
{
ScriptVariable name(name_str);
if(name.Strchr('/').IsValid()) {
choice.filename = name;
choice.id = guess_id_from_fname(name);
return true;
}
if(name.Strchr('=').IsInvalid()) { // no ``/'', no ``=''
FileStat fs(name.c_str());
if(fs.Exists() && fs.IsRegularFile()) {
choice.filename = name;
return true;
}
}
Database database;
bool ok;
ok = load_inifiles(database, cmd_com.inifiles, cmd_com.opt_selector);
if(!ok)
return false;
ScriptVector vect(name, "=", "");
int vlen = vect.Length();
if(vlen < 1) // nonsense, but technically possible (empty param)
return false;
if(vlen == 1) { // very special case, partially handled above
// it must be a pageset page ID, and there must be only one pageset
ScriptVector sets;
database.GetSets(sets);
if(sets.Length() < 1) {
fprintf(stderr, "there are no page sets\n");
return false;
}
if(sets.Length() > 1) {
fprintf(stderr,
"there are more than one page sets, specify the ID\n");
return false;
}
return get_fname_setitem(database, sets[0], vect[0], choice);
}
if(vlen == 2) { // setID=pageID or pageID=cmtID (if there's 1 realm)
ScriptVector sets, lists;
database.GetSets(sets);
database.GetLists(lists);
// check if there's one realm
if(sets.Length() == 0 && lists.Length() == 1)
return get_fname_listcmt(database, lists[0], vect[0], vect[1],
choice);
if(sets.Length() == 1 && lists.Length() == 0)
return get_fname_setcmt(database, sets[0], vect[0], vect[1],
choice);
// more than one realm (or none at all? n/p, will fail later)
return get_fname_setitem(database, vect[0], vect[1], choice);
}
// 3, 4
if(vect[0] == "list") {
if(vlen == 3)
return get_fname_listitem(database, vect[1], vect[2], choice);
else
return get_fname_listcmt(database, vect[1], vect[2], vect[3],
choice);
}
if(vect[0] == "set" || vect[0] == "pageset") {
if(vlen == 3)
return get_fname_setitem(database, vect[1], vect[2], choice);
else
return get_fname_setcmt(database, vect[1], vect[2], vect[3],
choice);
}
if(vlen > 3) {
fprintf(stderr, "realm type ``%s'' unknown\n", vect[0].c_str());
return false;
}
// exactly 3 tokens, must be realmID=pageID=cmtID
// now we need to guess the realm type
ScriptVector sets, lists;
database.GetSets(sets);
database.GetLists(lists);
bool may_be_set = svec_has_elem(sets, vect[0]);
bool may_be_list = svec_has_elem(lists, vect[0]);
if(may_be_list && may_be_set) {
fprintf(stderr,
"both list and set named ``%s'' exist, give the realm type\n",
vect[0].c_str());
return false;
}
if(may_be_list)
return get_fname_listcmt(database, vect[0], vect[1], vect[2], choice);
if(may_be_set)
return get_fname_setcmt(database, vect[0], vect[1], vect[2], choice);
// neither?
fprintf(stderr, "there's no list nor set named ``%s''\n", vect[0].c_str());
return false;
}
//////////////////////////////////////////////////////////////////////
static void print_headers(const HeadedTextMessage &parser, FILE *stream)
{
const ScriptVector &h = parser.GetHeaders();
int len = h.Length();
int i;
for(i = 0; i < len; i += 2)
fprintf(stream, "%s: %s\n", h[i].c_str(), h[i+1].c_str());
}
//////////////////////////////////////////////////////////////////////
static void perform_single_task(Task *task, HeadedTextMessage &parser,
const cmdline_update &opts)
{
const char *hname = headername(task->category);
ScriptVector &hdr = parser.GetHeaders();
bool found = false;
int i;
for(i = 0; i < hdr.Length(); i += 2)
if(hdr[i] == hname) {
found = true;
break;
}
if(!found) { // trivial case
if(task->add) {
hdr.AddItem(hname);
hdr.AddItem(task->name);
} else {
// actually, nothing to do: can't remove anything
// from an empty list
}
return;
}
ScriptVector v(hdr[i+1], ",", " \t\r");
int pos = -1;
int j;
for(j = 0; j < v.Length(); j++)
if(v[j] == task->name) {
pos = j;
break;
}
if(task->add) {
if(pos != -1) // already there!
return;
v.AddItem(task->name);
} else {
if(pos == -1) // not there, can't remove
return;
v.Remove(pos, 1);
}
// if we're here, we should reconstruct the changed header
if(v.Length() == 0) // empty? simply remove the header altogether
hdr.Remove(i, 2);
else
hdr[i+1] = v.Join(", ");
}
static void
do_update_unixtime(const cmdline_update &opts, HeadedTextMessage &parser)
{
if(!opts.force_date_update) {
ScriptVariable orig = parser.FindHeader("unixtime");
if(orig.IsValid() && orig != "")
return;
}
long long tm = opts.date > 1 ? opts.date : time(0);
parser.SetHeader("unixtime", ScriptNumber(tm));
}
static void do_update_teaser(HeadedTextMessage &parser)
{
ScriptVariable descr = parser.FindHeader("descr");
if(descr.IsValid() && descr != "") {
parser.RemoveHeader("teaser_len");
return;
}
ScriptVariable &body = parser.GetBody();
ScriptVariable::Substring pos = body.Strstr("<!--break-->");
int x = pos.IsValid() ? pos.Index() : body.Length();
parser.SetHeader("teaser_len", ScriptNumber(x));
}
static bool proceed_update_headedtext(const cmdline_update &opts)
{
const char *fp = opts.filename.c_str();
FILE *s = fopen(fp, "r");
if(!s) {
perror(fp);
return false;
}
HeadedTextMessage parser;
int c;
while((c = fgetc(s)) != EOF) {
if(!parser.FeedChar(c)) // int, ok
break;
}
fclose(s);
if(opts.dry_run || opts.verbose) {
fprintf(stderr, "== header fields before changes ==============\n");
print_headers(parser, stderr);
}
do_update_unixtime(opts, parser);
if(!opts.dont_touch_id) {
if(opts.id.IsValid() && opts.id != "")
parser.SetHeader("id", opts.id);
else
fprintf(stderr, "WARNING: couldn't guess the id, not updated\n");
}
if(opts.teaser_update)
do_update_teaser(parser);
Task *p;
for(p = opts.first; p; p = p->next)
perform_single_task(p, parser, opts);
if(opts.dry_run || opts.verbose) {
fprintf(stderr, "== header fields after changes ===============\n");
print_headers(parser, stderr);
fprintf(stderr, "==============================================\n");
}
if(opts.dry_run) {
fprintf(stderr, "NOTICE: dry run, not updating any files\n");
return true;
}
if(opts.backup) {
int r = rename(fp, (opts.filename + opts.baksuffix).c_str());
if(r == -1) {
perror((opts.filename + opts.baksuffix).c_str());
fprintf(stderr, "couldn't backup old version, aborting\n");
return false;
}
}
// save parser to file
s = fopen(fp, "w");
if(!s) {
perror(fp);
return false;
}
ScriptVariable nc = parser.Serialize();
fputs(nc.c_str(), s);
fclose(s);
return true;
}
//////////////////////////////////////////////////////////////////////
int perform_update(cmdline_common &cmd_com, int argc, const char * const *argv)
{
if(argc < 2) {
fprintf(stderr, "``update'' requires args, try ``help update''\n");
return 1;
}
cmdline_update opts;
bool ok;
ok = choose_filename(cmd_com, argv[1], opts);
if(!ok)
return 1;
ok = parse_update_cmdl(argc - 2, argv + 2, opts);
if(!ok)
return 1;
if(opts.dry_run || opts.verbose)
fprintf(stderr, "NOTICE: file '%s' selected\n", opts.filename.c_str());
ok = proceed_update_headedtext(opts);
if(!ok)
return 1;
return 0;
}
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
void help_inspect(FILE *stream)
{
fprintf(stream,
"The ``inspect'' command prints a ``headed text'' file header\n"
"and optionally some lines of its body. Usage:\n"
"\n"
" thalassa [...] inspect <filespec> [<options>]\n"
"\n"
FILESPEC_TEXT
"\n"
"Valid options are:\n"
"\n"
" -b <n> show the first <n> lines of the body\n"
" -B show the whole body\n"
" -v verbose messages\n"
"\n"
);
}
struct cmdline_inspect : public file_choice {
int lines;
bool whole_body, verbose;
cmdline_inspect() : lines(0), whole_body(false), verbose(false) {}
};
// the file name (or object spec) is already not there
static bool
parse_inspect_cmdl(int argc, const char *const *argv, cmdline_inspect &options)
{
const char *const *p = argv;
while(*p) {
if((*p)[0] == '-') {
if((*p)[1] == 0 || (*p)[2] != 0) {
fprintf(stderr, "option %s unknown\n", *p);
return false;
}
switch((*p)[1]) {
case 'b':
if(!p[1]) {
fprintf(stderr, "-b requires a parameter\n");
return false;
}
{ // to localize vars
long n;
bool ok = ScriptVariable(p[1]).GetLong(n, 10);
if(!ok || n < 0) {
fprintf(stderr, "invalid number ``%s''\n", p[1]);
return false;
}
options.lines = n;
}
p += 2;
break;
case 'B':
options.whole_body = true;
p++;
break;
case 'v':
options.verbose = true;
p++;
break;
default:
fprintf(stderr, "option %s unknown\n", *p);
return false;
}
continue;
}
fprintf(stderr, "extra argument(s) starting with %s\n", *p);
return false;
}
return true;
}
int perform_inspect(cmdline_common &cmd_com, int argc, const char *const *argv)
{
if(argc < 2) {
fprintf(stderr, "``inspect'' requires args, try ``help inspect''\n");
return 1;
}
cmdline_inspect options;
bool ok;
ok = choose_filename(cmd_com, argv[1], options);
if(!ok)
return 1;
ok = parse_inspect_cmdl(argc - 2, argv + 2, options);
if(!ok)
return 1;
if(options.verbose)
fprintf(stderr, "NOTICE: file '%s' selected\n",
options.filename.c_str());
const char *fp = options.filename.c_str();
FILE *s = fopen(fp, "r");
if(!s) {
perror(fp);
return false;
}
HeadedTextMessage parser;
int c;
while((c = fgetc(s)) != EOF) {
if(!parser.FeedChar(c)) // int, ok
break;
}
fclose(s);
print_headers(parser, stdout);
fputc('\n', stdout);
int lines = 0;
const char *b = parser.GetBody().c_str();
while(*b && (lines < options.lines || options.whole_body)) {
fputc(*b, stdout);
if(*b == '\n')
lines++;
b++;
}
return 0;
}