aboutsummaryrefslogtreecommitdiff
path: root/src/cmd_sie.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
commit8351fa7752bb8341ef405a37ff7c73142715e981 (patch)
treedde603d313f4f26934f2ce20a22dfd9b69324cf0 /src/cmd_sie.c
parentbeafdbe0d64340387d619c38554976410129f05b (diff)
downloadbokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz
bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip
commands: split the command table by domain
Diffstat (limited to 'src/cmd_sie.c')
-rw-r--r--src/cmd_sie.c171
1 files changed, 171 insertions, 0 deletions
diff --git a/src/cmd_sie.c b/src/cmd_sie.c
new file mode 100644
index 0000000..d5b2e8e
--- /dev/null
+++ b/src/cmd_sie.c
@@ -0,0 +1,171 @@
+#include "commands.h"
+#include "cmd_util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "audit.h"
+#include "config.h"
+#include "db.h"
+#include "sha256.h"
+#include "sie.h"
+#include "util.h"
+
+static int hash_file(const char *path, char hex[65], int64_t *size)
+{
+ FILE *f = fopen(path, "rb");
+ if (!f)
+ return -1;
+ SHA256_CTX ctx;
+ sha256_init(&ctx);
+ unsigned char chunk[65536];
+ size_t rn;
+ int64_t total = 0;
+ while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0) {
+ sha256_update(&ctx, (const BYTE *)chunk, rn);
+ total += (int64_t)rn;
+ }
+ int bad = ferror(f);
+ fclose(f);
+ if (bad)
+ return -1;
+ unsigned char digest[32];
+ sha256_final(&ctx, digest);
+ util_hex(digest, 32, hex);
+ if (size)
+ *size = total;
+ return 0;
+}
+static yyjson_mut_val *h_sie_export(struct req *r)
+{
+ int64_t fy = 0;
+ if (req_fy(r, &fy) != 0)
+ return NULL;
+ int inline_out = 0;
+ arg_bool(r->args, "inline", &inline_out);
+
+ if (mkdir_p(g_cfg.export_dir, 0700) != 0)
+ return fail(r, "INTERNAL", "cannot create export directory");
+ char label[64] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT label FROM fiscal_years WHERE org_id=?1 AND id=?2", -1,
+ &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, fy);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return fail(r, "NOT_FOUND", "fiscal year not found");
+ }
+ snprintf(label, sizeof label, "%s", sqlite3_column_text(st, 0));
+ sqlite3_finalize(st);
+ for (char *p = label; *p; p++)
+ if (*p == '/')
+ *p = '-';
+ char path[4096];
+ snprintf(path, sizeof path, "%s/%lld_%s.se", g_cfg.export_dir,
+ (long long)r->org_id, label);
+ char *err = NULL;
+ if (sie_export_file(r->db, r->org_id, fy, path, &err) != 0) {
+ yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "export failed");
+ free(err);
+ return e;
+ }
+ char hex[65];
+ int64_t size = 0;
+ if (hash_file(path, hex, &size) != 0)
+ return fail(r, "INTERNAL", "could not read exported file");
+
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "sie.export", "{}", "OK", NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "path", path);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex);
+ yyjson_mut_obj_add_int(r->rdoc, o, "size", size);
+ if (inline_out) {
+ size_t flen = 0;
+ unsigned char *content = read_file(path, &flen);
+ if (!content)
+ return fail(r, "INTERNAL", "could not read exported file");
+ char *b64 = util_b64(content, flen);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "content_base64", b64);
+ free(b64);
+ free(content);
+ }
+ return o;
+}
+
+static yyjson_mut_val *h_sie_import(struct req *r)
+{
+ const char *b64 = arg_str(r->args, "content_base64");
+ const char *path = arg_str(r->args, "path");
+ unsigned char *content = NULL;
+ size_t len = 0;
+ if (b64) {
+ if (util_b64_decode(b64, strlen(b64), &content, &len) != 0)
+ return fail(r, "INVALID_ARGS",
+ "content_base64 is not valid base64");
+ } else if (path) {
+ content = read_file(path, &len);
+ if (!content)
+ return fail(r, "NOT_FOUND", "could not read file");
+ } else {
+ return fail(r, "INVALID_ARGS", "content_base64 or path is required");
+ }
+ if (len > 64u * 1024u * 1024u) {
+ free(content);
+ return fail(r, "TOO_LARGE", "SIE file is too large");
+ }
+ int64_t fy = 0, vouchers = 0, accounts = 0;
+ char *err = NULL;
+ int rc = sie_import_content(r->db, r->org_id, r->sess->user_id, content,
+ len, r->dry_run, &fy, &vouchers, &accounts,
+ &err);
+ free(content);
+ if (rc != 0) {
+ yyjson_mut_val *e = fail(r, "INVALID_ARGS", err ? err : "import failed");
+ free(err);
+ return e;
+ }
+ if (!r->dry_run) {
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "sie.import", reqjson, "OK", NULL);
+ free(reqjson);
+ }
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy);
+ yyjson_mut_obj_add_int(r->rdoc, o, "vouchers", vouchers);
+ yyjson_mut_obj_add_int(r->rdoc, o, "accounts_created", accounts);
+ if (r->dry_run)
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ return o;
+}
+
+
+static const struct cmd_arg args_sie_export[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "inline", ARG_BOOL, 0, "false", NULL, "Also return content_base64" },
+};
+
+static const struct cmd_arg args_sie_import[] = {
+ { "content_base64", ARG_STR, 0, NULL, NULL, "SIE file content, base64" },
+ { "path", ARG_STR, 0, NULL, NULL,
+ "Server-side path; alternative to content_base64" },
+};
+
+const struct command g_cmd_sie[] = {
+ { "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export,
+ CMD_ARGS(args_sie_export) },
+ { "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1,
+ 1, h_sie_import, CMD_ARGS(args_sie_import) },
+};
+
+const struct cmd_table g_cmd_table_sie = {
+ g_cmd_sie, sizeof g_cmd_sie / sizeof g_cmd_sie[0]
+};