agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 15bb8424d696e8fa99fc53f78389024c9b64c6d3
parent 8445ac56d625bfa81e154b3d1378ce4d06345fe2
Author: Marc Coquand <marc@coquand.email>
Date: Sun, 1 Jun 2025 13:56:04 +0100
Add journal rename
Diffstat:
| M | main.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(®ex_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(®ex_summary, ®ex_description, ®ex_dtstamp,
- ®ex_lastmod, ®ex_uid) != 0) {
+ ®ex_lastmod, ®ex_uid,
+ ®ex_fuse_file) != 0) {
fprintf(stderr, "Failed to compile regular expressions\n");
return -1;
}
@@ -877,6 +1014,7 @@ int main(int argc, char *argv[])
regfree(®ex_dtstamp);
regfree(®ex_lastmod);
regfree(®ex_uid);
+ regfree(®ex_fuse_file);
LOG("Regexp freed");
return ret;
}