agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 25041ea993bd7df5e786e55ec442676b22c284f5
parent 417be015e6aa1bccb5e3e4b4834eecaf5373f77d
Author: Marc Coquand <marc@coquand.email>
Date:   Wed, 25 Jun 2025 20:50:18 +0100

Overhaul the project, use tree structure instead

Diffstat:
MMakefile | 8++++----
Mjournal_entry.c | 443+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mjournal_entry.h | 26+++++++++++++++++---------
Mmain.c | 243+++++++++----------------------------------------------------------------------
Dsample_ics/2025-01-01.ics | 8--------
Atree.c | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atree.h | 38++++++++++++++++++++++++++++++++++++++
7 files changed, 465 insertions(+), 407 deletions(-)
diff --git a/Makefile b/Makefile
@@ -5,11 +5,11 @@ LIBS = `pkg-config uuid fuse3 libical --cflags --libs`
 
 all: caldavfs
 
-caldavfs: main.c util.c hashmap.c journal_entry.c
-	$(CC) $(CFLAGS) main.c journal_entry.c util.c hashmap.c -o mount_caldavfs $(LIBS)
+caldavfs: main.c util.c hashmap.c journal_entry.c tree.c
+	$(CC) $(CFLAGS) *.c -o mount_caldavfs $(LIBS)
 
-debug: main.c util.c hashmap.c journal_entry.c
-	$(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) main.c journal_entry.c util.c hashmap.c -o mount_caldavfs-debug $(LIBS)
+debug: main.c util.c hashmap.c journal_entry.c tree.c
+	$(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) *.c -o mount_caldavfs-debug $(LIBS)
 
 clean:
 	rm -f caldavfs caldavfs-debug
diff --git a/journal_entry.c b/journal_entry.c
@@ -1,6 +1,7 @@
 #include "journal_entry.h"
 
 #include "hashmap.h"
+#include "tree.h"
 #include "util.h"
 #include <dirent.h>
 #include <errno.h>
@@ -18,17 +19,115 @@
 #include <uuid/uuid.h>
 #include <wordexp.h>
 
-struct hashmap *entries_fuse = NULL;
-
 char ICS_DIR[256];
 
+// tree of journal entries
+struct tree_node *fuse_tree_root = NULL;
+
 // journal_entry
 struct hashmap *entries_original_ics = NULL;
 
-// string -> string
-struct hashmap *entries_today = NULL;
+struct tree_node *
+create_file_node(struct journal_entry *entry)
+{
+	struct tree_node *file_node =
+	    create_tree_node(entry, free_journal_entry);
+	return file_node;
+}
+struct journal_entry *
+get_entry(tree_node *node)
+{
+	struct journal_entry *entry = node->data;
+	return entry;
+}
+
+char *
+get_node_filename(tree_node *node)
+{
+	struct journal_entry *entry = node->data;
+	if (entry == NULL)
+		return "";
+	return entry->filename;
+}
+
+icalcomponent *
+get_node_component(tree_node *node)
+{
+	struct journal_entry *entry = node->data;
+	if (entry == NULL)
+		return NULL;
+	return entry->component;
+}
+
+tree_node **
+get_node_children(tree_node *node)
+{
+	tree_node **entries = node->children;
+	return entries;
+}
+
+char **
+split_path(const char *path, size_t *count_out)
+{
+	char *path_copy = strdup(path);
+	if (!path_copy)
+		return NULL;
+
+	size_t count = 0;
+	char **segments = NULL;
+
+	char *token = strtok(path_copy, "/");
+	while (token) {
+		segments = xreallocarray(segments, sizeof(char *), (count + 1));
+		segments[count++] = strdup(token);
+		token = strtok(NULL, "/");
+	}
+	free(path_copy);
+
+	*count_out = count;
+	return segments;
+}
+
+void
+free_segments(char **segments, size_t count)
+{
+	for (size_t i = 0; i < count; ++i)
+		free(segments[i]);
+	free(segments);
+}
+
+struct tree_node *
+find_node_by_path(struct tree_node *root, const char *path)
+{
+	if (!root || !path || *path == '\0')
+		return NULL;
+
+	size_t count;
+	char **segments = split_path(path, &count);
+	if (!segments)
+		return NULL;
+
+	tree_node *current = root;
+
+	for (size_t i = 0; i < count && current; ++i) {
+		const char *segment = segments[i];
+		tree_node *next = NULL;
+
+		for (size_t j = 0; j < current->child_count; ++j) {
+			const char *child_name =
+			    get_node_filename(current->children[j]);
+			if (strcmp(child_name, segment) == 0) {
+				next = current->children[j];
+				break;
+			}
+		}
+		current = next;
+	}
+
+	free_segments(segments, count);
+	return current;
+}
 
