agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 16bca4d3c54de3d0b348cd99728142af286915b4
parent 3a171b4ea68f1d091d61a1eec55c2123e9a55f40
Author: Marc Coquand <marc@coquand.email>
Date:   Fri, 22 Aug 2025 18:29:48 +0200

Add draft/final support

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

Diffstat:
Magendafs.8.scd | 4++++
Mfuse_node.c | 40+++++++++++++++++++++++++++++++++++++++-
Mfuse_node.h | 7+++++++
Mical_extra.c | 28++++++++++++++++++++++++++++
Mical_extra.h | 6+++++-
Mmain.c | 49++++++++++++++++++++++++++++++++++++++++++++++++-
6 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/agendafs.8.scd b/agendafs.8.scd
@@ -51,6 +51,10 @@ usage. Built-in extended attributes are:
 	Date-time start, ISO-formatted. Specifies the "beginning" of the
 	file.
 
+*user.status*
+	Either *draft* or *final*. Can not be removed. By default, 
+	directories are set to *final* while files are set to *draft*.
+
 Arbitrary user-provided attributes are also supported. Limits are set to
 255 bytes for attributes and 64 kilobytes for their value.
 
diff --git a/fuse_node.c b/fuse_node.c
@@ -106,7 +106,6 @@ write_ical_file(memory_region *mreg, const struct tree_node *node,
 		icalproperty_set_lastmodified(prop, get_ical_now());
 	}
 
-	icalcomponent_set_status(ic, ICAL_STATUS_FINAL);
 	icalcomponent *inner = icalcomponent_get_inner(ic);
 
 	const char *descr_prop = icalcomponent_get_description(inner);
@@ -518,6 +517,31 @@ get_node_class(memory_region *mreg, const struct tree_node *node)
 	return format_ical_class(classv);
 }
 
