agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 15bb8424d696e8fa99fc53f78389024c9b64c6d3
parent 8445ac56d625bfa81e154b3d1378ce4d06345fe2
Author: Marc Coquand <marc@coquand.email>
Date:   Sun,  1 Jun 2025 13:56:04 +0100

Add journal rename

Diffstat:
Mmain.c | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 195 insertions(+), 57 deletions(-)
diff --git a/main.c b/main.c
@@ -6,6 +6,7 @@
 #include <fuse3/fuse.h>
 #include <pthread.h>
 #include <regex.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -22,7 +23,9 @@
 
 const char *date_title_fmt = "%Y%m%d-%H:%M";
 regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod,
-    regex_uid;
+    regex_uid, regex_fuse_file;
+
+time_t INVALID_TIME = (time_t)-1;
 
 typedef struct {
 	char *filename;
@@ -71,7 +74,7 @@ static time_t parse_datetime(const char *datetime)
 
 static int compile_regexes(regex_t *regex_summary, regex_t *regex_description,
 			   regex_t *regex_dtstamp, regex_t *regex_lastmod,
-			   regex_t *regex_uid)
+			   regex_t *regex_uid, regex_t *regex_fuse_file)
 {
 	// Parse text after SUMMARY: up until \r\n
 	if (regcomp(regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0)
@@ -87,6 +90,10 @@ static int compile_regexes(regex_t *regex_summary, regex_t *regex_description,
 		return -1;
 	if (regcomp(regex_uid, "UID:([^\r\n]+)", REG_EXTENDED) != 0)
 		return -1;
+	if (regcomp(regex_fuse_file,
+		    "^([0-9]{8})-([0-9]{2}:[0-9]{2})(_[^/\\:*?\"<>|]+)?\\.txt$",
+		    REG_EXTENDED) != 0)
+		return -1;
 	return 0;
 }
 
@@ -95,8 +102,6 @@ static char *extract_match(const char *content, regmatch_t match)
 {
 	size_t len = match.rm_eo - match.rm_so;
 	char *m_str = calloc(len + 1, sizeof(char));
-	if (!m_str)
-		return NULL;
 	memcpy(m_str, content + match.rm_so, len);
 	m_str[len] = '\0';
 	return m_str;
@@ -152,8 +157,6 @@ static char *escape_text(const char *src)
 
 	size_t len = strlen(src);
 	char *escaped = calloc(len * 2 + 1, 1);
-	if (!escaped)
-		return NULL;
 
 	char *dst = escaped;
 	for (const char *p = src; *p; ++p) {
@@ -385,14 +388,7 @@ static void load_journal_entries()
 {
 	pthread_mutex_lock(&entries_mutex);
 
-	if (entries_original_key) {
-		hashmap_free(entries_original_key);
-	}
 	entries_original_key = hashmap_new(NULL);
-
-	if (entries_fuse_key) {
-		hashmap_free(entries_fuse_key);
-	}
 	entries_fuse_key = hashmap_new(free_journal_entry);
 
 	LOG("Loading journal entries from: %s\n", ICS_DIR);
@@ -467,6 +463,19 @@ static void load_journal_entries()
 	closedir(dir);
 	pthread_mutex_unlock(&entries_mutex);
 }
+
+int write_entry_content(char *buf, size_t max_len, journal_entry *entry)
+{
+	int size = snprintf(buf, max_len, "%s\n---\n%s", entry->summary,
+			    entry->description);
+	return size;
+}
+
+int get_entry_content_size(journal_entry *entry)
+{
+	return write_entry_content(NULL, 0, entry);
+}
+
 int journal_getattr(const char *path, struct stat *stbuf,
 		    struct fuse_file_info *fi)
 {
@@ -491,8 +500,9 @@ int journal_getattr(const char *path, struct stat *stbuf,
 	stbuf->st_mode = S_IFREG | 0444;
 	stbuf->st_nlink = 1;
 
-	int size = snprintf(NULL, 0, "%s\n---\n%s", entry->summary,
-			    entry->description);
+	// Write to nothing and get the size
+	int size = get_entry_content_size(entry);
+
 	stbuf->st_size = (off_t)size;
 
 	LOG("Entry found %s, %s", entry->filename, entry->description);
@@ -556,22 +566,11 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
 	LOG("Got entry %s", entry->description);
 
 	// Calculate required length for full content
-	int needed_len = (size_t)snprintf(NULL, 0, "%s\n---\n%s",
-					  entry->summary, entry->description);
-	if (needed_len < 0) {
-		ret_code = -EIO;
-		goto unlock_and_return;
-	}
+	int needed_len = get_entry_content_size(entry);
 
 	char *full_content = calloc(needed_len + 1, sizeof(char));
-	if (!full_content) {
-		perror("Memory allocation failed");
-		ret_code = -ENOMEM;
-		goto unlock_and_return;
-	}
 
-	snprintf(full_content, needed_len + 1, "%s\n---\n%s", entry->summary,
-		 entry->description);
+	write_entry_content(full_content, needed_len + 1, entry);
 
 	if (offset >= needed_len) {
 		// EOF
@@ -647,7 +646,7 @@ char *journal_entry_to_ical(const journal_entry *entry)
 	return ical;
 }
 
-int update_entry_content(journal_entry *entry, const char *buf, size_t size)
+int parse_entry_content(journal_entry *entry, const char *buf, size_t size)
 {
 	char *copy = strndup(buf, size);
 	if (!copy)
@@ -664,7 +663,6 @@ int update_entry_content(journal_entry *entry, const char *buf, size_t size)
 	*sep = '\0';
 	description = sep + 5;
 
-	// Summary and timestamp part
 	char *summary_line = strtok(copy, "\n");
 	if (!summary_line) {
 		free(copy);
@@ -689,6 +687,38 @@ int update_entry_content(journal_entry *entry, const char *buf, size_t size)
 	return 0;
 }
 
+int write_entry_to_ical_file(const journal_entry *entry, const char *filepath)
+{
+	char *ical_str = journal_entry_to_ical(entry);
+	if (!ical_str) {
+		return -EIO;
+	}
+
+	FILE *f = fopen(filepath, "w");
+	if (!f) {
+		perror("Failed to open ICS file");
+		free(ical_str);
+		return -EIO;
+	}
+
+	fputs(ical_str, f);
+	fclose(f);
+	free(ical_str);
+
+	return 0;
+}
+
+// Full filepath
+char *get_original_filepath(const journal_entry *entry)
+{
+	size_t needed =
+	    snprintf(NULL, 0, "%s/%s", ICS_DIR, entry->filename_original);
+	char *filepath = calloc(needed + 1, sizeof(char));
+	snprintf(filepath, needed + 1, "%s/%s", ICS_DIR,
+		 entry->filename_original);
+	return filepath;
+}
+
 static int journal_write(const char *path, const char *buf, size_t size,
 			 off_t offset, struct fuse_file_info *fi)
 {
@@ -700,52 +730,145 @@ static int journal_write(const char *path, const char *buf, size_t size,
 	}
 
 	// Update hashmap with input
-	int ret = update_entry_content(entry, buf, size);
+	int ret = parse_entry_content(entry, buf, size);
 	if (ret != 0) {
 		pthread_mutex_unlock(&entries_mutex);
 		return ret;
 	}
 
-	// Get filepath of original file
-	char filepath[512];
-	snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR,
-		 entry->filename_original);
+	char *filepath = get_original_filepath(entry);
 
-	// Write back to original .ics file
-	char *ical_str = journal_entry_to_ical(entry);
-	if (!ical_str) {
+	if (write_entry_to_ical_file(entry, filepath) != 0) {
 		pthread_mutex_unlock(&entries_mutex);
 		return -EIO;
 	}
+	pthread_mutex_unlock(&entries_mutex);
+	return size;
+}
 
-	FILE *f = fopen(filepath, "w");
-	if (!f) {
-		perror("Failed to open ICS file");
-		free(ical_str);
-		pthread_mutex_unlock(&entries_mutex);
+journal_entry *copy_journal_entry(const journal_entry *src)
+{
+	journal_entry *copy = calloc(1, sizeof(journal_entry));
+
+	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->timestamp = src->timestamp;
+	copy->created_at = src->created_at;
+
+	return copy;
+}
+
+time_t timestamp_from_filename(const char *filename)
+{
+
+	regmatch_t matches[4];
+
+	int ret = regexec(&regex_fuse_file, filename, 4, matches, 0);
+	if (ret != 0)
+		return INVALID_TIME;
+
+	// Extract YYYYMMDD and HH:MM
+	char date_str[9] = {0};
+	char time_str[6] = {0};
+
+	int len_date = matches[1].rm_eo - matches[1].rm_so;
+	int len_time = matches[2].rm_eo - matches[2].rm_so;
+
+	if (len_date != 8 || len_time != 5)
+		return (time_t)-1;
+
+	strncpy(date_str, filename + matches[1].rm_so, len_date);
+	strncpy(time_str, filename + matches[2].rm_so, len_time);
+
+	int year, month, day, hour, minute;
+
+	if (sscanf(date_str, "%4d%2d%2d", &year, &month, &day) != 3)
+		return INVALID_TIME;
+
+	if (sscanf(time_str, "%2d:%2d", &hour, &minute) != 2)
+		return (time_t)-1;
+
+	struct tm tm_time = {0};
+	tm_time.tm_year = year - 1900;
+	tm_time.tm_mon = month - 1;
+	tm_time.tm_mday = day;
+	tm_time.tm_hour = hour;
+	tm_time.tm_min = minute;
+	tm_time.tm_sec = 0;
+	tm_time.tm_isdst = -1;
+
+	time_t timestamp = mktime(&tm_time);
+	return timestamp;
+}
+
+static int do_journal_entry_rename(time_t new_created_at, const char *old,
+				   const char *new)
+{
+	const char *old_basename = strrchr(old, '/');
+	old_basename = old_basename + 1;
+
+	const char *new_basename = strrchr(new, '/');
+	new_basename = new_basename + 1;
+
+	journal_entry *old_entry = hashmap_get(entries_fuse_key, old_basename);
+	if (!old_entry) {
+		LOG("Entry does not exist");
+		return -EINVAL;
+	}
+
+	journal_entry *new_entry = copy_journal_entry(old_entry);
+
+	hashmap_remove(entries_original_key, old_entry->filename_original);
+	hashmap_remove(entries_fuse_key, old_entry->filename);
+
+	free(new_entry->filename);
+	new_entry->filename = strdup(new_basename);
+	new_entry->created_at = new_created_at;
+
+	hashmap_insert(entries_original_key, new_entry->filename_original,
+		       new_entry);
+	hashmap_insert(entries_fuse_key, new_entry->filename, new_entry);
+
+	// Write back to the original
+	char *filepath = get_original_filepath(new_entry);
+
+	if (write_entry_to_ical_file(new_entry, filepath) != 0) {
+		free(filepath);
 		return -EIO;
 	}
 
-	fputs(ical_str, f);
-	fclose(f);
-	free(ical_str);
-	pthread_mutex_unlock(&entries_mutex);
-	return size;
+	free(filepath);
+	return 0;
 }
 
-static const struct fuse_operations journal_oper = {
-    .getattr = journal_getattr,
-    .readdir = journal_readdir,
-    .open = journal_open,
-    .read = journal_read,
-    .write = journal_write,
-};
+static int journal_rename(const char *old, const char *new, unsigned int flags)
+{
+	pthread_mutex_lock(&entries_mutex);
+	LOG("Renaming %s -> %s", old, new);
+
+	// Strip path, validate just the new basename
+	const char *new_basename = strrchr(new, '/');
+	time_t new_created_at = timestamp_from_filename(new);
+
+	if (new_created_at == INVALID_TIME) {
+		LOG("Invalid filename format: %s", new_basename);
+		return -EINVAL;
+	}
+
+	int res = do_journal_entry_rename(new_created_at, old, new);
+
+	pthread_mutex_unlock(&entries_mutex);
+	return res;
+}
 
 void update_fuse_entry_from_original(const char *filename)
 {
 	pthread_mutex_lock(&entries_mutex);
 	const char *base_name = strrchr(filename, '/');
-	base_name = base_name ? base_name + 1 : filename;
 	journal_entry *entry = hashmap_get(entries_original_key, base_name);
 	// TODO: Create entry when file is not found
 	if (!entry) {
@@ -844,6 +967,19 @@ void *watch_ics_dir(void *arg)
 	close(fd);
 	return NULL;
 }
+int journal_create(const char *file_name, mode_t mode,
+		   struct fuse_file_info *info)
+{
+	return -EIO;
+}
+
+static const struct fuse_operations journal_oper = {.getattr = journal_getattr,
+						    .readdir = journal_readdir,
+						    .open = journal_open,
+						    .read = journal_read,
+						    .write = journal_write,
+						    .create = journal_create,
+						    .rename = journal_rename};
 
 int main(int argc, char *argv[])
 {
@@ -855,7 +991,8 @@ int main(int argc, char *argv[])
 	}
 
 	if (compile_regexes(&regex_summary, &regex_description, &regex_dtstamp,
-			    &regex_lastmod, &regex_uid) != 0) {
+			    &regex_lastmod, &regex_uid,
+			    &regex_fuse_file) != 0) {
 		fprintf(stderr, "Failed to compile regular expressions\n");
 		return -1;
 	}
@@ -877,6 +1014,7 @@ int main(int argc, char *argv[])
 	regfree(&regex_dtstamp);
 	regfree(&regex_lastmod);
 	regfree(&regex_uid);
+	regfree(&regex_fuse_file);
 	LOG("Regexp freed");
 	return ret;
 }