-struct hashmap *entries_notes = NULL;
 // Full filepath of original ics file
 char *
 get_original_filepath(const struct journal_entry *entry)
@@ -76,37 +175,26 @@ get_entry_from_fuse_path(const char *path)
 		LOG("Invalid path: '%s'\n", path ? path : "NULL");
 		return NULL;
 	}
+	tree_node *node = find_node_by_path(fuse_tree_root, path);
 
-	const char *prefix = "/entries/";
-	size_t prefix_len = strlen(prefix);
-
-	if (strncmp(path, prefix, prefix_len) != 0) {
-		LOG("Path does not start with '/entries/': '%s'\n", path);
-		return NULL;
-	}
-
-	// Extract the part after "/entries/"
-	const char *key = path + prefix_len;
-
-	// If key is empty (i.e., path is exactly "/entries/"), reject
-	if (strlen(key) == 0) {
-		LOG("Empty key in path: '%s'\n", path);
+	if (!node || !node->data) {
+		LOG("No entry found for key: '%s'\n", path);
 		return NULL;
 	}
 
-	LOG("Looking up key: '%s'", key);
-	struct journal_entry *entry = hashmap_get(entries_fuse, key);
-
-	if (!entry) {
-		LOG("No entry found for key: '%s'\n", key);
-		return NULL;
-	}
+	struct journal_entry *entry = node->data;
 
 	LOG("Entry found: filename = '%s', original = '%s', path = '%s'",
 	    entry->filename, entry->filename_original, path);
 	return entry;
 }
 