+const char *
+get_node_status(memory_region *mreg, const struct tree_node *node)
+{
+
+	// TODO: Take this as parameter instead...
+	icalcomponent *component = get_icalcomponent_from_node(mreg, node);
+	if (!component) {
+		return 0;
+	}
+
+	icalcomponent *inner = icalcomponent_get_inner(component);
+
+	const icalproperty *statusp =
+	    icalcomponent_get_first_property(inner, ICAL_STATUS_PROPERTY);
+
+	if (!statusp) {
+		LOG("No status found");
+		return NULL;
+	}
+
+	const enum icalproperty_status statusv =
+	    icalproperty_get_status(statusp);
+	return format_ical_status(statusv);
+}
+
 int
 delete_node_class(memory_region *mreg, const struct tree_node *node)
 {
@@ -556,6 +580,20 @@ set_node_class(memory_region *mreg, const struct tree_node *node,
 	return write_ical_file(mreg, node, component);
 }
 
+int
+set_node_status(memory_region *mreg, const struct tree_node *node,
+		const icalproperty_status new_status)
+{
+	icalcomponent *component = get_icalcomponent_from_node(mreg, node);
+	if (!component) {
+		return 0;
+	}
+
+	icalcomponent_set_status(component, new_status);
+
+	return write_ical_file(mreg, node, component);
+}
+
 size_t
 calculate_node_size(memory_region *mreg, const struct tree_node *node,
 		    icalcomponent *node_component)
diff --git a/fuse_node.h b/fuse_node.h
@@ -79,6 +79,9 @@ set_node_categories(memory_region *mreg, const struct tree_node *node,
 const char *
 get_node_class(memory_region *mreg, const struct tree_node *node);
 
+const char *
+get_node_status(memory_region *mreg, const struct tree_node *node);
+
 int
 delete_node_class(memory_region *mreg, const struct tree_node *node);
 
@@ -86,6 +89,10 @@ int
 set_node_class(memory_region *mreg, const struct tree_node *node,
 	       const icalproperty_class new_class);
 
+int
+set_node_status(memory_region *mreg, const struct tree_node *node,
+		const icalproperty_status new_status);
+
 struct stat
 get_node_stat(memory_region *mreg, const struct tree_node *node,
 	      icalcomponent *node_component);
diff --git a/ical_extra.c b/ical_extra.c
@@ -65,6 +65,19 @@ parse_ical_class(const char *input)
 	return ICAL_CLASS_NONE;
 }
 
+const icalproperty_status
+parse_ical_status(const char *input)
+{
+	// FIXME should check if VJOURNAL, VEVENT or VTODO
+	if (strcmp(input, "draft") == 0) {
+		return ICAL_STATUS_DRAFT;
+	}
+	else if (strcmp(input, "final") == 0) {
+		return ICAL_STATUS_FINAL;
+	}
+	return ICAL_STATUS_FINAL;
+}
+
 const char *
 format_ical_class(const enum icalproperty_class iclass)
 {
@@ -80,6 +93,18 @@ format_ical_class(const enum icalproperty_class iclass)
 	return NULL;
 }
 
+const char *
+format_ical_status(const enum icalproperty_status istatus)
+{
+	if (istatus == ICAL_STATUS_DRAFT) {
+		return "draft";
+	}
+	else if (istatus == ICAL_STATUS_FINAL) {
+		return "final";
+	}
+	return NULL;
+}
+
 bool
 is_directory_component(icalcomponent *component)
 {
@@ -181,6 +206,7 @@ create_vjournal_directory(memory_region *mreg, const char *summary)
 
 	icalproperty *private = icalproperty_new_class(ICAL_CLASS_PRIVATE);
 	icalcomponent_add_property(journal, private);
+	icalcomponent_set_status(journal, ICAL_STATUS_FINAL);
 
 	icalproperty *is_directory = icalproperty_new_x(IS_DIRECTORY_PROPERTY);
 	icalproperty_set_x_name(is_directory, IS_DIRECTORY_PROPERTY);
@@ -219,6 +245,8 @@ create_vjournal_entry(memory_region *mreg, const char *summary)
 	icalproperty *private = icalproperty_new_class(ICAL_CLASS_PRIVATE);
 	icalcomponent_add_property(journal, private);
 
+	icalcomponent_set_status(journal, ICAL_STATUS_DRAFT);
+
 	char *without_extension = without_file_extension(mreg, summary);
 
 	icalcomponent_add_property(journal,
diff --git a/ical_extra.h b/ical_extra.h
@@ -45,9 +45,13 @@ set_parent_child_relationship_to_component(icalcomponent *parent,
 const icalproperty_class
 parse_ical_class(const char *input);
 
+const icalproperty_status
+parse_ical_status(const char *input);
+
 const char *
 format_ical_class(const enum icalproperty_class iclass);
-
+const char *
+format_ical_status(const enum icalproperty_status istatus);
 void
 icalcomponent_insert_description(memory_region *mreg, icalcomponent *ic,
 				 const char *buf, size_t size, off_t offset);
diff --git a/main.c b/main.c
@@ -383,6 +383,12 @@ fuse_removexattr(const char *path, const char *attribute)
 		status = delete_node_class(mreg, node);
 		goto cleanup_return;
 	}
+
+	// It should probably always be set
+	else if (strcmp(attribute, "user.status") == 0) {
+		status = -EPERM;
+		goto cleanup_return;
+	}
 	if (strcmp(attribute, "user.dtstart") == 0) {
 		status = clear_dtstart(mreg, node);
 		goto cleanup_return;
@@ -454,11 +460,13 @@ fuse_setxattr(const char *path, const char *attribute, const char *value,
 	}
 
 	else if (strcmp(attribute, "user.categories") == 0) {
+		LOG("Updating categories");
 		status = set_node_categories(mreg, node, value, s);
 		goto cleanup_return;
 	}
 
 	else if (strcmp(attribute, "user.class") == 0) {
+		LOG("Updating class");
 		enum icalproperty_class iclass = parse_ical_class(value);
 
 		if (iclass == ICAL_CLASS_NONE) {
@@ -469,10 +477,25 @@ fuse_setxattr(const char *path, const char *attribute, const char *value,
 		status = set_node_class(mreg, node, iclass);
 		goto cleanup_return;
 	}
+	else if (strcmp(attribute, "user.status") == 0) {
+		LOG("Updating status");
+		enum icalproperty_status istatus = parse_ical_status(value);
+
+		if (istatus == ICAL_STATUS_NONE) {
+			LOG("INVALID STATUS");
+			status = -EINVAL;
+			goto cleanup_return;
+		}
+
+		status = set_node_status(mreg, node, istatus);
+		goto cleanup_return;
+	}
 	else if (starts_with_str(attribute, "user.")) {
 		icalcomponent *ic = get_icalcomponent_from_node(mreg, node);
 
 		const char *rkey = attribute + 5;
+		LOG("Updating %s", rkey);
+
 		icalcomponent_set_custom_x_value(mreg, ic, rkey, value);
 		status = write_ical_file(mreg, node, ic);
 		goto cleanup_return;
@@ -533,12 +556,20 @@ fuse_listxattr(const char *path, char *list, size_t size)
 	fprintf(stream, "user.uid");
 	fputc('\0', stream);
 
+	LOG("Checking node class");
 	const char *iclass = get_node_class(mreg, node);
 	if (iclass != NULL && strcmp("", iclass) != 0) {
 		fprintf(stream, "user.class");
 		fputc('\0', stream);
 	}
 
+	LOG("Checking node status");
+	const char *istatus = get_node_status(mreg, node);
+	if (istatus != NULL && strcmp("", istatus) != 0) {
+		fprintf(stream, "user.status");
+		fputc('\0', stream);
+	}
+
 	icalcomponent_print_x_props(stream, comp);
 
 	if (fclose(stream) != 0) {
@@ -547,7 +578,7 @@ fuse_listxattr(const char *path, char *list, size_t size)
 	}
 
 	if (size > 0) {
-		if (xattr_list_len < size) {
+		if (xattr_list_len > size) {
 			memcpy(list, xattribute_list, xattr_list_len);
 		}
 		else {
@@ -626,6 +657,22 @@ fuse_getxattr(const char *path, const char *attribute, char *buf, size_t s)
 		status = string_len;
 		goto cleanup_return;
 	}
+	else if (strcmp(attribute, "user.status") == 0) {
+		const char *c = get_node_status(mreg, node);
+		if (!c || *c == '\0') {
+			status = -ENODATA;
+			goto cleanup_return;
+		}
+
+		int string_len = strlen(c);
+
+		if (s > 0) {
+			snprintf(buf, s, "%s", c);
+		}
+
+		status = string_len;
+		goto cleanup_return;
+	}
 	else if (strcmp(attribute, "user.dtstart") == 0) {
 		const char *res = get_dtstart(mreg, node);
 		if (!res) {