agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 27ec9083093cf039d9b0412b3333df16d2bfe4ec
parent 5622b3d0f8743e8faceef9ae6edeeda35235ed9a
Author: Marc Coquand <marc@coquand.email>
Date: Thu, 5 Jun 2025 20:08:54 +0100
Add slightly hacky restructure to handle today vs journal notes
Later on we should have a more robust lookup solution for these
symlinks, as they will have subdirectories and alternative names.
Diffstat:
| M | main.c | | | 196 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
1 file changed, 134 insertions(+), 62 deletions(-)
diff --git a/main.c b/main.c
@@ -91,26 +91,38 @@ journal_entry *
get_entry_from_fuse_path(const char *path)
{
if (!path || path[0] != '/') {
- LOG("invalid path '%s'\n", path ? path : "NULL");
+ LOG("Invalid path: '%s'\n", path ? path : "NULL");
return NULL;
}
- const char *key = path + 1;
+ const char *prefix = "/journal/";
+ size_t prefix_len = strlen(prefix);
+
+ // Must start with "/journal/"
+ if (strncmp(path, prefix, prefix_len) != 0) {
+ LOG("Path does not start with '/journal/': '%s'\n", path);
+ return NULL;
+ }
+
+ // Extract the part after "/journal/"
+ const char *key = path + prefix_len;
+
+ // If key is empty (i.e., path is exactly "/journal/"), reject
if (strlen(key) == 0) {
- LOG("empty key for path '%s'\n", path);
+ LOG("Empty key in path: '%s'\n", path);
return NULL;
}
- LOG("looking up key '%s'\n", key);
+ LOG("Looking up key: '%s'\n", key);
journal_entry *entry = hashmap_get(entries_fuse_key, key);
if (!entry) {
- LOG("no entry found for key '%s'\n", key);
+ LOG("No entry found for key: '%s'\n", key);
return NULL;
}
- LOG("Entry is %s", entry->filename);
- LOG("Entry original is %s", entry->filename_original);
+ LOG("Entry found: filename = '%s', original = '%s'\n", entry->filename,
+ entry->filename_original);
return entry;
}
@@ -177,43 +189,6 @@ time_string(icaltimetype t, const char *format)
return buffer;
}
-// Uses created_at as filename.
-// However, in case of collission, set the add 1,2 et.c.
-char *
-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");
- return NULL;
- }
-
- // Attempt filename and increment suffix if needed
- char *unique_filename = NULL;
- if (asprintf(&unique_filename, "%s.txt", filename_time_str) == -1) {
- free(filename_time_str);
- return NULL;
- }
-
- // In the rare cases that some entry is created on the same minute.
- int suffix = 1;
- while (hashmap_get(entries_map, unique_filename) != NULL) {
- suffix++;
- free(unique_filename);
- if (asprintf(&unique_filename, "%s_%d.txt", filename_time_str,
- suffix) == -1) {
- return NULL;
- }
- }
-
- free(filename_time_str);
-
- return unique_filename;
-}
-
// Returns -1 on failure, 0 on success
int
load_journal_entry(char *filename, journal_entry *entry)
@@ -240,16 +215,11 @@ load_journal_entry(char *filename, journal_entry *entry)
// Make sure filename, which happens when
// Two entries are created on the same minute
- char *unique_filename = get_unique_filename(entry, entries_fuse_key);
- if (!unique_filename) {
- LOG("Failed to generate filename for %s", filename);
-
- free(filepath);
+ if (asprintf(&entry->filename, "%s.txt",
+ icalcomponent_get_uid(entry->component)) == -1) {
+ LOG("Failed to write filename");
return -1;
- }
-
- entry->filename = unique_filename;
- LOG("Got unique filename %s", unique_filename);
+ };
free(filepath);
return 0;
@@ -359,11 +329,45 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
int ret_code = 0;
if (strcmp(path, "/") == 0) {
- stbuf->st_mode = S_IFDIR | 0755;
+ stbuf->st_mode = S_IFDIR | 0444;
+ stbuf->st_nlink = 2;
+ goto unlock_and_return;
+ }
+
+ if (strcmp(path, "/journal") == 0) {
+ stbuf->st_mode = S_IFDIR | 0444;
+ stbuf->st_nlink = 2;
+ goto unlock_and_return;
+ }
+
+ if (strcmp(path, "/today") == 0) {
+ stbuf->st_mode = S_IFDIR | 0444;
stbuf->st_nlink = 2;
goto unlock_and_return;
}
+ const char *today_prefix = "/today/";
+ if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
+ const char *key = path + strlen(today_prefix);
+ journal_entry *entry = hashmap_get(entries_fuse_key, key);
+
+ if (!entry) {
+ ret_code = -ENOENT;
+ goto unlock_and_return;
+ }
+
+ icalcomponent *component = entry->component;
+ struct icaltimetype dt = icalcomponent_get_dtstamp(component);
+ if (icaltime_compare_date_only(dt, icaltime_today()) != 0) {
+ ret_code = -ENOENT;
+ goto unlock_and_return;
+ }
+
+ stbuf->st_mode = S_IFLNK | 0444;
+ stbuf->st_nlink = 1;
+ goto unlock_and_return;
+ }
+
journal_entry *entry = get_entry_from_fuse_path(path);
if (!entry) {
LOG("Entry not found");
@@ -394,6 +398,28 @@ unlock_and_return:
}
int
+journal_readlink(const char *path, char *buf, size_t size)
+{
+ LOG("READLINK path: %s", path);
+ const char *today_prefix = "/today/";
+ if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
+ pthread_mutex_lock(&entries_mutex);
+ const char *key = path + strlen(today_prefix);
+ LOG("Lookup %s", key);
+ journal_entry *entry = hashmap_get(entries_fuse_key, key);
+
+ if (!entry)
+ return -ENOENT;
+
+ // Return symlink target
+ snprintf(buf, size, "../journal/%s", key);
+
+ pthread_mutex_unlock(&entries_mutex);
+ return 0;
+ }
+ return -ENOENT;
+}
+int
journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi,
enum fuse_readdir_flags flags)
@@ -401,20 +427,62 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
pthread_mutex_lock(&entries_mutex);
LOG("READDIR %s", path);
+ filler(buf, ".", NULL, 0, 0);
+ filler(buf, "..", NULL, 0, 0);
- size_t n_keys = 0;
- char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
- if (!keys) {
- pthread_mutex_unlock(&entries_mutex);
- return -ENOMEM;
+ // If we are in the root directory, expose the "journal" directory
+ if (strcmp(path, "/") == 0) {
+ filler(buf, "journal", NULL, 0, 0);
+ filler(buf, "today", NULL, 0, 0);
}
+ else if (strcmp(path, "/journal") == 0) {
+ size_t n_keys = 0;
+ char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
+ if (!keys) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -ENOMEM;
+ }
+
+ for (size_t i = 0; i < n_keys; i++) {
+ filler(buf, keys[i], NULL, 0, 0);
+ }
- for (size_t i = 0; i < n_keys; i++) {
- filler(buf, keys[i], NULL, 0, 0);
+ hashmap_free_keys(keys, n_keys);
}
+ else if (strcmp(path, "/today") == 0) {
+ time_t now = time(NULL);
+ struct tm *now_tm = localtime(&now);
+
+ size_t n_keys = 0;
+ char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
+ if (!keys) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -ENOMEM;
+ }
- hashmap_free_keys(keys, n_keys);
+ for (size_t i = 0; i < n_keys; i++) {
+ journal_entry *entry =
+ hashmap_get(entries_fuse_key, keys[i]);
+ if (!entry)
+ continue;
+
+ // Get the dtstamp of the first icalcomponent
+ icalcomponent *component = entry->component;
+ if (!component)
+ continue;
+
+ struct icaltimetype dt =
+ icalcomponent_get_dtstamp(component);
+ if (dt.year == (now_tm->tm_year + 1900) &&
+ dt.month == (now_tm->tm_mon + 1) &&
+ dt.day == now_tm->tm_mday) {
+ filler(buf, keys[i], NULL, 0,
+ 0); // same name as in /journal
+ }
+ }
+ hashmap_free_keys(keys, n_keys);
+ }
pthread_mutex_unlock(&entries_mutex);
return 0;
}
@@ -529,6 +597,8 @@ write_entry_to_ical_file(const journal_entry *entry)
icalproperty_set_lastmodified(prop, get_ical_now());
}
+ icalcomponent_set_status(entry->component, ICAL_STATUS_FINAL);
+
LOG("Set last modified");
char *ical_str = icalcomponent_as_ical_string_r(entry->component);
@@ -945,6 +1015,8 @@ static const struct fuse_operations journal_oper = {.getattr = journal_getattr,
.write = journal_write,
.create = journal_create,
.unlink = journal_unlink,
+ .readlink =
+ journal_readlink,
.rename = journal_rename};
size_t