+bool
+entry_is_directory(tree_node *node)
+{
+	return (node->child_count > 0);
+}
+
 char *
 read_stream(char *s, size_t size, void *d)
 {
@@ -128,10 +216,11 @@ icalcomponent *
 parse_ics_file(const char *filename)
 {
 	icalcomponent *component = NULL;
+
+	icalparser *parser = icalparser_new();
 	FILE *stream = fopen(filename, "r");
 	assert(stream != 0);
 
-	icalparser *parser = icalparser_new();
 	icalparser_set_gen_data(parser, stream);
 
 	char *line;
@@ -210,6 +299,7 @@ load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry)
 	// Make sure filename, which happens when
 	// Two entries are created on the same minute
 	// TODO: Allow custom fileformats
+	// TODO: Check for duplicates
 	if (asprintf(&entry->filename, "%s.txt",
 		     icalcomponent_get_summary(entry->component)) == -1) {
 		LOG("Failed to write filename");
@@ -233,12 +323,12 @@ load_journal_entries()
 		hashmap_free(entries_original_ics);
 		entries_original_ics = NULL;
 	}
-	if (entries_fuse) {
-		hashmap_free(entries_fuse);
-		entries_fuse = NULL;
+	if (fuse_tree_root) {
+		free_tree(fuse_tree_root);
+		fuse_tree_root = NULL;
 	}
 	entries_original_ics = hashmap_new(NULL);
-	entries_fuse = hashmap_new(free_journal_entry);
+	fuse_tree_root = create_tree_node(NULL, NULL);
 
 	LOG("Loading journal entries from: %s\n", ICS_DIR);
 
@@ -261,8 +351,10 @@ load_journal_entries()
 				continue;
 			}
 
-			if (hashmap_insert(entries_fuse, new_entry->filename,
-					   new_entry) != 0 ||
+			// TODO: Check for duplicate
+			tree_node *new_node = create_file_node(new_entry);
+
+			if (add_child(fuse_tree_root, new_node) != 0 ||
 			    hashmap_insert(entries_original_ics,
 					   new_entry->filename_original,
 					   new_entry) != 0) {
@@ -317,129 +409,6 @@ get_last_modified(struct journal_entry *entry)
 	}
 }
 
-// TODO: Probably better to calculate this on demand
-int
-build_today_hashmap()
-{
-	if (entries_today != NULL)
-		hashmap_free(entries_today);
-	entries_today = hashmap_new(free);
-
-	size_t n_keys = 0;
-	char **keys = hashmap_get_keys(entries_fuse, &n_keys);
-
-	if (!keys) {
-		LOG("Could not get keys");
-		return -1;
-	}
-
-	for (size_t i = 0; i < n_keys; i++) {
-		struct journal_entry *entry =
-		    hashmap_get(entries_fuse, keys[i]);
-		if (!entry)
-			continue;
-
-		// Get the dtstamp of the first icalcomponent
-		icalcomponent *component = entry->component;
-		if (!component)
-			continue;
-
-		struct icaltimetype dt = icalcomponent_get_dtstamp(component);
-
-		if (icaltime_compare_date_only(dt, icaltime_today()) == 0) {
-			const char *summary =
-			    icalcomponent_get_summary(component);
-			if (summary == NULL) {
-				LOG("SUMMARY IS NULL");
-				continue;
-			}
-			char *filename;
-
-			asprintf(&filename, "%s", summary);
-
-			if (filename == NULL) {
-				LOG("Could not get filename");
-				continue;
-			}
-
-			char *dest;
-
-			if (asprintf(&dest, "../entries/%s", entry->filename) ==
-			    -1)
-				return -1;
-
-			hashmap_insert(entries_today, filename, dest);
-		}
-	}
-	LOG("Build complete");
-
-	hashmap_free_keys(keys, n_keys);
-	return 0;
-}
-
-int
-build_notes_hashmap()
-{
-	if (entries_notes != NULL)
-		hashmap_free(entries_notes);
-	entries_notes = hashmap_new(free);
-	LOG("BUILDING ENTRIES");
-
-	size_t n_keys = 0;
-	char **keys = hashmap_get_keys(entries_fuse, &n_keys);
-
-	if (!keys) {
-		LOG("Could not get keys");
-		return -1;
-	}
-	LOG("GOT ENTRIES");
-
-	for (size_t i = 0; i < n_keys; i++) {
-		struct journal_entry *entry =
-		    hashmap_get(entries_fuse, keys[i]);
-
-		LOG("Looking up entry");
-		if (!entry)
-			continue;
-
-		// Get the dtstamp of the first icalcomponent
-		icalcomponent *component = entry->component;
-		if (!component)
-			continue;
-
-		icalcomponent_kind type =
-		    icalcomponent_isa(icalcomponent_get_inner(component));
-
-		if (type == ICAL_VJOURNAL_COMPONENT) {
-			const char *summary =
-			    icalcomponent_get_summary(component);
-			if (summary == NULL) {
-				LOG("SUMMARY IS NULL");
-				continue;
-			}
-			char *filename;
-			asprintf(&filename, "%s", summary);
-
-			if (filename == NULL) {
-				LOG("Could not get filename");
-				continue;
-			}
-
-			char *dest;
-
-			if (asprintf(&dest, "../entries/%s", entry->filename) ==
-			    -1)
-				return -1;
-
-			hashmap_insert(entries_notes, filename, dest);
-		}
-	}
-	LOG("Build complete");
-
-	hashmap_free_keys(keys, n_keys);
-	return 0;
-}
-
 struct journal_entry *
 copy_journal_entry(const struct journal_entry *src)
 {
@@ -546,8 +515,9 @@ do_journal_entry_rename(const char *old, const char *new)
 		*dot = '\0';
 	}
 	LOG("Summary is now %s", new_summary);
+	tree_node *entry_node = find_node_by_path(fuse_tree_root, old);
+	struct journal_entry *old_entry = entry_node->data;
 
-	struct journal_entry *old_entry = get_entry_from_fuse_path(old);
 	if (!old_entry) {
 		LOG("Entry not found '%s'", old);
 		free(new_copy);
@@ -573,11 +543,8 @@ do_journal_entry_rename(const char *old, const char *new)
 	hashmap_insert(entries_original_ics, new_entry->filename_original,
 		       new_entry);
 
-	hashmap_insert(entries_fuse, new_entry->filename, new_entry);
-
-	// Insert original ics first to avoid use after free
-	// Insert frees older entry
-	hashmap_remove(entries_fuse, old_entry->filename);
+	entry_node->data = new_entry;
+	free_journal_entry(old_entry);
 
 	write_entry_to_ical_file(new_entry);
 	free(new_copy);
@@ -639,6 +606,88 @@ remove_file_extension(char *path)
 	}
 }
 
