agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 40c4af71c6ca4926fc7da0b37030ea58f5f6f961
parent 58488c71d5cd19b609e87dd14915187a73b1d883
Author: Marc Coquand <marc@coquand.email>
Date:   Mon,  2 Jun 2025 19:51:44 +0100

Convert to libical

Diffstat:
Mmain.c | 254++++++++++++++++++++++++++++++-------------------------------------------------
1 file changed, 95 insertions(+), 159 deletions(-)
diff --git a/main.c b/main.c
@@ -36,12 +36,7 @@ time_t INVALID_TIME = (time_t)-1;
 typedef struct {
 	char *filename;
 	char *filename_original;
-	char *summary;
-	char *description;
-	char *uid;
-	char *categories;
-	time_t modified_at;
-	time_t created_at;
+	icalcomponent *component;
 } journal_entry;
 
 // We create two maps that link to the same entries
@@ -56,18 +51,12 @@ free_journal_entry(void *val)
 	journal_entry *entry = (journal_entry *)val;
 	if (!entry)
 		return;
-	free(entry->uid);
-	free(entry->filename);
-	free(entry->filename_original);
-	if (entry->summary != NULL && strcmp(entry->summary, "") != 0) {
-		free(entry->summary);
-	}
-	if (entry->categories != NULL && strcmp(entry->summary, "") != 0) {
-		free(entry->categories);
-	}
-	if (entry->description != NULL && strcmp(entry->description, "") != 0) {
-		free(entry->description);
-	}
+	if (entry->component)
+		icalcomponent_free(entry->component);
+	if (entry->filename)
+		free(entry->filename);
+	if (entry->filename_original)
+		free(entry->filename_original);
 	free(entry);
 }
 
@@ -300,64 +289,15 @@ parse_ics_to_journal_entry(const char *filename, journal_entry *entry)
 		LOG("No journal component");
 		return -2;
 	}
-
-	icalproperty *prop =
-	    icalcomponent_get_first_property(journal, ICAL_ANY_PROPERTY);
-	while (prop != NULL) {
-		icalproperty_kind kind = icalproperty_isa(prop);
-		switch (kind) {
-		case ICAL_SUMMARY_PROPERTY:
-			free(entry->summary);
-			entry->summary = strdup(icalproperty_get_summary(prop));
-			break;
-
-		case ICAL_DESCRIPTION_PROPERTY:
-			free(entry->description);
-			entry->description =
-			    strdup(icalproperty_get_description(prop));
-			break;
-
-		case ICAL_UID_PROPERTY:
-			free(entry->uid);
-			entry->uid = strdup(icalproperty_get_uid(prop));
-			break;
-
-		case ICAL_CATEGORIES_PROPERTY:
-			free(entry->categories);
-			entry->categories =
-			    strdup(icalproperty_get_categories(prop));
-			break;
-
-		case ICAL_LASTMODIFIED_PROPERTY: {
-			struct icaltimetype tt = icalproperty_get_dtstamp(prop);
-			entry->modified_at = icaltime_as_timet(tt);
-			break;
-		}
-
-		case ICAL_DTSTAMP_PROPERTY: {
-			struct icaltimetype tt = icalproperty_get_created(prop);
-			entry->created_at = icaltime_as_timet(tt);
-			break;
-		}
-		case ICAL_CREATED_PROPERTY: {
-			struct icaltimetype tt = icalproperty_get_created(prop);
-			entry->created_at = icaltime_as_timet(tt);
-			break;
-		}
-
-		default:
-			break;
-		}
-		prop =
-		    icalcomponent_get_next_property(journal, ICAL_ANY_PROPERTY);
-	}
+	entry->component = component;
 	return 0;
 }
 
 // Dynamically create a time string using format
 char *
