agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 698eb179ffe9bd84766044e28267970695e9765c
parent 0e1a87120d934b61c81efe2a865cf1931875e167
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 28 Jul 2025 12:40:25 +0200

Add file extension support

This solution makes it necessary to write to the ics files that
hey this is actually a directory. That requires actually having
write access which I hope is the case.

Implements: https://todo.sr.ht/~marcc/agendafs/5

Diffstat:
Mfuse_node.c | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mfuse_node_store.c | 4++--
Mfuse_node_store.h | 2+-
Mical_extra.c | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mical_extra.h | 10++++++++++
Mmain.c | 3+--
Mpath.c | 3+++
7 files changed, 141 insertions(+), 13 deletions(-)
diff --git a/fuse_node.c b/fuse_node.c
@@ -62,7 +62,7 @@ get_node_by_path(memory_region *mreg, const char *path)
 		for (size_t j = 0; j < current->child_count; ++j) {
 
 			const char *child_name =
-			    get_node_filename(mreg, current->children[j]);
+			    get_node_filename(current->children[j]);
 			if (strcmp(child_name, segment) == 0) {
 
 				next = current->children[j];
@@ -158,8 +158,27 @@ int
 update_summary(memory_region *mreg, icalcomponent *ics,
 	       const struct tree_node *node)
 {
-	const struct agenda_entry *e = get_entry(node);
-	icalcomponent_set_summary(ics, e->filename);
+	if (is_directory_component(ics)) {
+		icalcomponent_set_summary(ics, get_node_filename(node));
+	}
+	else {
+		char *filename_no_ext =
+		    without_file_extension(mreg, get_node_filename(node));
+		icalcomponent_set_summary(ics, filename_no_ext);
+	}
+
+	return write_ical_file(mreg, node, ics);
+}
+
+int
+update_node_file_extension(memory_region *mreg, icalcomponent *ics,
+			   const struct tree_node *node)
+{
+	const char *ext = get_file_extension(mreg, get_node_filename(node));
+	if (!ext) {
+		return 0;
+	}
+	icalcomponent_set_file_extension(ics, ext);
 	return write_ical_file(mreg, node, ics);
 }
 
@@ -179,14 +198,28 @@ parse_ics_to_agenda_entry(memory_region *mreg, const char *vdir_filepath)
 		LOG("Not journal component");
 		return NULL;
 	}
-	if (icalcomponent_get_summary(component) == NULL) {
+	const char *summary = icalcomponent_get_summary(component);
+	if (summary == NULL) {
 		LOG("No summary found");
 		return NULL;
 	}
 
+	char *filename = NULL;
+	if (is_directory_component(component)) {
+		filename = rstrdup(mreg, summary);
+	}
+	else {
+		const char *extension =
+		    icalcomponent_get_file_extension(component);
+		if (!extension) {
+			extension = "txt";
+		}
+
+		rasprintf(mreg, &filename, "%s.%s", summary, extension);
+	}
+
 	struct agenda_entry *e =
-	    create_agenda_entry(mreg, icalcomponent_get_summary(component),
-				get_filename(vdir_filepath));
+	    create_agenda_entry(mreg, filename, get_filename(vdir_filepath));
 
 	return e;
 }
@@ -269,6 +302,15 @@ load_root_node_tree()
 			struct tree_node *parent =
 			    get_node_by_uuid(mreg, parent_uid);
 			if (parent) {
+
+				icalcomponent *pic =
+				    get_icalcomponent_from_node(mreg, parent);
+				if (!is_directory_component(pic)) {
+					icalcomponent_mark_as_directory(pic);
+					assert(write_ical_file(mreg, parent,
+							       pic) == 0);
+				}
+
 				move_fuse_node(parent, child);
 			}
 			else {
@@ -626,11 +668,18 @@ update_or_create_fuse_entry_from_vdir(memory_region *mreg,
 		LOG("Has parent");
 		struct tree_node *new_parent =
 		    get_node_by_uuid(mreg, new_parent_uid);
-		move_node(new_parent, node);
+		move_fuse_node(new_parent, node);
+
+		icalcomponent *pic =
+		    get_icalcomponent_from_node(mreg, new_parent);
+		if (!is_directory_component(pic)) {
+			icalcomponent_mark_as_directory(pic);
+			assert(write_ical_file(mreg, new_parent, pic) == 0);
+		}
 	}
 	else {
 		LOG("Does not have parent, adding to root");
-		move_node(fuse_root, node);
+		move_fuse_node(fuse_root, node);
 	}
 
 	LOG("Tree updated");
@@ -670,6 +719,7 @@ do_agenda_rename(memory_region *mreg, const char *old, const char *new)
 
 	set_node_filename(child_node, new_filename);
 	update_summary(mreg, child_ics, child_node);
+	update_node_file_extension(mreg, child_ics, child_node);
 
 	if (old_parent_node && old_parent_ics) {
 		remove_parent_child_relationship_from_component(old_parent_ics,
diff --git a/fuse_node_store.c b/fuse_node_store.c
@@ -56,12 +56,12 @@ is_root_node(const struct tree_node *node)
 }
 
 const char *
-get_node_filename(memory_region *mreg, const struct tree_node *node)
+get_node_filename(const struct tree_node *node)
 {
 	struct agenda_entry *entry = node->data;
 	if (entry == NULL)
 		return "";
-	return rstrdup(mreg, entry->filename);
+	return entry->filename;
 }
 
 const char *
diff --git a/fuse_node_store.h b/fuse_node_store.h
@@ -33,7 +33,7 @@ bool
 is_root_node(const struct tree_node *node);
 
 const char *
-get_node_filename(memory_region *mreg, const struct tree_node *node);
+get_node_filename(const struct tree_node *node);
 
 int
 set_node_filename(struct tree_node *node, const char *filename);
diff --git a/ical_extra.c b/ical_extra.c
@@ -4,6 +4,7 @@
 #include "uuid/uuid.h"
 #include <stdbool.h>
 const char *IS_DIRECTORY_PROPERTY = "X-CALDAVFS-ISDIRECTORY";
+const char *FILE_EXTENSION_PROPERTY = "X-CALDAVFS-FILEEXT";
 
 icaltimetype
 get_last_modified(icalcomponent *component)
@@ -172,6 +173,16 @@ create_vjournal_directory(memory_region *mreg, const char *summary)
 	return calendar;
 }
 
+void
+icalcomponent_mark_as_directory(icalcomponent *ic)
+{
+	icalcomponent *inner = icalcomponent_get_inner(ic);
+	icalproperty *is_directory = icalproperty_new_x(IS_DIRECTORY_PROPERTY);
+	icalproperty_set_x_name(is_directory, IS_DIRECTORY_PROPERTY);
+	icalproperty_set_x(is_directory, "YES");
+	icalcomponent_add_property(inner, is_directory);
+}
+
 icalcomponent *
 create_vjournal_entry(memory_region *mreg, const char *summary)
 {
@@ -280,3 +291,58 @@ icalcomponent_insert_description(memory_region *mreg, icalcomponent *ic,
 	icalcomponent_set_description(ic, new_desc);
 }
 
+void
+icalcomponent_set_file_extension(icalcomponent *component,
+				 const char *extension)
+{
+	icalcomponent *inner =
+	    icalcomponent_get_first_component(component, ICAL_ANY_COMPONENT);
+	assert(inner);
+
+	icalproperty *prop_iter =
+	    icalcomponent_get_first_property(inner, ICAL_X_PROPERTY);
+	while (prop_iter != NULL) {
+		icalproperty *next_prop =
+		    icalcomponent_get_next_property(inner, ICAL_X_PROPERTY);
+		const char *prop_name = icalproperty_get_x_name(prop_iter);
+		if (prop_name &&
+		    strcmp(prop_name, FILE_EXTENSION_PROPERTY) == 0) {
+			icalcomponent_remove_property(inner, prop_iter);
+			icalproperty_free(prop_iter);
+			break;
+		}
+		prop_iter = next_prop;
+	}
+
+	icalproperty *fileext_prop =
+	    icalproperty_new_x(FILE_EXTENSION_PROPERTY);
+
+	icalproperty_set_x_name(fileext_prop, FILE_EXTENSION_PROPERTY);
+	icalproperty_set_x(fileext_prop, extension);
+
+	icalcomponent_add_property(inner, fileext_prop);
+	LOG("Set X-CALDAVFS-FILEEXT to '%s' on component.", extension);
+}
+
+const char *
+icalcomponent_get_file_extension(icalcomponent *component)
+{
+	icalcomponent *inner =
+	    icalcomponent_get_first_component(component, ICAL_ANY_COMPONENT);
+	if (!inner) {
+		return NULL;
+	}
+
+	for (icalproperty *prop =
+		 icalcomponent_get_first_property(inner, ICAL_X_PROPERTY);
+	     prop != NULL;
+	     prop = icalcomponent_get_next_property(inner, ICAL_X_PROPERTY)) {
+
+		const char *prop_name = icalproperty_get_x_name(prop);
+		if (prop_name &&
+		    strcmp(prop_name, FILE_EXTENSION_PROPERTY) == 0) {
+			return icalproperty_get_value_as_string(prop);
+		}
+	}
+	return NULL;
+}
diff --git a/ical_extra.h b/ical_extra.h
@@ -51,4 +51,14 @@ format_ical_class(const enum icalproperty_class iclass);
 void
 icalcomponent_insert_description(memory_region *mreg, icalcomponent *ic,
 				 const char *buf, size_t size, off_t offset);
+
+void
+icalcomponent_set_file_extension(icalcomponent *component,
+				 const char *extension);
+
+const char *
+icalcomponent_get_file_extension(icalcomponent *component);
+
+void
+icalcomponent_mark_as_directory(icalcomponent *ic);
 #endif // ical_extra_h_INCLUDED
diff --git a/main.c b/main.c
@@ -107,8 +107,7 @@ fuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset,
 
 		struct stat st = get_node_stat(mreg, node, ic);
 
-		if (filler(buf, get_node_filename(mreg, child), &st, 0, 0) !=
-		    0) {
+		if (filler(buf, get_node_filename(child), &st, 0, 0) != 0) {
 			status = -ENOMEM;
 			goto cleanup_return;
 		}
diff --git a/path.c b/path.c
@@ -26,6 +26,9 @@ get_file_extension(memory_region *m, const path *p)
 {
 	char *cpy = rstrdup(m, p);
 	char *last_dot = strrchr(cpy, '.');
+	if (last_dot) {
+		last_dot++;
+	}
 
 	return last_dot;
 }