agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit de45ebdb7c33f7439ef1c86929f40e2545d6bdae
parent 0ece7056243b3754af702545dd53a0013aeb72dd
Author: Marc Coquand <marc@coquand.email>
Date:   Thu, 26 Jun 2025 11:12:38 +0100

Broken directory support

Diffstat:
Mjournal_entry.c | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mjournal_entry.h | 6++++++
Mmain.c | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++------
3 files changed, 206 insertions(+), 13 deletions(-)
diff --git a/journal_entry.c b/journal_entry.c
@@ -21,6 +21,8 @@
 
 char ICS_DIR[256];
 
+static const char *IS_DIRECTORY_PROPERTY = "X-CALDAVFS-ISDIRECTORY";
+
 // tree of journal entries
 struct tree_node *fuse_tree_root = NULL;
 
@@ -189,10 +191,57 @@ get_entry_from_fuse_path(const char *path)
 	return entry;
 }
 
+// If it has a relationship type
+bool
+is_directory_component(icalcomponent *component)
+{
+	icalcomponent *journal = icalcomponent_get_first_component(
+	    component, ICAL_VJOURNAL_COMPONENT);
+
+	for (icalproperty *prop = icalcomponent_get_first_property(
+		 journal, ICAL_RELATEDTO_PROPERTY);
+	     prop != NULL; prop = icalcomponent_get_next_property(
+			       journal, ICAL_RELATEDTO_PROPERTY)) {
+
+		const char *reltype =
+		    icalproperty_get_parameter_as_string(prop, "RELTYPE");
+		if (reltype && strcasecmp(reltype, "PARENT") == 0) {
+			return true;
+		}
+	}
+	for (icalproperty *prop =
+		 icalcomponent_get_first_property(journal, ICAL_X_PROPERTY);
+	     prop != NULL;
+	     prop = icalcomponent_get_next_property(journal, ICAL_X_PROPERTY)) {
+		LOG("FOUND X PROPERTY");
+
+		const char *prop_name = icalproperty_get_x_name(prop);
+		if (prop_name &&
+		    strcmp(prop_name, IS_DIRECTORY_PROPERTY) == 0) {
+			const char *value =
+			    icalproperty_get_value_as_string(prop);
+			LOG("FOUND DIRECTORY");
+			return value && strcmp(value, "YES") == 0;
+		}
+	}
+	return false;
+}
+
+// We assume it to be a directory if it has a property directory property
+// so or is a parent node
 bool
 entry_is_directory(tree_node *node)
 {
-	return (node->child_count > 0);
+	if (!node->data) {
+		return true;
+	}
+	else if (node->child_count > 0) {
+		return true;
+	}
+	else {
+		struct journal_entry *entry = node->data;
+		return is_directory_component(entry->component);
+	}
 }
 
 char *
@@ -237,7 +286,7 @@ parse_ics_file(const char *filename)
 	return component;
 }
 
-// We assume that there is only one entry in the file
+// We assume that there is only one inner component in the file
 int
 parse_ics_to_journal_entry_component(const char *filename,
 				     struct journal_entry *entry)
@@ -300,8 +349,16 @@ load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry)
 	// 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) {
+	if (is_directory_component(entry->component)) {
+		if (asprintf(&entry->filename, "%s",
+			     icalcomponent_get_summary(entry->component)) ==
+		    -1) {
+			LOG("Failed to write filename");
+			return -1;
+		};
+	}
+	else if (asprintf(&entry->filename, "%s.txt",
+			  icalcomponent_get_summary(entry->component)) == -1) {
 		LOG("Failed to write filename");
 		return -1;
 	};
@@ -594,6 +651,39 @@ create_vjournal_entry(char *summary)
 	return calendar;
 }
 