+bool
+match_by_uid(icalcomponent *component, const char *target_uuid)
+{
+	const char *uid = icalcomponent_get_uid(component);
+	return uid && strcmp(uid, target_uuid) == 0;
+	return false;
+}
+
+// TODO: Optimize
+tree_node *
+find_by_component_uuid(tree_node *root, const char *target_uuid)
+{
+	if (!root)
+		return NULL;
+
+	struct journal_entry *entry = root->data;
+	if (entry && entry->component &&
+	    match_by_uid(entry->component, target_uuid)) {
+		return root;
+	}
+
+	for (size_t i = 0; i < root->child_count; ++i) {
+		tree_node *found =
+		    find_by_component_uuid(root->children[i], target_uuid);
+		if (found)
+			return found;
+	}
+	return NULL;
+}
+
+// TODO: Handle duplicates in filename
+int
+create_entry_from_fuse(const char *filename)
+{
+	const char *entry_prefix = "/";
+	if (strncmp(filename, entry_prefix, strlen(entry_prefix)) != 0) {
+		return -EINVAL;
+	}
+	char *new_basename = strrchr(filename, '/');
+	new_basename++;
+
+	char *without_extension = strdup(new_basename);
+	remove_file_extension(without_extension);
+
+	struct journal_entry *new_entry =
+	    xcalloc(1, sizeof(struct journal_entry));
+	icalcomponent *vjournal_component =
+	    create_vjournal_entry(without_extension);
+
+	new_entry->component = vjournal_component;
+	new_entry->filename = strdup(new_basename);
+	if (asprintf(&new_entry->filename_original, "%s.ics",
+		     icalcomponent_get_uid(new_entry->component)) == -1) {
+		LOG("Failed to create journal entry");
+		free_journal_entry(new_entry);
+
+		return -ENOMEM;
+	}
+	LOG("Inserting vjournal entry");
+
+	hashmap_insert(entries_original_ics, new_entry->filename_original,
+		       new_entry);
+
+	add_child(fuse_tree_root, create_file_node(new_entry));
+
+	// Write back to the original
+	char *filepath = get_original_filepath(new_entry);
+	LOG("Got original filepath %s. From %s, and %s", filepath,
+	    new_entry->filename_original, new_entry->filename);
+
+	if (write_entry_to_ical_file(new_entry) != 0) {
+		LOG("Write to entry failed");
+		free(filepath);
+		return -EIO;
+	}
+
+	free(filepath);
+
+	LOG("File updated");
+	return 0;
+}
+
 void
 update_or_create_fuse_entry_from_original(const char *filename)
 {
@@ -653,22 +702,80 @@ update_or_create_fuse_entry_from_original(const char *filename)
 		free_journal_entry(new_entry);
 		return;
 	}
+	const char *target_uuid = icalcomponent_get_uid(new_entry->component);
+	if (!target_uuid) {
+		return;
+	}
 	hashmap_insert(entries_original_ics, new_entry->filename_original,
 		       new_entry);
-	hashmap_insert(entries_fuse, new_entry->filename, new_entry);
+
+	// TODO: Handle directories
+	tree_node *existing_node =
+	    find_by_component_uuid(fuse_tree_root, target_uuid);
+
+	// TODO: Enable subdirectories
+	if (existing_node) {
+		update_node_data(existing_node, new_entry);
+	}
+	else {
+
+		add_child(fuse_tree_root, create_file_node(new_entry));
+	}
 
 	LOG("File updated");
 }
 
+// Deletes original file too!
+int
+delete_from_fuse_path(const char *filepath)
+{
+	int res = 0;
+	tree_node *node = find_node_by_path(fuse_tree_root, filepath);
+	if (!node) {
+		return -EIO;
+	}
+	struct journal_entry *entry = get_entry(node);
+	const char *entry_uuid = icalcomponent_get_uid(entry->component);
+	char *filepath_original = get_original_filepath(entry);
+	if (!filepath_original) {
+		LOG("Entry not found");
+		return -EIO;
+	}
+	LOG("Deleting file %s", filepath);
+
+	res = remove(filepath);
+
+	hashmap_remove(entries_original_ics, entry_uuid);
+
+	// TODO: Subdirectories SHOULD NOT BE REMOVED HERE
+	// Instead, update the children so they have a new parent
+	// which is the parent node.
+	// Requires a change_parent function.
+	detach_tree_node(node);
+	free_tree(node);
+	return res;
+}
+
 int
