agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 0e1a87120d934b61c81efe2a865cf1931875e167
parent 2cc5c4798900180da0cf7fc5a98e62d039db0a51
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 28 Jul 2025 11:53:31 +0200

Bugfixes; refactor; handle duplicate names

Closes: https://todo.sr.ht/~marcc/agendafs/8

Diffstat:
Mfuse_node.c | 62+++++++++++---------------------------------------------------
Mfuse_node.h | 5+----
Mfuse_node_store.c | 94++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mfuse_node_store.h | 9++++++---
Mical_extra.c | 13++++++++-----
Mmain.c | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mpath.c | 28++++++++++++++++++++++++++++
Mpath.h | 3+++
8 files changed, 192 insertions(+), 129 deletions(-)
diff --git a/fuse_node.c b/fuse_node.c
@@ -69,6 +69,9 @@ get_node_by_path(memory_region *mreg, const char *path)
 				break;
 			}
 		}
+		if (next == NULL) {
+			return NULL;
+		}
 		current = next;
 	}
 	LOG("Done");
@@ -266,7 +269,7 @@ load_root_node_tree()
 			struct tree_node *parent =
 			    get_node_by_uuid(mreg, parent_uid);
 			if (parent) {
-				move_node(parent, child);
+				move_fuse_node(parent, child);
 			}
 			else {
 				LOG("COULD NOT FIND PARENT NODE: %s",
@@ -274,7 +277,7 @@ load_root_node_tree()
 			}
 		}
 		else {
-			add_child(fuse_root, child);
+			add_fuse_child(fuse_root, child);
 		}
 	}
 	LOG("Inserted %zu entries", n_keys);
