|
// retoor <retoor@molodetz.nl>
|
|
|
|
#include "tool.h"
|
|
#include "db.h"
|
|
#include "messages.h"
|
|
#include "r_config.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <libgen.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
|
|
#define SNAPSHOT_MAX_FILE_SIZE 1048576
|
|
|
|
static char *snapshot_resolve_session_id(void) {
|
|
r_config_handle cfg = r_config_get_instance();
|
|
const char *session_id = r_config_get_session_id(cfg);
|
|
if (session_id && *session_id) return strdup(session_id);
|
|
return messages_generate_session_id();
|
|
}
|
|
|
|
static int mkdirs_recursive(const char *path) {
|
|
if (!path || !*path) return -1;
|
|
char temp[4096];
|
|
size_t len = strlen(path);
|
|
if (len >= sizeof(temp)) return -1;
|
|
memcpy(temp, path, len + 1);
|
|
if (temp[len - 1] == '/') temp[len - 1] = '\0';
|
|
for (char *p = temp + 1; *p; p++) {
|
|
if (*p == '/') {
|
|
*p = '\0';
|
|
if (mkdir(temp, 0755) != 0 && errno != EEXIST) return -1;
|
|
*p = '/';
|
|
}
|
|
}
|
|
if (mkdir(temp, 0755) != 0 && errno != EEXIST) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static int snapshot_mkdirs(const char *filepath) {
|
|
char *path_copy = strdup(filepath);
|
|
if (!path_copy) return -1;
|
|
char *dir = dirname(path_copy);
|
|
if (!dir || strcmp(dir, ".") == 0 || strcmp(dir, "/") == 0) {
|
|
free(path_copy);
|
|
return 0;
|
|
}
|
|
int rc = mkdirs_recursive(dir);
|
|
free(path_copy);
|
|
return rc;
|
|
}
|
|
|
|
static struct json_object *create_snapshot_get_description(void);
|
|
static char *create_snapshot_execute(tool_t *self, struct json_object *args);
|
|
static void create_snapshot_print_action(const char *name, struct json_object *args);
|
|
|
|
static struct json_object *list_snapshots_get_description(void);
|
|
static char *list_snapshots_execute(tool_t *self, struct json_object *args);
|
|
static void list_snapshots_print_action(const char *name, struct json_object *args);
|
|
|
|
static struct json_object *restore_snapshot_get_description(void);
|
|
static char *restore_snapshot_execute(tool_t *self, struct json_object *args);
|
|
static void restore_snapshot_print_action(const char *name, struct json_object *args);
|
|
|
|
static const tool_vtable_t create_snapshot_vtable = {
|
|
.get_description = create_snapshot_get_description,
|
|
.execute = create_snapshot_execute,
|
|
.print_action = create_snapshot_print_action
|
|
};
|
|
|
|
static const tool_vtable_t list_snapshots_vtable = {
|
|
.get_description = list_snapshots_get_description,
|
|
.execute = list_snapshots_execute,
|
|
.print_action = list_snapshots_print_action
|
|
};
|
|
|
|
static const tool_vtable_t restore_snapshot_vtable = {
|
|
.get_description = restore_snapshot_get_description,
|
|
.execute = restore_snapshot_execute,
|
|
.print_action = restore_snapshot_print_action
|
|
};
|
|
|
|
static tool_t create_snapshot_tool = { .vtable = &create_snapshot_vtable, .name = "create_snapshot" };
|
|
static tool_t list_snapshots_tool = { .vtable = &list_snapshots_vtable, .name = "list_snapshots" };
|
|
static tool_t restore_snapshot_tool = { .vtable = &restore_snapshot_vtable, .name = "restore_snapshot" };
|
|
|
|
tool_t *tool_create_snapshot_create(void) { return &create_snapshot_tool; }
|
|
tool_t *tool_list_snapshots_create(void) { return &list_snapshots_tool; }
|
|
tool_t *tool_restore_snapshot_create(void) { return &restore_snapshot_tool; }
|
|
|
|
void snapshot_live_record(const char *path, const char *content) {
|
|
if (!path || !content) return;
|
|
char *session_id = snapshot_resolve_session_id();
|
|
if (!session_id) return;
|
|
r_config_handle cfg = r_config_get_instance();
|
|
const char *prompt = r_config_get_current_prompt(cfg);
|
|
if (!prompt || !*prompt) prompt = "auto";
|
|
db_handle db = db_open(NULL);
|
|
if (!db) {
|
|
free(session_id);
|
|
return;
|
|
}
|
|
long long snapshot_id = 0;
|
|
if (db_snapshot_ensure_live(db, session_id, prompt, &snapshot_id) == R_SUCCESS) {
|
|
db_snapshot_upsert_file(db, snapshot_id, path, content);
|
|
}
|
|
db_close(db);
|
|
free(session_id);
|
|
}
|
|
|
|
static void create_snapshot_print_action(const char *name, struct json_object *args) {
|
|
(void)name;
|
|
if (!args) return;
|
|
struct json_object *desc;
|
|
if (json_object_object_get_ex(args, "description", &desc)) {
|
|
fprintf(stderr, " -> Creating snapshot: %s\n", json_object_get_string(desc));
|
|
}
|
|
}
|
|
|
|
static char *create_snapshot_execute(tool_t *self, struct json_object *args) {
|
|
(void)self;
|
|
struct json_object *paths_obj = NULL;
|
|
struct json_object *desc_obj = NULL;
|
|
|
|
if (!json_object_object_get_ex(args, "paths", &paths_obj) ||
|
|
!json_object_is_type(paths_obj, json_type_array)) {
|
|
return strdup("Error: missing or invalid 'paths' array argument");
|
|
}
|
|
if (!json_object_object_get_ex(args, "description", &desc_obj)) {
|
|
return strdup("Error: missing 'description' argument");
|
|
}
|
|
|
|
int path_count = json_object_array_length(paths_obj);
|
|
if (path_count <= 0) {
|
|
return strdup("Error: 'paths' array is empty");
|
|
}
|
|
|
|
const char **paths = calloc((size_t)path_count, sizeof(char *));
|
|
const char **contents = calloc((size_t)path_count, sizeof(char *));
|
|
char **allocated_contents = calloc((size_t)path_count, sizeof(char *));
|
|
struct json_object *skipped = json_object_new_array();
|
|
if (!paths || !contents || !allocated_contents || !skipped) {
|
|
free(paths);
|
|
free(contents);
|
|
free(allocated_contents);
|
|
json_object_put(skipped);
|
|
return strdup("Error: out of memory");
|
|
}
|
|
|
|
int captured = 0;
|
|
for (int i = 0; i < path_count; i++) {
|
|
const char *path = json_object_get_string(json_object_array_get_idx(paths_obj, i));
|
|
if (!path) continue;
|
|
|
|
FILE *fp = fopen(path, "r");
|
|
if (!fp) {
|
|
json_object_array_add(skipped, json_object_new_string(path));
|
|
continue;
|
|
}
|
|
|
|
fseek(fp, 0, SEEK_END);
|
|
long size = ftell(fp);
|
|
rewind(fp);
|
|
|
|
if (size < 0 || size > SNAPSHOT_MAX_FILE_SIZE) {
|
|
fclose(fp);
|
|
json_object_array_add(skipped, json_object_new_string(path));
|
|
continue;
|
|
}
|
|
|
|
char *buf = malloc((size_t)size + 1);
|
|
if (!buf) {
|
|
fclose(fp);
|
|
json_object_array_add(skipped, json_object_new_string(path));
|
|
continue;
|
|
}
|
|
|
|
size_t read_bytes = fread(buf, 1, (size_t)size, fp);
|
|
buf[read_bytes] = '\0';
|
|
fclose(fp);
|
|
|
|
paths[captured] = path;
|
|
contents[captured] = buf;
|
|
allocated_contents[captured] = buf;
|
|
captured++;
|
|
}
|
|
|
|
char *response = NULL;
|
|
if (captured == 0) {
|
|
response = strdup("Error: no files could be read");
|
|
} else {
|
|
char *session_id = snapshot_resolve_session_id();
|
|
if (!session_id) {
|
|
response = strdup("Error: could not determine session ID");
|
|
} else {
|
|
db_handle db = db_open(NULL);
|
|
if (!db) {
|
|
response = strdup("Error: failed to open database");
|
|
} else {
|
|
long long snapshot_id = 0;
|
|
r_status_t status = db_snapshot_create(db, session_id,
|
|
json_object_get_string(desc_obj),
|
|
paths, contents, captured, &snapshot_id);
|
|
db_close(db);
|
|
|
|
if (status != R_SUCCESS) {
|
|
response = strdup("Error: failed to create snapshot in database");
|
|
} else {
|
|
struct json_object *result = json_object_new_object();
|
|
json_object_object_add(result, "snapshot_id", json_object_new_int64(snapshot_id));
|
|
json_object_object_add(result, "files_captured", json_object_new_int(captured));
|
|
json_object_object_add(result, "description",
|
|
json_object_new_string(json_object_get_string(desc_obj)));
|
|
json_object_object_add(result, "skipped", json_object_get(skipped));
|
|
response = strdup(json_object_to_json_string(result));
|
|
json_object_put(result);
|
|
}
|
|
}
|
|
free(session_id);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < captured; i++) {
|
|
free(allocated_contents[i]);
|
|
}
|
|
free(paths);
|
|
free(contents);
|
|
free(allocated_contents);
|
|
json_object_put(skipped);
|
|
return response;
|
|
}
|
|
|
|
static struct json_object *create_snapshot_get_description(void) {
|
|
struct json_object *root = json_object_new_object();
|
|
json_object_object_add(root, "type", json_object_new_string("function"));
|
|
|
|
struct json_object *function = json_object_new_object();
|
|
json_object_object_add(function, "name", json_object_new_string("create_snapshot"));
|
|
json_object_object_add(function, "description",
|
|
json_object_new_string("Creates a snapshot of specified files, storing their contents "
|
|
"in the database for later restoration. Use before making significant changes."));
|
|
|
|
struct json_object *parameters = json_object_new_object();
|
|
json_object_object_add(parameters, "type", json_object_new_string("object"));
|
|
|
|
struct json_object *properties = json_object_new_object();
|
|
|
|
struct json_object *paths_prop = json_object_new_object();
|
|
json_object_object_add(paths_prop, "type", json_object_new_string("array"));
|
|
struct json_object *items = json_object_new_object();
|
|
json_object_object_add(items, "type", json_object_new_string("string"));
|
|
json_object_object_add(paths_prop, "items", items);
|
|
json_object_object_add(paths_prop, "description",
|
|
json_object_new_string("Array of absolute file paths to snapshot."));
|
|
json_object_object_add(properties, "paths", paths_prop);
|
|
|
|
struct json_object *desc_prop = json_object_new_object();
|
|
json_object_object_add(desc_prop, "type", json_object_new_string("string"));
|
|
json_object_object_add(desc_prop, "description",
|
|
json_object_new_string("Description of what this snapshot captures and why."));
|
|
json_object_object_add(properties, "description", desc_prop);
|
|
|
|
json_object_object_add(parameters, "properties", properties);
|
|
|
|
struct json_object *required = json_object_new_array();
|
|
json_object_array_add(required, json_object_new_string("paths"));
|
|
json_object_array_add(required, json_object_new_string("description"));
|
|
json_object_object_add(parameters, "required", required);
|
|
json_object_object_add(parameters, "additionalProperties", json_object_new_boolean(0));
|
|
|
|
json_object_object_add(function, "parameters", parameters);
|
|
|
|
r_config_handle cfg = r_config_get_instance();
|
|
if (r_config_use_strict(cfg)) {
|
|
json_object_object_add(function, "strict", json_object_new_boolean(1));
|
|
}
|
|
|
|
json_object_object_add(root, "function", function);
|
|
return root;
|
|
}
|
|
|
|
static void list_snapshots_print_action(const char *name, struct json_object *args) {
|
|
(void)name;
|
|
(void)args;
|
|
fprintf(stderr, " -> Listing snapshots\n");
|
|
}
|
|
|
|
static char *list_snapshots_execute(tool_t *self, struct json_object *args) {
|
|
(void)self;
|
|
(void)args;
|
|
|
|
char *session_id = snapshot_resolve_session_id();
|
|
if (!session_id) return strdup("Error: could not determine session ID");
|
|
|
|
db_handle db = db_open(NULL);
|
|
if (!db) {
|
|
free(session_id);
|
|
return strdup("Error: failed to open database");
|
|
}
|
|
|
|
struct json_object *result = NULL;
|
|
r_status_t status = db_snapshot_list(db, session_id, &result);
|
|
db_close(db);
|
|
free(session_id);
|
|
|
|
if (status != R_SUCCESS || !result) {
|
|
return strdup("Error: failed to list snapshots");
|
|
}
|
|
|
|
char *response = strdup(json_object_to_json_string(result));
|
|
json_object_put(result);
|
|
return response;
|
|
}
|
|
|
|
static struct json_object *list_snapshots_get_description(void) {
|
|
struct json_object *root = json_object_new_object();
|
|
json_object_object_add(root, "type", json_object_new_string("function"));
|
|
|
|
struct json_object *function = json_object_new_object();
|
|
json_object_object_add(function, "name", json_object_new_string("list_snapshots"));
|
|
json_object_object_add(function, "description",
|
|
json_object_new_string("Lists all file snapshots for the current session, "
|
|
"showing ID, description, creation time, and file count."));
|
|
|
|
struct json_object *parameters = json_object_new_object();
|
|
json_object_object_add(parameters, "type", json_object_new_string("object"));
|
|
json_object_object_add(parameters, "properties", json_object_new_object());
|
|
json_object_object_add(parameters, "additionalProperties", json_object_new_boolean(0));
|
|
|
|
json_object_object_add(function, "parameters", parameters);
|
|
|
|
r_config_handle cfg = r_config_get_instance();
|
|
if (r_config_use_strict(cfg)) {
|
|
json_object_object_add(function, "strict", json_object_new_boolean(1));
|
|
}
|
|
|
|
json_object_object_add(root, "function", function);
|
|
return root;
|
|
}
|
|
|
|
static void restore_snapshot_print_action(const char *name, struct json_object *args) {
|
|
(void)name;
|
|
if (!args) return;
|
|
struct json_object *id_obj;
|
|
if (json_object_object_get_ex(args, "snapshot_id", &id_obj)) {
|
|
fprintf(stderr, " -> Restoring snapshot #%lld\n",
|
|
(long long)json_object_get_int64(id_obj));
|
|
}
|
|
}
|
|
|
|
static char *restore_snapshot_execute(tool_t *self, struct json_object *args) {
|
|
(void)self;
|
|
struct json_object *id_obj = NULL;
|
|
|
|
if (!json_object_object_get_ex(args, "snapshot_id", &id_obj)) {
|
|
return strdup("Error: missing 'snapshot_id' argument");
|
|
}
|
|
|
|
long long snapshot_id = json_object_get_int64(id_obj);
|
|
|
|
db_handle db = db_open(NULL);
|
|
if (!db) return strdup("Error: failed to open database");
|
|
|
|
struct json_object *files = NULL;
|
|
r_status_t status = db_snapshot_get_files(db, snapshot_id, &files);
|
|
db_close(db);
|
|
|
|
if (status != R_SUCCESS || !files) {
|
|
return strdup("Error: failed to retrieve snapshot files");
|
|
}
|
|
|
|
int file_count = json_object_array_length(files);
|
|
if (file_count == 0) {
|
|
json_object_put(files);
|
|
return strdup("Error: snapshot contains no files");
|
|
}
|
|
|
|
int restored = 0;
|
|
struct json_object *failed = json_object_new_array();
|
|
|
|
for (int i = 0; i < file_count; i++) {
|
|
struct json_object *entry = json_object_array_get_idx(files, i);
|
|
struct json_object *path_obj = NULL;
|
|
struct json_object *content_obj = NULL;
|
|
|
|
if (!json_object_object_get_ex(entry, "path", &path_obj) ||
|
|
!json_object_object_get_ex(entry, "content", &content_obj)) {
|
|
continue;
|
|
}
|
|
|
|
const char *path = json_object_get_string(path_obj);
|
|
const char *content = json_object_get_string(content_obj);
|
|
if (!path || !content) continue;
|
|
|
|
snapshot_mkdirs(path);
|
|
|
|
char tmp_path[4096];
|
|
snprintf(tmp_path, sizeof(tmp_path), "%s.snapshot_tmp", path);
|
|
|
|
FILE *fp = fopen(tmp_path, "w");
|
|
if (!fp) {
|
|
json_object_array_add(failed, json_object_new_string(path));
|
|
continue;
|
|
}
|
|
|
|
size_t content_len = strlen(content);
|
|
size_t written = fwrite(content, 1, content_len, fp);
|
|
fclose(fp);
|
|
|
|
if (written != content_len) {
|
|
unlink(tmp_path);
|
|
json_object_array_add(failed, json_object_new_string(path));
|
|
continue;
|
|
}
|
|
|
|
if (rename(tmp_path, path) != 0) {
|
|
unlink(tmp_path);
|
|
json_object_array_add(failed, json_object_new_string(path));
|
|
continue;
|
|
}
|
|
|
|
restored++;
|
|
}
|
|
|
|
struct json_object *result = json_object_new_object();
|
|
json_object_object_add(result, "snapshot_id", json_object_new_int64(snapshot_id));
|
|
json_object_object_add(result, "restored", json_object_new_int(restored));
|
|
json_object_object_add(result, "failed", failed);
|
|
|
|
char *response = strdup(json_object_to_json_string(result));
|
|
json_object_put(result);
|
|
json_object_put(files);
|
|
return response;
|
|
}
|
|
|
|
static struct json_object *restore_snapshot_get_description(void) {
|
|
struct json_object *root = json_object_new_object();
|
|
json_object_object_add(root, "type", json_object_new_string("function"));
|
|
|
|
struct json_object *function = json_object_new_object();
|
|
json_object_object_add(function, "name", json_object_new_string("restore_snapshot"));
|
|
json_object_object_add(function, "description",
|
|
json_object_new_string("Restores files from a previously created snapshot. "
|
|
"Writes each file back to its original path atomically."));
|
|
|
|
struct json_object *parameters = json_object_new_object();
|
|
json_object_object_add(parameters, "type", json_object_new_string("object"));
|
|
|
|
struct json_object *properties = json_object_new_object();
|
|
|
|
struct json_object *id_prop = json_object_new_object();
|
|
json_object_object_add(id_prop, "type", json_object_new_string("integer"));
|
|
json_object_object_add(id_prop, "description",
|
|
json_object_new_string("The snapshot ID to restore. Use list_snapshots to find available IDs."));
|
|
json_object_object_add(properties, "snapshot_id", id_prop);
|
|
|
|
json_object_object_add(parameters, "properties", properties);
|
|
|
|
struct json_object *required = json_object_new_array();
|
|
json_object_array_add(required, json_object_new_string("snapshot_id"));
|
|
json_object_object_add(parameters, "required", required);
|
|
json_object_object_add(parameters, "additionalProperties", json_object_new_boolean(0));
|
|
|
|
json_object_object_add(function, "parameters", parameters);
|
|
|
|
r_config_handle cfg = r_config_get_instance();
|
|
if (r_config_use_strict(cfg)) {
|
|
json_object_object_add(function, "strict", json_object_new_boolean(1));
|
|
}
|
|
|
|
json_object_object_add(root, "function", function);
|
|
return root;
|
|
}
|