-time_string(time_t time_val, const char *format)
+time_string(icaltimetype t, const char *format)
 {
+	time_t time_val = icaltime_to_time_t(t);
 	struct tm tm_info;
 	if (!localtime_r(&time_val, &tm_info)) {
 		return NULL;
@@ -373,8 +313,11 @@ time_string(time_t time_val, const char *format)
 // Uses created_at as filename.
 // However, in case of collission, set the add 1,2 et.c.
 char *
-get_unique_filename(time_t created_at, struct hashmap *entries_map)
+get_unique_filename(journal_entry *entry, struct hashmap *entries_map)
 {
+	icalcomponent *c = entry->component;
+	icaltimetype created_at = icalcomponent_get_dtstamp(c);
+
 	char *filename_time_str = time_string(created_at, date_title_fmt);
 	if (!filename_time_str) {
 		LOG("Failed to format date string");
@@ -462,8 +405,8 @@ load_journal_entries()
 
 			// Make sure filename, which happens when
 			// Two entries are created on the same minute
-			char *unique_filename = get_unique_filename(
-			    new_entry->created_at, entries_fuse_key);
+			char *unique_filename =
+			    get_unique_filename(new_entry, entries_fuse_key);
 			if (!unique_filename) {
 				LOG("Failed to generate filename for %s",
 				    entry->d_name);
@@ -501,8 +444,9 @@ size_t
 write_entry_content(char *buf, journal_entry *entry)
 {
 	// Allocate memory for the content
-	size_t size =
-	    asprintf(&buf, "%s\n---\n%s", entry->summary, entry->description);
+	size_t size = asprintf(&buf, "%s\n---\n%s",
+			       icalcomponent_get_summary(entry->component),
+			       icalcomponent_get_description(entry->component));
 	if (size == -1) {
 		LOG("Out of memory");
 		exit(1);
@@ -513,8 +457,9 @@ write_entry_content(char *buf, journal_entry *entry)
 size_t
 get_entry_content_size(journal_entry *entry)
 {
-	size_t size = snprintf(NULL, 0, "%s\n---\n%s", entry->summary,
-			       entry->description);
+	size_t size = snprintf(NULL, 0, "%s\n---\n%s",
+			       icalcomponent_get_summary(entry->component),
+			       icalcomponent_get_description(entry->component));
 	if (size == -1) {
 		LOG("Out of mem");
 		exit(1);
@@ -543,7 +488,7 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 		ret_code = -ENOENT;
 		goto unlock_and_return;
 	}
-	stbuf->st_mode = S_IFREG | 0444;
+	stbuf->st_mode = S_IFREG | 0774;
 	stbuf->st_nlink = 1;
 
 	int size = get_entry_content_size(entry);
@@ -551,8 +496,21 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 	stbuf->st_size = (off_t)size;
 
 	LOG("Entry found: %s", entry->filename);
-	stbuf->st_mtime = entry->modified_at;
-	stbuf->st_ctime = entry->modified_at;
+	icalproperty *last_modified_prop = icalcomponent_get_next_property(
+	    entry->component, ICAL_LASTMODIFIED_PROPERTY);
+
+	time_t lastmodified;
+	if (last_modified_prop) {
+		icaltimetype ical_lastmodified =
+		    icalproperty_get_lastmodified(last_modified_prop);
+		lastmodified = icaltime_to_time_t(ical_lastmodified);
+	}
+	else {
+		lastmodified = time(NULL);
+	}
+
+	stbuf->st_mtime = lastmodified;
+	stbuf->st_ctime = lastmodified;
 	stbuf->st_atime = time(NULL);
 
 	ret_code = 0;
@@ -611,12 +569,12 @@ journal_read(const char *path, char *buf, size_t size, off_t offset,
 		goto unlock_and_return;
 	}
 
-	LOG("Got entry %s", entry->summary);
-
 	// Calculate required length for full content
 	char *full_content = NULL;
-	size_t needed_len = asprintf(&full_content, "%s\n---\n%s",
-				     entry->summary, entry->description);
+	size_t needed_len =
+	    asprintf(&full_content, "%s\n---\n%s",
+		     icalcomponent_get_summary(entry->component),
+		     icalcomponent_get_description(entry->component));
 	if (needed_len == -1) {
 		LOG("Out of memory");
 		exit(1);
@@ -643,51 +601,12 @@ unlock_and_return:
 	return ret_code;
 }
 
-char *
-journal_entry_to_ical(const journal_entry *entry)
-{
-	char *escaped_summary = escape_text(entry->summary);
-	char *escaped_description = escape_text(entry->description);
-
-	// Format timestamps
-	struct tm created_tm, modified_tm;
-	gmtime_r(&entry->created_at, &created_tm);
-	gmtime_r(&entry->modified_at, &modified_tm);
-
-	char dtstamp[32], lastmod[32];
-	strftime(dtstamp, sizeof(dtstamp), "%Y%m%dT%H%M%SZ", &created_tm);
-	strftime(lastmod, sizeof(lastmod), "%Y%m%dT%H%M%SZ", &modified_tm);
-
-	char *ical = NULL;
-
-	asprintf(&ical,
-		 "BEGIN:VCALENDAR\n"
-		 "PRODID:caldavfs\n"
-		 "VERSION:2.0\n"
-		 "BEGIN:VJOURNAL\n"
-		 "UID:%s\n"
-		 "DTSTAMP:%s\n"
-		 "CREATED:%s\n"
-		 "LAST-MODIFIED:%s\n"
-		 "SUMMARY:%s\n"
-		 "STATUS:FINAL\n"
-		 "DESCRIPTION:%s\n"
-		 "END:VJOURNAL\n"
-		 "END:VCALENDAR\n",
-		 entry->uid, dtstamp, dtstamp, lastmod, escaped_summary,
-		 escaped_description);
-
-	free(escaped_summary);
-	free(escaped_description);
-	return ical;
-}
-
 size_t
 parse_entry_content(journal_entry *entry, const char *buf, size_t size)
 {
 	char *copy = strndup(buf, size);
 
-	char *summary = NULL, *description = NULL;
+	char *description = NULL;
 	char *sep = strstr(copy, "\n---\n");
 
 	// Malformed input
@@ -699,26 +618,27 @@ parse_entry_content(journal_entry *entry, const char *buf, size_t size)
 	*sep = '\0';
 	description = sep + 5;
 
-	char *summary_line = strtok(copy, "\n");
-	if (!summary_line) {
+	char *summary = strtok(copy, "\n");
+	if (!summary) {
 		free(copy);
 		return -EINVAL;
 	}
 
-	summary = strdup(summary_line);
-
-	// Clean up old values
-	if (entry->summary != NULL && strcmp(entry->summary, "") != 0) {
-		free(entry->summary);
+	icalproperty *prop = icalcomponent_get_first_property(
+	    entry->component, ICAL_LASTMODIFIED_PROPERTY);
+	if (!prop) {
+		prop = icalproperty_new_lastmodified(icaltime_null_time());
+		icalcomponent_add_property(entry->component, prop);
 	}
-
-	if (entry->description != NULL && strcmp(entry->description, "") != 0) {
-		free(entry->description);
+	else {
+		time_t now = time(NULL);
+		struct icaltimetype now_tt =
+		    icaltime_from_timet_with_zone(now, 0, NULL);
+		icalproperty_set_lastmodified(prop, now_tt);
 	}
-	entry->summary = summary;
-	entry->description = strdup(description);
-	entry->modified_at = time(NULL);
 
+	icalcomponent_set_summary(entry->component, summary);
+	icalcomponent_set_description(entry->component, description);
 	free(copy);
 	return 0;
 }
@@ -726,7 +646,7 @@ parse_entry_content(journal_entry *entry, const char *buf, size_t size)
 size_t
 write_entry_to_ical_file(const journal_entry *entry, const char *filepath)
 {
-	char *ical_str = journal_entry_to_ical(entry);
+	char *ical_str = icalcomponent_as_ical_string_r(entry->component);
 
 	FILE *f = fopen(filepath, "w");
 	if (!f) {
@@ -762,7 +682,6 @@ journal_write(const char *path, const char *buf, size_t size, off_t offset,
 		return -ENOENT;
 	}
 
-	// Update hashmap with input
 	LOG("Parsing entry content");
 	int ret = parse_entry_content(entry, buf, size);
 	if (ret != 0) {
@@ -788,12 +707,7 @@ copy_journal_entry(const journal_entry *src)
 
 	copy->filename = strdup(src->filename);
 	copy->filename_original = strdup(src->filename_original);
-	copy->summary = strdup(src->summary);
-	copy->description = strdup(src->description);
-	copy->uid = strdup(src->uid);
-
-	copy->modified_at = src->modified_at;
-	copy->created_at = src->created_at;
+	copy->component = icalcomponent_new_clone(src->component);
 
 	return copy;
 }
@@ -855,7 +769,10 @@ do_journal_entry_rename(time_t new_created_at, const char *old, const char *new)
 	journal_entry *new_entry = copy_journal_entry(old_entry);
 	free(new_entry->filename);
 	new_entry->filename = strdup(new_basename);
-	new_entry->created_at = new_created_at;
+	struct icaltimetype created_at_ical =
+	    icaltime_from_timet_with_zone(new_created_at, 0, NULL);
+
+	icalcomponent_set_dtstamp(new_entry->component, created_at_ical);
 
 	// hashmap frees old_entry
 	hashmap_remove(entries_original_key, old_entry->filename_original);
@@ -962,6 +879,37 @@ create_new_unique_ics_uid()
 	return res;
 }
 
+icalcomponent *
+create_vjournal_entry(time_t created_at)
+{
+	struct icaltimetype created_at_ical =
+	    icaltime_from_timet_with_zone(created_at, 0, NULL);
+
+	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();
+
+	icalcomponent_add_property(
+	    journal, icalproperty_new_uid(create_new_unique_ics_uid()));
+	icalcomponent_add_property(journal,
+				   icalproperty_new_dtstamp(created_at_ical));
+	icalcomponent_add_property(journal, icalproperty_new_summary(""));
+	icalcomponent_add_property(journal, icalproperty_new_description(""));
+
+	icalcomponent_add_property(journal,
+				   icalproperty_new_lastmodified(
+				       icaltime_current_time_with_zone(NULL)));
+
+	icalcomponent_add_component(calendar, journal);
+
+	return calendar;
+}
+
 int
 journal_create(const char *filename, mode_t mode, struct fuse_file_info *info)
 {
@@ -978,15 +926,8 @@ journal_create(const char *filename, mode_t mode, struct fuse_file_info *info)
 		pthread_mutex_unlock(&entries_mutex);
 		return -EINVAL;
 	}
-
 	journal_entry *new_entry = xcalloc(1, sizeof(journal_entry));
-	new_entry->filename = strdup(new_basename);
-	new_entry->created_at = created_at;
-	new_entry->modified_at = created_at;
-	new_entry->summary = "";
-	new_entry->description = "";
-	new_entry->uid = create_new_unique_ics_uid();
-	asprintf(&new_entry->filename_original, "%s.ics", new_entry->uid);
+	create_vjournal_entry(created_at);
 
 	hashmap_insert(entries_original_key, new_entry->filename_original,
 		       new_entry);
@@ -1038,11 +979,6 @@ update_fuse_entry_from_original(const char *filename)
 	new_entry->filename_original =
 	    strndup(entry->filename_original, strlen(entry->filename_original));
 
-	LOG("Updating file path %s, new entry %s %s. New summary: %s , new "
-	    "desc: %s ",
-	    filename, new_entry->filename, new_entry->filename_original,
-	    new_entry->summary, new_entry->description);
-
 	hashmap_insert(entries_original_key, new_entry->filename_original,
 		       new_entry);
 	hashmap_insert(entries_fuse_key, new_entry->filename, new_entry);