@@ -522,14 +525,11 @@ insert_fuse_node_to_path(memory_region *mreg, const char *fuse_path,
 int
 create_entry_from_fuse(memory_region *mreg, const char *fuse_path)
 {
-	// TODO: We are mixing some responsibilities here.
-	// Any function that operates on the global state should not
-	// have memory_region as a parameter, as that makes it easier
-	// to mix up the contexts.
 	const char *entry_prefix = "/";
 	if (strncmp(fuse_path, entry_prefix, strlen(entry_prefix)) != 0) {
 		return -EINVAL;
 	}
+
 	const char *new_filename = get_filename(fuse_path);
 	icalcomponent *new_component =
 	    create_vjournal_entry(mreg, new_filename);
@@ -638,41 +638,6 @@ update_or_create_fuse_entry_from_vdir(memory_region *mreg,
 }
 
 int
-delete_dir_from_fuse_path(memory_region *mreg, const char *filepath)
-{
-	struct tree_node *node = get_node_by_path(mreg, filepath);
-	if (!node) {
-		return -EIO;
-	}
-
-	icalcomponent *node_ics = get_icalcomponent_from_node(mreg, node);
-	if (!node_is_directory(mreg, node, node_ics)) {
-		return -ENOTDIR;
-	}
-
-	if (node->child_count > 0) {
-		return -ENOTEMPTY;
-	}
-
-	const char *filepath_original = get_vdir_filepath(mreg, node);
-	if (!filepath_original) {
-		LOG("Entry not found");
-		return -EIO;
-	}
-	LOG("Deleting file %s, located at %s", filepath, filepath_original);
-
-	int res = remove(filepath_original);
-	if (res != 0) {
-		LOG("Failed to delete file");
-		return -res;
-	}
-
-	delete_fuse_node(node);
-
-	return 0;
-}
-
-int
 do_agenda_rename(memory_region *mreg, const char *old, const char *new)
 {
 	char *new_copy = rstrdup(mreg, new);
@@ -691,6 +656,7 @@ do_agenda_rename(memory_region *mreg, const char *old, const char *new)
 
 	struct tree_node *child_node = get_node_by_path(mreg, old);
 	assert(child_node);
+
 	icalcomponent *child_ics =
 	    get_icalcomponent_from_node(mreg, child_node);
 	assert(child_ics);
@@ -723,21 +689,13 @@ do_agenda_rename(memory_region *mreg, const char *old, const char *new)
 }
 
 int
-delete_from_fuse_path(memory_region *mreg, const char *filepath)
+delete_vdir_entry(memory_region *mreg, struct tree_node *node)
 {
 	int res = 0;
-	struct tree_node *node = get_node_by_path(mreg, filepath);
-	if (!node) {
-		return -ENOENT;
-	}
 
-	icalcomponent *node_ics = get_icalcomponent_from_node(mreg, node);
-	if (node_is_directory(mreg, node, node_ics)) {
-		return -EISDIR;
-	}
 	const char *filepath_original = get_vdir_filepath(mreg, node);
 
-	LOG("Deleting file %s, located at %s", filepath, filepath_original);
+	LOG("Deleting file %s", filepath_original);
 
 	res = remove(filepath_original);
 	if (res == -1) {
@@ -757,6 +715,8 @@ delete_from_vdir_path(memory_region *mreg, const char *filepath)
 		return -ENOENT;
 	}
 
+	LOG("Found node to delete");
+
 	// TODO: Subdirectories SHOULD NOT BE REMOVED HERE
 	// Instead, update the children so they have a new parent
 	// which is the parent node.
diff --git a/fuse_node.h b/fuse_node.h
@@ -112,10 +112,7 @@ int
 do_agenda_rename(memory_region *mreg, const char *, const char *);
 
 int
-delete_from_fuse_path(memory_region *mreg, const char *filepath);
-
-char *
-get_fuse_path_from_vdir(memory_region *mreg, const char *filepath);
+delete_vdir_entry(memory_region *mreg, struct tree_node *node);
 
 int
 delete_from_vdir_path(memory_region *mreg, const char *filepath);
diff --git a/fuse_node_store.c b/fuse_node_store.c
@@ -1,5 +1,6 @@
 #include "agenda_entry.h"
 #include "hashmap.h"
+#include "path.h"
 #include "tree.h"
 #include "util.h"
 #include <stddef.h>
@@ -41,6 +42,13 @@ get_entry(const struct tree_node *node)
 	return node->data;
 }
 
+const char *
+oget_node_filename(struct tree_node *node)
+{
+	const struct agenda_entry *entry = get_entry(node);
+	return entry->filename;
+}
+
 bool
 is_root_node(const struct tree_node *node)
 {
@@ -57,50 +65,6 @@ get_node_filename(memory_region *mreg, const struct tree_node *node)
 }
 
 const char *
-get_node_path(memory_region *mreg, struct tree_node *node)
-{
-	if (!node->parent) {
-		return "/";
-	}
-
-	struct tree_node *curr = node;
-	char *result = NULL;
-	size_t result_len = 0;
-
-	FILE *stream = open_memstream(&result, &result_len);
-	assert(stream);
-
-	do {
-		if (!is_root_node(curr)) {
-			const char *filename = get_node_filename(mreg, curr);
-			fprintf(stream, "/%s", filename);
-		}
-
-		curr = curr->parent;
-
-	} while (curr->parent);
-
-	fclose(stream);
-	if (result) {
-		region_register(mreg, result, free);
-	}
-
-	return result;
-}
-
-const char *
-get_fuse_path_from_vdir(memory_region *mreg, const char *filepath_vdir)
-{
-	struct tree_node *node =
-	    hashmap_get(entries_vdir, get_filename(filepath_vdir));
-
-	if (node) {
-		return get_node_path(mreg, node);
-	}
-	return NULL;
-}
-
-const char *
 get_vdir_filepath(memory_region *mreg, const struct tree_node *node)
 {
 	const struct agenda_entry *entry = get_entry(node);
@@ -145,3 +109,45 @@ upsert_fuse_node(const struct agenda_entry *entry)
 	return node;
 }
 
+// Add fuse child to node.
+// OBS: Modifies filename if not unique!
+size_t
+add_fuse_child(struct tree_node *parent, struct tree_node *child)
+{
+	const char *filename = oget_node_filename(child);
+
+	char *new_filename = xstrdup(filename);
+
+	size_t conflict_count = 1;
+
+	while (true) {
+		bool is_unique = true;
+		for (size_t i = 0; i < parent->child_count; i++) {
+			const char *sibling_filename =
+			    oget_node_filename(parent->children[i]);
+
+			if (strcmp(new_filename, sibling_filename) == 0) {
+				is_unique = false;
+				break;
+			}
+		}
+
+		if (is_unique) {
+			break;
+		}
+
+		free(new_filename);
+		new_filename = filename_numbered(filename, conflict_count++);
+	}
+
+	set_node_filename(child, new_filename);
+
+	return add_child(parent, child);
+}
+
+size_t
+move_fuse_node(struct tree_node *new_parent, struct tree_node *child)
+{
+	detach_tree_node(child);
+	return add_fuse_child(new_parent, child);
+}
diff --git a/fuse_node_store.h b/fuse_node_store.h
@@ -35,9 +35,6 @@ is_root_node(const struct tree_node *node);
 const char *
 get_node_filename(memory_region *mreg, const struct tree_node *node);
 
-const char *
-get_node_path(memory_region *mreg, struct tree_node *node);
-
 int
 set_node_filename(struct tree_node *node, const char *filename);
 
@@ -50,4 +47,10 @@ delete_fuse_node(struct tree_node *node);
 struct tree_node *
 upsert_fuse_node(const struct agenda_entry *entry);
 
+size_t
+add_fuse_child(struct tree_node *parent, struct tree_node *child);
+
+size_t
+move_fuse_node(struct tree_node *new_parent, struct tree_node *child);
+
 #endif // fuse_node_store_h_INCLUDED
diff --git a/ical_extra.c b/ical_extra.c
@@ -119,11 +119,14 @@ get_ical_now()
 size_t
 get_entry_content_size(icalcomponent *component)
 {
-	size_t size =
-	    snprintf(NULL, 0, "%s", icalcomponent_get_description(component));
-
-	assert(size != -1);
-	return size;
+	const char *description = icalcomponent_get_description(component);
+	if (!description) {
+		return 0;
+	}
+	else {
+		size_t size = strlen(description);
+		return size;
+	}
 }
 
 char *
diff --git a/main.c b/main.c
@@ -131,6 +131,7 @@ fuse_open(const char *path, struct fuse_file_info *fi)
 
 	const struct tree_node *node = get_node_by_path(mreg, path);
 	if (!node) {
+		LOG("Entry not found");
 		status = -ENOENT;
 	}
 
@@ -160,44 +161,47 @@ static int
 fuse_read(const char *path, char *buf, size_t size, off_t offset,
 	  struct fuse_file_info *fi)
 {
-	LOG("READ %s", path);
-
+	LOG("READ called: offset=%ld, size=%zu", offset, size);
 	memory_region *mreg = create_region();
 	pthread_mutex_lock(&entries_mutex);
-	int ret_code = 0;
+	int ret_i = 0;
 
 	const struct tree_node *n = get_node_by_path(mreg, path);
 	if (!n) {
-		ret_code = -ENOENT;
+		ret_i = -ENOENT;
 		goto cleanup_return;
 	}
 	icalcomponent *ic = get_icalcomponent_from_node(mreg, n);
+	if (!ic) {
+		ret_i = -EIO;
+		goto cleanup_return;
+	}
+
 	const char *description = icalcomponent_get_description(ic);
 	if (!description) {
-		description = "";
+		goto cleanup_return;
 	}
 
 	size_t content_len = strlen(description);
 
 	if (offset >= content_len) {
-		ret_code = 0;
 		goto cleanup_return;
 	}
 
-	if (offset + size > content_len) {
-		size = content_len - offset;
+	size_t bytes_to_read = content_len - offset;
+	if (bytes_to_read > size) {
+		bytes_to_read = size;
 	}
-
-	if (size > 0) {
-		memcpy(buf, description + offset, size);
+	if (bytes_to_read > 0) {
+		memcpy(buf, description + offset, bytes_to_read);
 	}
 
-	ret_code = size;
+	ret_i = bytes_to_read;
 
 cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
 	rfree_all(mreg);
-	return ret_code;
+	return ret_i;
 }
 
 static int
@@ -249,24 +253,54 @@ fuse_unlink(const char *file)
 	pthread_mutex_lock(&entries_mutex);
 	int res = 0;
 
-	res = delete_from_fuse_path(mreg, file);
+	struct tree_node *node = get_node_by_path(mreg, file);
+	if (!node) {
+		res = -ENOENT;
+		goto cleanup_return;
+	}
+
+	icalcomponent *node_ics = get_icalcomponent_from_node(mreg, node);
+	if (node_is_directory(mreg, node, node_ics)) {
+		res = -EISDIR;
+		goto cleanup_return;
+	}
+
+	res = delete_vdir_entry(mreg, node);
 
+cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
 	rfree_all(mreg);
 	return res;
 }
 
 static int
-fuse_rmdir(const char *file)
+fuse_rmdir(const char *filepath)
 {
-	LOG("DELETE %s", file);
+	LOG("DELETE %s", filepath);
 
 	memory_region *mreg = create_region();
 	pthread_mutex_lock(&entries_mutex);
 	int res = 0;
 
-	res = delete_dir_from_fuse_path(mreg, file);
+	struct tree_node *node = get_node_by_path(mreg, filepath);
+	if (!node) {
+		res = -ENOENT;
+		goto cleanup_return;
+	}
+
+	icalcomponent *node_ics = get_icalcomponent_from_node(mreg, node);
+	if (!node_is_directory(mreg, node, node_ics)) {
+		res = -ENOTDIR;
+		goto cleanup_return;
+	}
+
+	if (node->child_count > 0) {
+		return -ENOTEMPTY;
+	}
 
+	res = delete_vdir_entry(mreg, node);
+
+cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
 	rfree_all(mreg);
 	return res;
@@ -279,28 +313,57 @@ fuse_rename(const char *old, const char *new, unsigned int flags)
 	memory_region *mreg = create_region();
 	pthread_mutex_lock(&entries_mutex);
 	LOG("RENAME %s -> %s", old, new);
+	int res = 0;
+
+	struct tree_node *existing_node = get_node_by_path(mreg, new);
+	if (existing_node && node_has_children(existing_node)) {
+		res = -ENOTEMPTY;
+		goto cleanup_return;
+	}
+
+	if (existing_node) {
+		if (delete_vdir_entry(mreg, existing_node) != 0) {
+			res = -EIO;
+			goto cleanup_return;
+		}
+	}
 
-	int res = do_agenda_rename(mreg, old, new);
+	res = do_agenda_rename(mreg, old, new);
 
+cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
 	rfree_all(mreg);
 	return res;
 }
 
 static int
-fuse_create(const char *filename, mode_t mode, struct fuse_file_info *info)
+fuse_create(const char *filepath, mode_t mode, struct fuse_file_info *info)
 {
 
 	memory_region *mreg = create_region();
 	pthread_mutex_lock(&entries_mutex);
-	LOG("CREATE %s", filename);
+	LOG("CREATE %s", filepath);
+
+	int status = 0;
+
+	if (pathIsHidden(filepath)) {
+		status = -EPERM;
+		goto cleanup_return;
+	}
+
+	struct tree_node *node = get_node_by_path(mreg, filepath);
+	if (node) {
+		status = -EEXIST;
+		goto cleanup_return;
+	}
 
 	// TODO: Handle mode and fuse_file_info
-	create_entry_from_fuse(mreg, filename);
+	create_entry_from_fuse(mreg, filepath);
 
+cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
 	rfree_all(mreg);
-	return 0;
+	return status;
 }
 
 static int
diff --git a/path.c b/path.c
@@ -103,3 +103,31 @@ write_to_file(const path *filepath, const char *content)
 	fclose(f);
 	return 0;
 }
+
+// Creates a new path
+// my-file.txt -> my-file.1.txt
+// Caller is responsible for freeing memory
+path *
+filename_numbered(const char *filename, size_t n)
+{
+	const char *ext = strrchr(filename, '.');
+	char *new_filename;
+	size_t len = 0;
+
+	FILE *stream = open_memstream(&new_filename, &len);
+	assert(stream);
+
+	if (ext) {
+		size_t base_len = ext - filename;
+		fwrite(filename, 1, base_len, stream);
+		fprintf(stream, ".%zu%s", n, ext);
+	}
+	else {
+		fprintf(stream, "%s.%zu", filename, n);
+	}
+
+	fclose(stream);
+
+	return new_filename;
+}
+
diff --git a/path.h b/path.h
@@ -35,4 +35,7 @@ split_path(memory_region *m, const path *p, char **segments);
 size_t
 write_to_file(const path *filepath, const char *content);
 
+path *
+filename_numbered(const char *filename, size_t n);
+
 #endif // path_h_INCLUDED