-update_fuse_entry_delete(const char *file)
+update_delete_from_original_path(const char *filepath)
 {
 	// Strip path, validate just the new basename
-	const char *keyname = strrchr(file, '/');
+	const char *keyname = strrchr(filepath, '/');
 	keyname++;
-	LOG("Found entry to delete");
+	struct journal_entry *entry =
+	    hashmap_get(entries_original_ics, keyname);
+	if (!entry) {
+		return -EIO;
+	}
+
+	tree_node *node = find_by_component_uuid(fuse_tree_root, filepath);
+
+	// TODO: Subdirectories SHOULD NOT BE REMOVED HERE
+	// Instead, update the children so they have a new parent
+	// which is the parent node.
+	// Requires a change_parent function.
+	detach_tree_node(node);
+	free_tree(node);
 	hashmap_remove(entries_original_ics, keyname);
-	// Frees
-	hashmap_remove(entries_fuse, keyname);
 	return 0;
 }
diff --git a/journal_entry.h b/journal_entry.h
@@ -36,19 +36,11 @@ struct journal_entry {
 
 extern char ICS_DIR[256];
 
-// We create two maps that link to the same entries
-// journal_entry
-extern struct hashmap *entries_fuse;
+extern struct tree_node *fuse_tree_root;
 
 // journal_entry
 extern struct hashmap *entries_original_ics;
 
-// string -> string
-extern struct hashmap *entries_today;
-
-// string -> string
-extern struct hashmap *entries_notes;
-
 // Full filepath of original ics file
 char *
 get_original_filepath(const struct journal_entry *entry);
@@ -87,6 +79,22 @@ load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry);
 int
 parse_ics_to_journal_entry_component(const char *filename,
 				     struct journal_entry *entry);
+struct tree_node *
+find_node_by_path(struct tree_node *root, const char *path);
+
+struct journal_entry *
+get_entry(struct tree_node *node);
+
+bool
+entry_is_directory(struct tree_node *node);
+
+int
+delete_from_fuse_path(const char *filepath);
+
+int
+create_entry_from_fuse(const char *filename);
+int
+update_delete_from_original_path(const char *filepath);
 
 // Returns -1 on failure, 0 on success
 int
diff --git a/main.c b/main.c
@@ -1,3 +1,4 @@
+#include "tree.h"
 #define FUSE_USE_VERSION 31
 
 #include "hashmap.h"
@@ -40,59 +41,25 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 		stbuf->st_nlink = 2;
 		goto unlock_and_return;
 	}