+icalcomponent *
+create_vjournal_directory(char *summary)
+{
+
+	icalcomponent *calendar = icalcomponent_new_vcalendar();
+
+	icalcomponent_add_property(calendar, icalproperty_new_version("2.0"));
+	icalcomponent_add_property(calendar,
+				   icalproperty_new_prodid("-//caldavfs//EN"));
+
+	// Step 2: Create a VJOURNAL entry
+	icalcomponent *journal = icalcomponent_new_vjournal();
+	char *id = create_new_unique_ics_uid();
+
+	icalcomponent_add_property(journal, icalproperty_new_uid(id));
+
+	icalcomponent_add_property(
+	    journal, icalproperty_new_dtstamp(icaltime_current_time_with_zone(
+			 icaltimezone_get_utc_timezone())));
+	icalcomponent_add_property(journal, icalproperty_new_summary(summary));
+
+	// Empty description; unused
+	icalcomponent_add_property(journal, icalproperty_new_description(""));
+
+	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(journal, is_directory);
+
+	icalcomponent_add_component(calendar, journal);
+	return calendar;
+}
+
 void
 remove_file_extension(char *path)
 {
@@ -638,13 +728,13 @@ find_by_component_uuid(tree_node *root, const char *target_uuid)
 
 // TODO: Handle duplicates in filename
 int
-create_entry_from_fuse(const char *filename)
+create_entry_from_fuse(const char *fuse_path)
 {
 	const char *entry_prefix = "/";
-	if (strncmp(filename, entry_prefix, strlen(entry_prefix)) != 0) {
+	if (strncmp(fuse_path, entry_prefix, strlen(entry_prefix)) != 0) {
 		return -EINVAL;
 	}
-	char *new_basename = strrchr(filename, '/');
+	char *new_basename = strrchr(fuse_path, '/');
 	new_basename++;
 
 	char *without_extension = strdup(new_basename);
@@ -688,6 +778,56 @@ create_entry_from_fuse(const char *filename)
 	return 0;
 }
 
+int
+create_directory_from_fuse_path(const char *fuse_path)
+{
+	const char *entry_prefix = "/";
+	if (strncmp(fuse_path, entry_prefix, strlen(entry_prefix)) != 0) {
+		return -EINVAL;
+	}
+	char *new_basename = strrchr(fuse_path, '/');
+	new_basename++;
+
+	struct journal_entry *new_entry =
+	    xcalloc(1, sizeof(struct journal_entry));
+	icalcomponent *vjournal_component =
+	    create_vjournal_directory(new_basename);
+
+	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 directory");
+
+	hashmap_insert(entries_original_ics, new_entry->filename_original,
+		       new_entry);
+
+	// TODO: Subdirectory
+	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;
+}
+
+// TODO: Add return status
 void
 update_or_create_fuse_entry_from_original(const char *filename)
 {
diff --git a/journal_entry.h b/journal_entry.h
@@ -100,6 +100,9 @@ update_delete_from_original_path(const char *filepath);
 int
 load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry);
 
+icalcomponent *
+create_vjournal_directory(char *summary);
+
 // Initializes
 // entries_original_key
 // entries_fuse_key
@@ -135,6 +138,9 @@ create_new_unique_ics_uid();
 icalcomponent *
 create_vjournal_entry(char *summary);
 
+int
+create_directory_from_fuse_path(const char *fuse_path);
+
 void
 remove_file_extension(char *path);
 
diff --git a/main.c b/main.c
@@ -1,4 +1,5 @@
 #include "tree.h"
+#include <cerrno>
 #define FUSE_USE_VERSION 31
 
 #include "hashmap.h"
@@ -99,13 +100,45 @@ 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) {
-		size_t n_keys = fuse_tree_root->child_count;
-		for (size_t i = 0; i < n_keys; i++) {
-			struct journal_entry *entry =
-			    fuse_tree_root->children[i]->data;
-			filler(buf, entry->filename, NULL, 0, 0);
+		for (size_t i = 0; i < fuse_tree_root->child_count; i++) {
+			tree_node *node = fuse_tree_root->children[i];
+			struct journal_entry *entry = node->data;
+
+			struct stat st = {0};
+			// TODO: Fix
+			st.st_ino = i + 1;
+			st.st_mode = S_IFREG | 0744;
+
+			if (entry_is_directory(node)) {
+				st.st_mode = S_IFDIR | 0755;
+			}
+
+			filler(buf, entry->filename, &st, 0, 0);
+		}
+	}
+	else {
+		tree_node *node = find_node_by_path(fuse_tree_root, path);
+		if (node) {
+			for (size_t i = 0; i < node->child_count; i++) {
+				tree_node *child = node->children[i];
+				struct journal_entry *entry = child->data;
+
+				struct stat st = {0};
+				// TODO: Fix
+				st.st_ino = i + 1;
+				st.st_mode = S_IFREG | 0744;
+
+				if (entry_is_directory(child)) {
+					st.st_mode = S_IFDIR | 0755;
+				}
+
+				filler(buf, entry->filename, &st, 0, 0);
+			}
+		}
+		else {
+			pthread_mutex_unlock(&entries_mutex);
+			return -ENOENT;
 		}
 	}
 	pthread_mutex_unlock(&entries_mutex);
@@ -123,6 +156,19 @@ journal_open(const char *path, struct fuse_file_info *fi)
 }
 
 int
+journal_mkdir(const char *path, mode_t mode)
+{
+	pthread_mutex_lock(&entries_mutex);
+	LOG("MKDIR %s", path);
+	if (create_directory_from_fuse_path(path) != 0) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -EIO;
+	};
+	pthread_mutex_unlock(&entries_mutex);
+	return 0;
+}
+
+int
 journal_read(const char *path, char *buf, size_t size, off_t offset,
 	     struct fuse_file_info *fi)
 {
@@ -329,6 +375,7 @@ static const struct fuse_operations journal_oper = {.getattr = journal_getattr,
 						    .utimens = journal_utimens,
 						    .write = journal_write,
 						    .create = journal_create,
+						    .mkdir = journal_mkdir,
 						    .unlink = journal_unlink,
 						    .readlink =
 							journal_readlink,