-	else if (strcmp(path, "/entries") == 0) {
-		stbuf->st_mode = S_IFDIR | 0444;
-		stbuf->st_nlink = 2;
-		goto unlock_and_return;
-	}
-	else if (strcmp(path, "/today") == 0) {
-		stbuf->st_mode = S_IFDIR | 0444;
-		stbuf->st_nlink = 2;
+
+	// TODO: Handle directories
+	tree_node *node = find_node_by_path(fuse_tree_root, path);
+	if (!node) {
+		LOG("Entry not found");
+		ret_code = -ENOENT;
 		goto unlock_and_return;
 	}
-	else if (strcmp(path, "/notes") == 0) {
+
+	struct journal_entry *entry = get_entry(node);
+	if (entry_is_directory(node)) {
 		stbuf->st_mode = S_IFDIR | 0444;
 		stbuf->st_nlink = 2;
-		goto unlock_and_return;
 	}
-
-	const char *today_prefix = "/today/";
-	if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
-		const char *key = path + strlen(today_prefix);
-		char *entry_filename = hashmap_get(entries_today, key);
-		if (!entry_filename) {
-			ret_code = -ENOENT;
-			goto unlock_and_return;
-		}
-
-		stbuf->st_mode = S_IFLNK | 0444;
+	else {
+		stbuf->st_mode = S_IFREG | 0774;
 		stbuf->st_nlink = 1;
-		goto unlock_and_return;
 	}
 
-	const char *notes_prefix = "/notes/";
-	if (strncmp(path, notes_prefix, strlen(notes_prefix)) == 0) {
-		const char *key = path + strlen(notes_prefix);
-		char *entry_filename = hashmap_get(entries_notes, key);
-		if (!entry_filename) {
-			ret_code = -ENOENT;
-			goto unlock_and_return;
-		}
-
-		stbuf->st_mode = S_IFLNK | 0444;
-		stbuf->st_nlink = 1;
-		goto unlock_and_return;
-	}
-
-	struct journal_entry *entry = get_entry_from_fuse_path(path);
-	if (!entry) {
-		LOG("Entry not found");
-		ret_code = -ENOENT;
-		goto unlock_and_return;
-	}
-	stbuf->st_mode = S_IFREG | 0774;
-	stbuf->st_nlink = 1;
-
 	int size = get_entry_content_size(entry);
 
 	stbuf->st_size = (off_t)size;
@@ -117,49 +84,6 @@ int
 journal_readlink(const char *path, char *buf, size_t size)
 {
 	LOG("READLINK path: %s", path);
-	const char *today_prefix = "/today/";
-	if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
-		pthread_mutex_lock(&entries_mutex);
-		const char *key = path + strlen(today_prefix);
-		LOG("Lookup %s", key);
-		char *link = hashmap_get(entries_today, key);
-
-		if (!link) {
-			LOG("Link not found for %s", key);
-
-			pthread_mutex_unlock(&entries_mutex);
-			return -ENOENT;
-		}
-		LOG("Link found %s", link);
-
-		// Return symlink target
-		snprintf(buf, size, "%s", link);
-
-		pthread_mutex_unlock(&entries_mutex);
-		return 0;
-	}
-	const char *notes_prefix = "/notes/";
-	if (strncmp(path, notes_prefix, strlen(notes_prefix)) == 0) {
-		pthread_mutex_lock(&entries_mutex);
-		const char *key = path + strlen(notes_prefix);
-		LOG("Lookup %s", key);
-		char *link = hashmap_get(entries_notes, key);
-
-		if (!link) {
-			LOG("Link not found for %s", key);
-
-			pthread_mutex_unlock(&entries_mutex);
-			return -ENOENT;
-		}
-		LOG("Link found %s", link);
-
-		// Return symlink target
-		snprintf(buf, size, "%s", link);
-
-		pthread_mutex_unlock(&entries_mutex);
-		return 0;
-	}
-
 	return -ENOENT;
 }
 
@@ -175,58 +99,14 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 	filler(buf, "..", NULL, 0, 0);
 
 	// If we are in the root directory, expose the "journal" directory
+	// TODO: Subdirectories
 	if (strcmp(path, "/") == 0) {
-		filler(buf, "entries", NULL, 0, 0);
-		filler(buf, "today", NULL, 0, 0);
-		filler(buf, "notes", NULL, 0, 0);
-	}
-	else if (strcmp(path, "/entries") == 0) {
-		size_t n_keys = 0;
-		char **keys = hashmap_get_keys(entries_fuse, &n_keys);
-		if (!keys) {
-			pthread_mutex_unlock(&entries_mutex);
-			return -ENOMEM;
-		}
-
+		size_t n_keys = fuse_tree_root->child_count;
 		for (size_t i = 0; i < n_keys; i++) {
-			filler(buf, keys[i], NULL, 0, 0);
+			struct journal_entry *entry =
+			    fuse_tree_root->children[i]->data;
+			filler(buf, entry->filename, NULL, 0, 0);
 		}
-
-		hashmap_free_keys(keys, n_keys);
-	}
-	else if (strcmp(path, "/today") == 0) {
-		size_t n_keys = 0;
-		LOG("BUILDING TODAY HASHMAP");
-
-		build_today_hashmap();
-
-		LOG("GETTING KEYS");
-		char **keys = hashmap_get_keys(entries_today, &n_keys);
-		if (!keys) {
-			return -ENOMEM;
-		}
-		for (size_t i = 0; i < n_keys; i++) {
-			filler(buf, keys[i], NULL, 0, 0);
-		}
-
-		hashmap_free_keys(keys, n_keys);
-	}
-	else if (strcmp(path, "/notes") == 0) {
-		size_t n_keys = 0;
-		LOG("BUILDING NOTES HASHMAP");
-
-		build_notes_hashmap();
-
-		LOG("GETTING KEYS");
-		char **keys = hashmap_get_keys(entries_notes, &n_keys);
-		if (!keys) {
-			return -ENOMEM;
-		}
-		for (size_t i = 0; i < n_keys; i++) {
-			filler(buf, keys[i], NULL, 0, 0);
-		}
-
-		hashmap_free_keys(keys, n_keys);
 	}
 	pthread_mutex_unlock(&entries_mutex);
 	return 0;
@@ -326,40 +206,13 @@ journal_unlink(const char *file)
 	LOG("DELETE %s", file);
 	int res = 0;
 
-	// Strip path, validate just the new basename
-	const char *keyname = strrchr(file, '/');
-	keyname++;
-
-	struct journal_entry *entry = hashmap_get(entries_fuse, keyname);
-	if (!entry) {
-		LOG("Entry does not exist");
-		pthread_mutex_unlock(&entries_mutex);
-		return -EIO;
-	}
-	LOG("Found entry to delete");
-
-	char *filepath = get_original_filepath(entry);
-	if (!filepath) {
-		LOG("Entry not found");
-		pthread_mutex_unlock(&entries_mutex);
-		return -EIO;
-	}
-	LOG("Deleting file %s", filepath);
-
-	res = remove(filepath);
+	res = delete_from_fuse_path(file);
 	if (res != 0) {
 		LOG("Failed to delete file");
 		pthread_mutex_unlock(&entries_mutex);
-
-		free(filepath);
 		return -EIO;
 	}
 
-	hashmap_remove(entries_original_ics, keyname);
-	// Frees
-	hashmap_remove(entries_fuse, keyname);
-	free(filepath);
-
 	LOG("Delete successful");
 	pthread_mutex_unlock(&entries_mutex);
 	return 0;
@@ -370,13 +223,6 @@ journal_rename(const char *old, const char *new, unsigned int flags)
 {
 	pthread_mutex_lock(&entries_mutex);
 	LOG("RENAME %s -> %s", old, new);
-	if (strncmp(old, "/entries/", strlen("/entries/")) != 0 ||
-	    strncmp(new, "/entries/", strlen("/entries/")) != 0) {
-
-		pthread_mutex_unlock(&entries_mutex);
-		LOG("Invalid file names");
-		return -ENOTSUP;
-	}
 	int res = do_journal_entry_rename(old, new);
 
 	pthread_mutex_unlock(&entries_mutex);
@@ -388,48 +234,8 @@ journal_create(const char *filename, mode_t mode, struct fuse_file_info *info)
 {
 	pthread_mutex_lock(&entries_mutex);
 	LOG("CREATE %s", filename);
-
-	const char *entry_prefix = "/entries/";
-	if (strncmp(filename, entry_prefix, strlen(entry_prefix)) != 0) {
-		return -EINVAL;
-	}
-	char *new_basename = strrchr(filename, '/');
-	new_basename++;
-	char *without_extension = strdup(new_basename);
-	remove_file_extension(without_extension);
-
-	struct journal_entry *new_entry =
-	    xcalloc(1, sizeof(struct journal_entry));
-	icalcomponent *vjournal_component =
-	    create_vjournal_entry(without_extension);
-
-	new_entry->component = vjournal_component;
-	new_entry->filename = strdup(new_basename);
-	if (asprintf(&new_entry->filename_original, "%s.ics",
-		     icalcomponent_get_uid(new_entry->component)) == -1) {
-		LOG("Failed to create journal entry");
-		free_journal_entry(new_entry);
-		return -ENOMEM;
-	}
-	LOG("Inserting vjournal entry");
-
-	hashmap_insert(entries_original_ics, new_entry->filename_original,
-		       new_entry);
-	hashmap_insert(entries_fuse, new_entry->filename, new_entry);
-
-	// Write back to the original
-	char *filepath = get_original_filepath(new_entry);
-	LOG("Got original filepath %s. From %s, and %s", filepath,
-	    new_entry->filename_original, new_entry->filename);
-
-	if (write_entry_to_ical_file(new_entry) != 0) {
-		LOG("Write to entry failed");
-		free(filepath);
-		return -EIO;
-	}
-
-	free(filepath);
-
+	// TODO: Handle mode and fuse_file_info
+	create_entry_from_fuse(filename);
 	pthread_mutex_unlock(&entries_mutex);
 	return 0;
 }
@@ -480,7 +286,8 @@ watch_ics_dir(void *arg)
 
 				if (event->mask & IN_DELETE) {
 					pthread_mutex_lock(&entries_mutex);
-					update_fuse_entry_delete(full_path);
+					update_delete_from_original_path(
+					    full_path);
 					pthread_mutex_unlock(&entries_mutex);
 				}
 				else if (event->mask & IN_MODIFY) {
@@ -534,6 +341,8 @@ main(int argc, char *argv[])
 
 	tzset();
 
+	fuse_tree_root = create_tree_node(NULL, NULL);
+
 	if (load_caldavfs_environment() != 0) {
 		fprintf(stderr, "Failed to load ICS directory\n");
 		exit(1);
@@ -552,10 +361,8 @@ main(int argc, char *argv[])
 	pthread_join(watcher_thread, NULL); // Clean up
 	LOG("Pthread freed");
 
-	hashmap_free(entries_fuse);
 	hashmap_free(entries_original_ics);
-	hashmap_free(entries_today);
-	hashmap_free(entries_notes);
+	free_tree(fuse_tree_root);
 	LOG("Hashmap freed");
 
 	LOG("Regexp freed");
diff --git a/sample_ics/2025-01-01.ics b/sample_ics/2025-01-01.ics
@@ -1,8 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VJOURNAL
-UID:20250101T120000Z-001@host.com
-DTSTAMP:20250101T120000Z
-SUMMARY:New Year's Day Reflections
-DESCRIPTION:the past year and setting goals for the new one HELLO.\n\nHi world\n\n\n
-END:VJOURNAL
-END:VCA
diff --git a/tree.c b/tree.c
@@ -0,0 +1,106 @@
+
+#include "tree.h"
+#include "util.h"
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+
+tree_node *
+create_tree_node(void *data, void (*free_fn)(void *))
+{
+	tree_node *node = xmalloc(sizeof(tree_node));
+	if (!node)
+		return NULL;
+	node->data = data;
+	node->parent = NULL;
+	node->children = NULL;
+	node->child_count = 0;
+	node->child_capacity = 0;
+	node->free_fn = free_fn;
+
+	return node;
+}
+
+size_t
+add_child(tree_node *parent, tree_node *child)
+{
+	if (parent->child_count == parent->child_capacity) {
+		size_t new_capacity =
+		    parent->child_capacity ? parent->child_capacity * 2 : 4;
+
+		parent->children = xreallocarray(parent->children, new_capacity,
+						 sizeof(tree_node *));
+		parent->child_capacity = new_capacity;
+	}
+	parent->children[parent->child_count++] = child;
+	child->parent = parent;
+	return 0;
+}
+void
+update_node_data(tree_node *node, void *data)
+{
+	if (node->data && node->free_fn) {
+		node->free_fn(node->data);
+	}
+	node->data = data;
+}
+
+// Free a node and all its children
+void
+free_tree(tree_node *node)
+{
+	if (!node)
+		return;
+	for (size_t i = 0; i < node->child_count; ++i) {
+		free_tree(node->children[i]);
+	}
+	if (node->free_fn && node->data) {
+		node->free_fn(node->data);
+	}
+	free(node->children);
+	free(node);
+}
+
+void
+print_tree(tree_node *node, int depth, void (*print_data)(void *))
+{
+	for (int i = 0; i < depth; i++)
+		printf("  ");
+	if (print_data)
+		print_data(node->data);
+	else
+		printf("(no data)\n");
+
+	for (size_t i = 0; i < node->child_count; ++i) {
+		print_tree(node->children[i], depth + 1, print_data);
+	}
+}
+
+bool
+detach_tree_node(tree_node *node)
+{
+	if (!node || !node->parent)
+		return false;
+
+	tree_node *parent = node->parent;
+
+	// Find index of this node in parent->children
+	size_t index = (size_t)-1;
+	for (size_t i = 0; i < parent->child_count; ++i) {
+		if (parent->children[i] == node) {
+			index = i;
+			break;
+		}
+	}
+	if (index == (size_t)-1)
+		return false; // not found, shouldn't happen
+
+	// Shift remaining children to fill the gap
+	for (size_t i = index + 1; i < parent->child_count; ++i) {
+		parent->children[i - 1] = parent->children[i];
+	}
+	parent->child_count--;
+
+	node->parent = NULL;
+	return true;
+}
diff --git a/tree.h b/tree.h
@@ -0,0 +1,38 @@
+#ifndef tree_h
+#define tree_h
+
+#include <stdbool.h>
+#include <stdlib.h>
+
+typedef struct tree_node {
+	void *data;
+
+	struct tree_node *parent;
+	struct tree_node **children;
+	size_t child_count;
+	size_t child_capacity;
+
+	void (*free_fn)(void *);
+} tree_node;
+
+tree_node *
+create_tree_node(void *data, void (*free_fn)(void *));
+
+void
+update_node_data(tree_node *node, void *data);
+
+// detach from parent
+bool
+detach_tree_node(tree_node *node);
+
+size_t
+add_child(tree_node *parent, tree_node *child);
+
+void
+free_tree(tree_node *node);
+
+void
+print_tree(tree_node *node, int depth, void (*print_data)(void *));
+
+#endif
+