agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit d0566a84d00d523038098fa603e60259f4fa3eee
parent 5fbe24b3c74068345e4a69342db653b5bdda759a
Author: Marc Coquand <marc@coquand.email>
Date: Sun, 15 Jun 2025 11:23:53 +0100
Refactor out journal_entry (1)
Diffstat:
| M | Makefile | | | 8 | ++++---- |
| A | journal_entry.c | | | 584 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | journal_entry.h | | | 125 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | main.c | | | 679 | ++----------------------------------------------------------------------------- |
| M | util.h | | | 7 | +++++++ |
5 files changed, 730 insertions(+), 673 deletions(-)
diff --git a/Makefile b/Makefile
@@ -5,11 +5,11 @@ LIBS = `pkg-config uuid fuse3 libical --cflags --libs`
all: caldavfs
-caldavfs: main.c util.c hashmap.c
- $(CC) $(CFLAGS) main.c util.c hashmap.c -o mount_caldavfs $(LIBS)
+caldavfs: main.c util.c hashmap.c journal_entry.c
+ $(CC) $(CFLAGS) main.c journal_entry.c util.c hashmap.c -o mount_caldavfs $(LIBS)
-debug: main.c util.c hashmap.c
- $(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) main.c util.c hashmap.c -o mount_caldavfs-debug $(LIBS)
+debug: main.c util.c hashmap.c journal_entry.c
+ $(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) main.c journal_entry.c util.c hashmap.c -o mount_caldavfs-debug $(LIBS)
clean:
rm -f caldavfs caldavfs-debug
diff --git a/journal_entry.c b/journal_entry.c
@@ -0,0 +1,584 @@
+#include "journal_entry.h"
+
+#include "hashmap.h"
+#include "util.h"
+#include <dirent.h>
+#include <errno.h>
+#include <libical/ical.h>
+#include <pthread.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <uuid/uuid.h>
+#include <wordexp.h>
+
+struct hashmap *entries_fuse = NULL;
+
+char ICS_DIR[256];
+
+// journal_entry
+struct hashmap *entries_original_ics = NULL;
+
+// string -> string
+struct hashmap *entries_today = NULL;
+
+struct hashmap *entries_notes = NULL;
+// Full filepath of original ics file
+char *
+get_original_filepath(const struct journal_entry *entry)
+{
+ char *filepath = NULL;
+ asprintf(&filepath, "%s/%s", ICS_DIR, entry->filename_original);
+ return filepath;
+}
+
+icaltimetype
+get_ical_now()
+{
+ return icaltime_from_timet_with_zone(time(NULL), 0,
+ icaltimezone_get_utc_timezone());
+}
+
+void
+free_journal_entry(void *val)
+{
+ struct journal_entry *entry = (struct journal_entry *)val;
+ if (!entry)
+ return;
+ if (entry->component)
+ icalcomponent_free(entry->component);
+ if (entry->filename)
+ free(entry->filename);
+ if (entry->filename_original)
+ free(entry->filename_original);
+ free(entry);
+}
+
+char *
+get_filename(const char *full_path)
+{
+ char *base_name = strrchr(full_path, '/');
+ if (!base_name)
+ return (char *)full_path;
+ return base_name + 1;
+}
+
+struct journal_entry *
+get_entry_from_fuse_path(const char *path)
+{
+ if (!path || path[0] != '/') {
+ LOG("Invalid path: '%s'\n", path ? path : "NULL");
+ return NULL;
+ }
+
+ const char *prefix = "/entries/";
+ size_t prefix_len = strlen(prefix);
+
+ if (strncmp(path, prefix, prefix_len) != 0) {
+ LOG("Path does not start with '/entries/': '%s'\n", path);
+ return NULL;
+ }
+
+ // Extract the part after "/entries/"
+ const char *key = path + prefix_len;
+
+ // If key is empty (i.e., path is exactly "/entries/"), reject
+ if (strlen(key) == 0) {
+ LOG("Empty key in path: '%s'\n", path);
+ return NULL;
+ }
+
+ LOG("Looking up key: '%s'", key);
+ struct journal_entry *entry = hashmap_get(entries_fuse, key);
+
+ if (!entry) {
+ LOG("No entry found for key: '%s'\n", key);
+ return NULL;
+ }
+
+ LOG("Entry found: filename = '%s', original = '%s', path = '%s'",
+ entry->filename, entry->filename_original, path);
+ return entry;
+}
+
+char *
+read_stream(char *s, size_t size, void *d)
+{
+ return fgets(s, (int)size, (FILE *)d);
+}
+
+size_t
+parse_entry_content(struct journal_entry *entry, const char *buf, size_t size)
+{
+
+ char *description = NULL;
+ asprintf(&description, "%s", buf);
+
+ icalcomponent_set_description(entry->component, description);
+ return 0;
+}
+
+icalcomponent *
+parse_ics_file(const char *filename)
+{
+ icalcomponent *component = NULL;
+ FILE *stream = fopen(filename, "r");
+ assert(stream != 0);
+
+ icalparser *parser = icalparser_new();
+ icalparser_set_gen_data(parser, stream);
+
+ char *line;
+ do {
+ line = icalparser_get_line(parser, read_stream);
+ icalcomponent *temp = icalparser_add_line(parser, line);
+ if (temp != NULL && component == NULL) {
+ component = temp; // Save the first completed component
+ }
+ } while (line != NULL);
+
+ fclose(stream);
+ icalparser_free(parser);
+ return component;
+}
+
+// We assume that there is only one entry in the file
+int
+parse_ics_to_journal_entry_component(const char *filename,
+ struct journal_entry *entry)
+{
+ icalcomponent *component = parse_ics_file(filename);
+ if (component == NULL) {
+ LOG("No component");
+ return -1;
+ }
+ // Verify if necessary?
+ icalcomponent *journal =
+ icalcomponent_get_first_real_component(component);
+ if (journal == NULL) {
+ LOG("No journal component");
+ return -2;
+ }
+ entry->component = component;
+ return 0;
+}
+
+char *
+time_string(icaltimetype t, const char *format)
+{
+
+ char *buffer = xmalloc(64);
+ time_t tme = icaltime_as_timet(t);
+ struct tm tm_info;
+ localtime_r(&tme, &tm_info);
+
+ strftime(buffer, 64, format, &tm_info);
+ return buffer;
+}
+
+// Returns -1 on failure, 0 on success
+int
+load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry)
+{
+ char *filepath = NULL;
+ if (asprintf(&filepath, "%s/%s", ICS_DIR, filename) == -1) {
+ perror("aspintf failed");
+ return -1;
+ }
+ entry->filename_original = strdup(filename);
+ if (!entry->filename_original) {
+ perror("strdup failed");
+ free(filepath);
+ return -1;
+ }
+
+ if (parse_ics_to_journal_entry_component(filepath, entry) != 0) {
+ LOG("Failed to parse entry: %s", filename);
+ free(filepath);
+ return -1;
+ }
+
+ // Make sure filename, which happens when
+ // Two entries are created on the same minute
+ // TODO: Allow custom fileformats
+ if (asprintf(&entry->filename, "%s.txt",
+ icalcomponent_get_summary(entry->component)) == -1) {
+ LOG("Failed to write filename");
+ return -1;
+ };
+ LOG("Parsed %s to file %s", entry->filename_original, entry->filename);
+
+ free(filepath);
+ return 0;
+}
+
+// Initializes
+// entries_original_key
+// entries_fuse_key
+// Should only be run once
+void
+load_journal_entries()
+{
+ LOG("Allocating journal entries");
+ if (entries_original_ics) {
+ hashmap_free(entries_original_ics);
+ entries_original_ics = NULL;
+ }
+ if (entries_fuse) {
+ hashmap_free(entries_fuse);
+ entries_fuse = NULL;
+ }
+ entries_original_ics = hashmap_new(NULL);
+ entries_fuse = hashmap_new(free_journal_entry);
+
+ LOG("Loading journal entries from: %s\n", ICS_DIR);
+
+ DIR *dir = opendir(ICS_DIR);
+ if (!dir) {
+ perror("opendir");
+ return;
+ }
+
+ struct dirent *entry;
+ while ((entry = readdir(dir))) {
+ if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) {
+ struct journal_entry *new_entry =
+ xcalloc(1, sizeof(struct journal_entry));
+
+ if (load_journal_entry_from_ics_file(entry->d_name,
+ new_entry) != 0) {
+ LOG("FAILED TO LOAD JOURNAL ENTRY");
+ free_journal_entry(new_entry);
+ continue;
+ }
+
+ if (hashmap_insert(entries_fuse, new_entry->filename,
+ new_entry) != 0 ||
+ hashmap_insert(entries_original_ics,
+ new_entry->filename_original,
+ new_entry) != 0) {
+ LOG("Failed to insert entry");
+ free_journal_entry(new_entry);
+ continue;
+ }
+ }
+ }
+ closedir(dir);
+}
+
+size_t
+write_entry_content(char *buf, struct journal_entry *entry)
+{
+ // Allocate memory for the content
+ size_t size = asprintf(&buf, "%s",
+ icalcomponent_get_description(entry->component));
+ if (size == -1) {
+ LOG("Out of memory");
+ exit(1);
+ }
+ return size;
+}
+
+size_t
+get_entry_content_size(struct journal_entry *entry)
+{
+ size_t size = snprintf(NULL, 0, "%s",
+ icalcomponent_get_description(entry->component));
+ if (size == -1) {
+ LOG("Out of mem");
+ exit(1);
+ }
+ return size;
+}
+
+// Gets last modified, and if not exists, falls back to dtstamp
+icaltimetype
+get_last_modified(struct journal_entry *entry)
+{
+ icalproperty *last_modified_prop = icalcomponent_get_next_property(
+ entry->component, ICAL_LASTMODIFIED_PROPERTY);
+
+ if (last_modified_prop) {
+ icaltimetype ical_lastmodified =
+ icalproperty_get_lastmodified(last_modified_prop);
+ return ical_lastmodified;
+ }
+ else {
+ return icalcomponent_get_dtstamp(entry->component);
+ }
+}
+
+// TODO: Probably better to calculate this on demand
+int
+build_today_hashmap()
+{
+ if (entries_today != NULL)
+ hashmap_free(entries_today);
+ entries_today = hashmap_new(free);
+
+ size_t n_keys = 0;
+ char **keys = hashmap_get_keys(entries_fuse, &n_keys);
+
+ if (!keys) {
+ LOG("Could not get keys");
+ return -1;
+ }
+
+ for (size_t i = 0; i < n_keys; i++) {
+ struct journal_entry *entry =
+ hashmap_get(entries_fuse, 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 (icaltime_compare_date_only(dt, icaltime_today()) == 0) {
+ const char *summary =
+ icalcomponent_get_summary(component);
+ if (summary == NULL) {
+ LOG("SUMMARY IS NULL");
+ continue;
+ }
+ char *filename;
+
+ asprintf(&filename, "%s", summary);
+
+ if (filename == NULL) {
+ LOG("Could not get filename");
+ continue;
+ }
+
+ char *dest;
+
+ if (asprintf(&dest, "../entries/%s", entry->filename) ==
+ -1)
+ return -1;
+
+ hashmap_insert(entries_today, filename, dest);
+ }
+ }
+ LOG("Build complete");
+
+ hashmap_free_keys(keys, n_keys);
+ return 0;
+}
+
+int
+build_notes_hashmap()
+{
+ if (entries_notes != NULL)
+ hashmap_free(entries_notes);
+ entries_notes = hashmap_new(free);
+ LOG("BUILDING ENTRIES");
+
+ size_t n_keys = 0;
+ char **keys = hashmap_get_keys(entries_fuse, &n_keys);
+
+ if (!keys) {
+ LOG("Could not get keys");
+ return -1;
+ }
+ LOG("GOT ENTRIES");
+
+ for (size_t i = 0; i < n_keys; i++) {
+ struct journal_entry *entry =
+ hashmap_get(entries_fuse, keys[i]);
+
+ LOG("Looking up entry");
+ if (!entry)
+ continue;
+
+ // Get the dtstamp of the first icalcomponent
+ icalcomponent *component = entry->component;
+ if (!component)
+ continue;
+
+ icalcomponent_kind type =
+ icalcomponent_isa(icalcomponent_get_inner(component));
+
+ if (type == ICAL_VJOURNAL_COMPONENT) {
+ const char *summary =
+ icalcomponent_get_summary(component);
+ if (summary == NULL) {
+ LOG("SUMMARY IS NULL");
+ continue;
+ }
+ char *filename;
+ asprintf(&filename, "%s", summary);
+
+ if (filename == NULL) {
+ LOG("Could not get filename");
+ continue;
+ }
+
+ char *dest;
+
+ if (asprintf(&dest, "../entries/%s", entry->filename) ==
+ -1)
+ return -1;
+
+ hashmap_insert(entries_notes, filename, dest);
+ }
+ }
+ LOG("Build complete");
+
+ hashmap_free_keys(keys, n_keys);
+ return 0;
+}
+
+struct journal_entry *
+copy_journal_entry(const struct journal_entry *src)
+{
+ struct journal_entry *copy = xcalloc(1, sizeof(struct journal_entry));
+
+ copy->filename = strdup(src->filename);
+ copy->filename_original = strdup(src->filename_original);
+ copy->component = icalcomponent_new_clone(src->component);
+
+ return copy;
+}
+
+size_t
+write_entry_to_ical_file(const struct journal_entry *entry)
+{
+
+ char *filepath = get_original_filepath(entry);
+
+ // Always set last modified to the time we write
+ icalproperty *prop = icalcomponent_get_first_property(
+ entry->component, ICAL_LASTMODIFIED_PROPERTY);
+ if (!prop) {
+ prop = icalproperty_new_lastmodified(get_ical_now());
+ icalcomponent_add_property(entry->component, prop);
+ }
+ else {
+ 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);
+
+ LOG("Got ical_str %s", ical_str);
+
+ FILE *f = fopen(filepath, "w");
+ if (!f) {
+ perror("Failed to open ICS file");
+ free(ical_str);
+ free(filepath);
+ return -EIO;
+ }
+
+ fputs(ical_str, f);
+ fclose(f);
+ free(ical_str);
+ free(filepath);
+
+ return 0;
+}
+size_t
+load_caldavfs_environment()
+{
+ char *ics_dir_env = getenv("CALDAVFS_ICS_DIR");
+
+ if (ics_dir_env == NULL) {
+ fprintf(
+ stderr,
+ "Must set CALDAVFS_ICS_DIR to the absolute path of your "
+ "calendar. See README.\n");
+ return -1;
+ }
+
+ wordexp_t expanded;
+ if (wordexp(ics_dir_env, &expanded, 0) != 0 || expanded.we_wordc == 0) {
+ fprintf(stderr, "Failed to expand CALDAVFS_ICS_DIR\n");
+ wordfree(&expanded);
+ return -1;
+ }
+
+ char *expanded_path = expanded.we_wordv[0];
+
+ struct stat s;
+ if (stat(expanded_path, &s) == 0 && S_ISDIR(s.st_mode)) {
+ strlcpy(ICS_DIR, expanded_path, sizeof(ICS_DIR));
+ return 0;
+ }
+
+ fprintf(stderr, "CALDAVFS_ICS_DIR is not a directory: %s\n",
+ expanded_path);
+ return -1;
+}
+
+int
+do_journal_entry_rename(const char *old, const char *new)
+{
+ char *new_copy = strdup(new);
+ if (!new_copy)
+ return -ENOMEM;
+ char *new_filename = get_filename(new_copy);
+
+ char *old_filename = get_filename(old);
+ if (!old_filename)
+ return -EINVAL;
+
+ char *new_summary = strdup(new_filename);
+
+ // Remove everything after extension
+ // TODO: Store the extension as a separate field in the ics file
+ char *dot = strrchr(new_summary, '.');
+ if (dot) {
+ *dot = '\0';
+ }
+ LOG("Summary is now %s", new_summary);
+
+ struct journal_entry *old_entry = get_entry_from_fuse_path(old);
+ if (!old_entry) {
+ LOG("Entry not found '%s'", old);
+ free(new_copy);
+ free(new_summary);
+ return -EIO;
+ }
+
+ struct journal_entry *new_entry = copy_journal_entry(old_entry);
+ if (!new_entry) {
+ free(new_copy);
+ free(new_summary);
+ return -EIO;
+ }
+
+ icalcomponent_set_summary(new_entry->component, new_summary);
+ // TODO: Filename must be unique
+ asprintf(&new_entry->filename, "%s", new_filename);
+ LOG("New filename is now %s", new_filename);
+
+ LOG("Removed original entry from fuse");
+
+ LOG("Inserting %s", new_entry->filename_original);
+ hashmap_insert(entries_original_ics, new_entry->filename_original,
+ new_entry);
+
+ hashmap_insert(entries_fuse, new_entry->filename, new_entry);
+
+ // Insert original ics first to avoid use after free
+ // Insert frees older entry
+ hashmap_remove(entries_fuse, old_entry->filename);
+
+ write_entry_to_ical_file(new_entry);
+ free(new_copy);
+
+ return 0;
+}
+
diff --git a/journal_entry.h b/journal_entry.h
@@ -0,0 +1,125 @@
+#ifndef JOURNAL_ENTRY_H
+#define JOURNAL_ENTRY_H
+#include "hashmap.h"
+#include "util.h"
+#include <dirent.h>
+#include <libical/ical.h>
+#include <pthread.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <uuid/uuid.h>
+#include <wordexp.h>
+
+// TODO: Move this to a file structure to a separate module, refactor all
+// operations to use the module exposed files
+// Such as
+// - rename
+// - remove
+// - get filename
+// et.c.
+struct journal_entry {
+ // filename is full path relative to fuse directory
+ // I.E. hello world.txt
+ char *filename;
+ // filename_original is the relative path to ICS_DIR
+ // I.E. 910319208nrao19p.ics
+ char *filename_original;
+ icalcomponent *component;
+};
+
+extern char ICS_DIR[256];
+
+// We create two maps that link to the same entries
+// journal_entry
+extern struct hashmap *entries_fuse;
+
+// journal_entry
+extern struct hashmap *entries_original_ics;
+
+// string -> string
+extern struct hashmap *entries_today;
+
+// string -> string
+extern struct hashmap *entries_notes;
+
+// Full filepath of original ics file
+char *
+get_original_filepath(const struct journal_entry *entry);
+
+void
+free_journal_entry(void *val);
+
+char *
+get_filename(const char *full_path);
+
+struct journal_entry *
+get_entry_from_fuse_path(const char *path);
+
+icalcomponent *
+parse_ics_file(const char *filename);
+
+// We assume that there is only one entry in the file
+int
+parse_ics_to_journal_entry_component(const char *filename,
+ struct journal_entry *entry);
+
+int
+do_journal_entry_rename(const char *old, const char *new_file);
+
+size_t
+parse_entry_content(struct journal_entry *entry, const char *buf, size_t size);
+
+size_t
+write_entry_to_ical_file(const struct journal_entry *entry);
+
+// Returns -1 on failure, 0 on success
+int
+load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry);
+
+// We assume that there is only one entry in the file
+int
+parse_ics_to_journal_entry_component(const char *filename,
+ struct journal_entry *entry);
+
+// Returns -1 on failure, 0 on success
+int
+load_journal_entry_from_ics_file(char *filename, struct journal_entry *entry);
+
+// Initializes
+// entries_original_key
+// entries_fuse_key
+// Should only be run once
+void
+load_journal_entries();
+
+icaltimetype
+get_ical_now();
+
+size_t
+write_entry_content(char *buf, struct journal_entry *entry);
+
+size_t
+get_entry_content_size(struct journal_entry *entry);
+
+// Gets last modified, and if not exists, falls back to dtstamp
+icaltimetype
+get_last_modified(struct journal_entry *entry);
+
+// TODO: Probably better to calculate this on demand
+int
+build_today_hashmap();
+
+int
+build_notes_hashmap();
+
+size_t
+load_caldavfs_environment();
+
+#endif // JOURNAL_ENTRY_H
diff --git a/main.c b/main.c
@@ -1,6 +1,7 @@
#define FUSE_USE_VERSION 31
#include "hashmap.h"
+#include "journal_entry.h"
#include "util.h"
#include <dirent.h>
#include <errno.h>
@@ -18,457 +19,11 @@
#include <unistd.h>
#include <uuid/uuid.h>
#include <wordexp.h>
-#define LOG(fmt, ...) \
- fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
// Listening to file changes of the original content
#define EVENT_BUF_LEN (1024 * (sizeof(struct inotify_event) + 16))
-char ICS_DIR[256];
-
-const char *date_title_fmt = "%Y%m%d-%H:%M";
-regex_t regex_fuse_file;
-
-time_t INVALID_TIME = (time_t)-1;
-
-icaltimezone *local_ical_time;
-
-// TODO: Move this to a file structure to a separate module, refactor all
-// operations to use the module exposed files
-// Such as
-// - rename
-// - remove
-// - get filename
-// et.c.
-typedef struct {
- // filename is full path relative to fuse directory
- // I.E. hello world.txt
- char *filename;
- // filename_original is full path relative to fuse directory
- // I.E. 910319208nrao19p.ics
- char *filename_original;
- icalcomponent *component;
-} journal_entry;
-
-// We create two maps that link to the same entries
-// journal_entry
-struct hashmap *entries_fuse = NULL;
-
-// journal_entry
-struct hashmap *entries_original_ics = NULL;
-
-// string -> string
-struct hashmap *entries_today = NULL;
-
-// string -> string
-struct hashmap *entries_notes = NULL;
-
static pthread_mutex_t entries_mutex = PTHREAD_MUTEX_INITIALIZER;
-icaltimetype
-get_ical_now()
-{
- return icaltime_from_timet_with_zone(time(NULL), 0,
- icaltimezone_get_utc_timezone());
-}
-
-// Full filepath of original ics file
-char *
-get_original_filepath(const journal_entry *entry)
-{
- char *filepath = NULL;
- asprintf(&filepath, "%s/%s", ICS_DIR, entry->filename_original);
- return filepath;
-}
-
-void
-free_journal_entry(void *val)
-{
- journal_entry *entry = (journal_entry *)val;
- if (!entry)
- return;
- if (entry->component)
- icalcomponent_free(entry->component);
- if (entry->filename)
- free(entry->filename);
- if (entry->filename_original)
- free(entry->filename_original);
- free(entry);
-}
-
-char *
-get_filename(const char *full_path)
-{
- char *base_name = strrchr(full_path, '/');
- if (!base_name)
- return (char *)full_path;
- return base_name + 1;
-}
-int
-compile_regexes(regex_t *regex_fuse_file)
-{
- if (regcomp(regex_fuse_file,
- "^([0-9]{8})-([0-9]{2}:[0-9]{2})(_[^/\\:*?\"<>|]+)?\\.txt$",
- REG_EXTENDED) != 0)
- return -1;
- return 0;
-}
-
-journal_entry *
-get_entry_from_fuse_path(const char *path)
-{
- if (!path || path[0] != '/') {
- LOG("Invalid path: '%s'\n", path ? path : "NULL");
- return NULL;
- }
-
- const char *prefix = "/entries/";
- size_t prefix_len = strlen(prefix);
-
- if (strncmp(path, prefix, prefix_len) != 0) {
- LOG("Path does not start with '/entries/': '%s'\n", path);
- return NULL;
- }
-
- // Extract the part after "/entries/"
- const char *key = path + prefix_len;
-
- // If key is empty (i.e., path is exactly "/entries/"), reject
- if (strlen(key) == 0) {
- LOG("Empty key in path: '%s'\n", path);
- return NULL;
- }
-
- LOG("Looking up key: '%s'", key);
- journal_entry *entry = hashmap_get(entries_fuse, key);
-
- if (!entry) {
- LOG("No entry found for key: '%s'\n", key);
- return NULL;
- }
-
- LOG("Entry found: filename = '%s', original = '%s', path = '%s'",
- entry->filename, entry->filename_original, path);
- return entry;
-}
-
-char *
-read_stream(char *s, size_t size, void *d)
-{
- return fgets(s, (int)size, (FILE *)d);
-}
-
-icalcomponent *
-parse_ics_file(const char *filename)
-{
- icalcomponent *component = NULL;
- FILE *stream = fopen(filename, "r");
- assert(stream != 0);
-
- icalparser *parser = icalparser_new();
- icalparser_set_gen_data(parser, stream);
-
- char *line;
- do {
- line = icalparser_get_line(parser, read_stream);
- icalcomponent *temp = icalparser_add_line(parser, line);
- if (temp != NULL && component == NULL) {
- component = temp; // Save the first completed component
- }
- } while (line != NULL);
-
- fclose(stream);
- icalparser_free(parser);
- return component;
-}
-
-// We assume that there is only one entry in the file
-int
-parse_ics_to_journal_entry_component(const char *filename, journal_entry *entry)
-{
- icalcomponent *component = parse_ics_file(filename);
- if (component == NULL) {
- LOG("No component");
- return -1;
- }
- // Verify if necessary?
- icalcomponent *journal =
- icalcomponent_get_first_real_component(component);
- if (journal == NULL) {
- LOG("No journal component");
- return -2;
- }
- entry->component = component;
- return 0;
-}
-
-char *
-time_string(icaltimetype t, const char *format)
-{
-
- char *buffer = xmalloc(64);
- time_t tme = icaltime_as_timet(t);
- struct tm tm_info;
- localtime_r(&tme, &tm_info);
-
- strftime(buffer, 64, format, &tm_info);
- return buffer;
-}
-
-// Returns -1 on failure, 0 on success
-int
-load_journal_entry_from_ics_file(char *filename, journal_entry *entry)
-{
- char *filepath = NULL;
- if (asprintf(&filepath, "%s/%s", ICS_DIR, filename) == -1) {
- perror("aspintf failed");
- return -1;
- }
- entry->filename_original = strdup(filename);
- if (!entry->filename_original) {
- perror("strdup failed");
- free(filepath);
- return -1;
- }
-
- if (parse_ics_to_journal_entry_component(filepath, entry) != 0) {
- LOG("Failed to parse entry: %s", filename);
- free(filepath);
- return -1;
- }
-
- // Make sure filename, which happens when
- // Two entries are created on the same minute
- // TODO: Allow custom fileformats
- if (asprintf(&entry->filename, "%s.txt",
- icalcomponent_get_summary(entry->component)) == -1) {
- LOG("Failed to write filename");
- return -1;
- };
- LOG("Parsed %s to file %s", entry->filename_original, entry->filename);
-
- free(filepath);
- return 0;
-}
-
-// Initializes
-// entries_original_key
-// entries_fuse_key
-// Should only be run once
-void
-load_journal_entries()
-{
- pthread_mutex_lock(&entries_mutex);
- LOG("Allocating journal entries");
- if (entries_original_ics) {
- hashmap_free(entries_original_ics);
- entries_original_ics = NULL;
- }
- if (entries_fuse) {
- hashmap_free(entries_fuse);
- entries_fuse = NULL;
- }
- entries_original_ics = hashmap_new(NULL);
- entries_fuse = hashmap_new(free_journal_entry);
-
- LOG("Loading journal entries from: %s\n", ICS_DIR);
-
- DIR *dir = opendir(ICS_DIR);
- if (!dir) {
- perror("opendir");
- pthread_mutex_unlock(&entries_mutex);
- return;
- }
-
- struct dirent *entry;
- while ((entry = readdir(dir))) {
- if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) {
- journal_entry *new_entry =
- xcalloc(1, sizeof(journal_entry));
-
- if (load_journal_entry_from_ics_file(entry->d_name,
- new_entry) != 0) {
- LOG("FAILED TO LOAD JOURNAL ENTRY");
- free_journal_entry(new_entry);
- continue;
- }
-
- if (hashmap_insert(entries_fuse, new_entry->filename,
- new_entry) != 0 ||
- hashmap_insert(entries_original_ics,
- new_entry->filename_original,
- new_entry) != 0) {
- LOG("Failed to insert entry");
- free_journal_entry(new_entry);
- continue;
- }
- }
- }
- closedir(dir);
- pthread_mutex_unlock(&entries_mutex);
-}
-
-size_t
-write_entry_content(char *buf, journal_entry *entry)
-{
- // Allocate memory for the content
- size_t size = asprintf(&buf, "%s",
- icalcomponent_get_description(entry->component));
- if (size == -1) {
- LOG("Out of memory");
- exit(1);
- }
- return size;
-}
-
-size_t
-get_entry_content_size(journal_entry *entry)
-{
- size_t size = snprintf(NULL, 0, "%s",
- icalcomponent_get_description(entry->component));
- if (size == -1) {
- LOG("Out of mem");
- exit(1);
- }
- return size;
-}
-
-// Gets last modified, and if not exists, falls back to dtstamp
-icaltimetype
-get_last_modified(journal_entry *entry)
-{
- icalproperty *last_modified_prop = icalcomponent_get_next_property(
- entry->component, ICAL_LASTMODIFIED_PROPERTY);
-
- if (last_modified_prop) {
- icaltimetype ical_lastmodified =
- icalproperty_get_lastmodified(last_modified_prop);
- return ical_lastmodified;
- }
- else {
- return icalcomponent_get_dtstamp(entry->component);
- }
-}
-
-// TODO: Probably better to calculate this on demand
-int
-build_today_hashmap()
-{
- if (entries_today != NULL)
- hashmap_free(entries_today);
- entries_today = hashmap_new(free);
-
- size_t n_keys = 0;
- char **keys = hashmap_get_keys(entries_fuse, &n_keys);
-
- if (!keys) {
- LOG("Could not get keys");
- return -ENOMEM;
- }
-
- for (size_t i = 0; i < n_keys; i++) {
- journal_entry *entry = hashmap_get(entries_fuse, 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 (icaltime_compare_date_only(dt, icaltime_today()) == 0) {
- const char *summary =
- icalcomponent_get_summary(component);
- if (summary == NULL) {
- LOG("SUMMARY IS NULL");
- continue;
- }
- char *filename;
-
- asprintf(&filename, "%s", summary);
-
- if (filename == NULL) {
- LOG("Could not get filename");
- continue;
- }
-
- char *dest;
-
- if (asprintf(&dest, "../entries/%s", entry->filename) ==
- -1)
- return -ENOMEM;
-
- hashmap_insert(entries_today, filename, dest);
- }
- }
- LOG("Build complete");
-
- hashmap_free_keys(keys, n_keys);
- return 0;
-}
-
-int
-build_notes_hashmap()
-{
- if (entries_notes != NULL)
- hashmap_free(entries_notes);
- entries_notes = hashmap_new(free);
- LOG("BUILDING ENTRIES");
-
- size_t n_keys = 0;
- char **keys = hashmap_get_keys(entries_fuse, &n_keys);
-
- if (!keys) {
- LOG("Could not get keys");
- return -ENOMEM;
- }
- LOG("GOT ENTRIES");
-
- for (size_t i = 0; i < n_keys; i++) {
- journal_entry *entry = hashmap_get(entries_fuse, keys[i]);
-
- LOG("Looking up entry");
- if (!entry)
- continue;
-
- // Get the dtstamp of the first icalcomponent
- icalcomponent *component = entry->component;
- if (!component)
- continue;
-
- icalcomponent_kind type =
- icalcomponent_isa(icalcomponent_get_inner(component));
-
- if (type == ICAL_VJOURNAL_COMPONENT) {
- const char *summary =
- icalcomponent_get_summary(component);
- if (summary == NULL) {
- LOG("SUMMARY IS NULL");
- continue;
- }
- char *filename;
- asprintf(&filename, "%s", summary);
-
- if (filename == NULL) {
- LOG("Could not get filename");
- continue;
- }
-
- char *dest;
-
- if (asprintf(&dest, "../entries/%s", entry->filename) ==
- -1)
- return -ENOMEM;
-
- hashmap_insert(entries_notes, filename, dest);
- }
- }
- LOG("Build complete");
-
- hashmap_free_keys(keys, n_keys);
- return 0;
-}
int
journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
@@ -504,7 +59,6 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
const char *today_prefix = "/today/";
if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
const char *key = path + strlen(today_prefix);
- build_today_hashmap();
char *entry_filename = hashmap_get(entries_today, key);
if (!entry_filename) {
ret_code = -ENOENT;
@@ -519,9 +73,6 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
const char *notes_prefix = "/notes/";
if (strncmp(path, notes_prefix, strlen(notes_prefix)) == 0) {
const char *key = path + strlen(notes_prefix);
- LOG("NOTES");
- build_notes_hashmap();
- LOG("BUILT NOTES HASHMAP");
char *entry_filename = hashmap_get(entries_notes, key);
if (!entry_filename) {
ret_code = -ENOENT;
@@ -533,7 +84,7 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
goto unlock_and_return;
}
- journal_entry *entry = get_entry_from_fuse_path(path);
+ struct journal_entry *entry = get_entry_from_fuse_path(path);
if (!entry) {
LOG("Entry not found");
ret_code = -ENOENT;
@@ -686,7 +237,7 @@ journal_open(const char *path, struct fuse_file_info *fi)
{
pthread_mutex_lock(&entries_mutex);
LOG("Opening journal for %s", path);
- journal_entry *entry = get_entry_from_fuse_path(path);
+ struct journal_entry *entry = get_entry_from_fuse_path(path);
pthread_mutex_unlock(&entries_mutex);
return entry ? 0 : -ENOENT;
}
@@ -699,7 +250,7 @@ journal_read(const char *path, char *buf, size_t size, off_t offset,
pthread_mutex_lock(&entries_mutex);
LOG("READ on path %s", path);
- journal_entry *entry = get_entry_from_fuse_path(path);
+ struct journal_entry *entry = get_entry_from_fuse_path(path);
if (!entry) {
ret_code = -ENOENT;
goto unlock_and_return;
@@ -744,83 +295,12 @@ unlock_and_return:
return ret_code;
}
-size_t
-parse_entry_content(journal_entry *entry, const char *buf, size_t size)
-{
- char *copy = strndup(buf, size);
-
- char *description = NULL;
-
- char *sep = strstr(copy, "\n---\n");
-
- // Malformed input
- if (!sep) {
- free(copy);
- return -EINVAL;
- }
-
- *sep = '\0';
- description = sep + 5;
-
- char *summary = strtok(copy, "\n");
- if (!summary) {
- free(copy);
- return -EINVAL;
- }
-
- icalcomponent_set_summary(entry->component, summary);
- icalcomponent_set_description(entry->component, description);
- free(copy);
- return 0;
-}
-
-size_t
-write_entry_to_ical_file(const journal_entry *entry)
-{
-
- char *filepath = get_original_filepath(entry);
-
- // Always set last modified to the time we write
- icalproperty *prop = icalcomponent_get_first_property(
- entry->component, ICAL_LASTMODIFIED_PROPERTY);
- if (!prop) {
- prop = icalproperty_new_lastmodified(get_ical_now());
- icalcomponent_add_property(entry->component, prop);
- }
- else {
- 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);
-
- LOG("Got ical_str %s", ical_str);
-
- FILE *f = fopen(filepath, "w");
- if (!f) {
- perror("Failed to open ICS file");
- free(ical_str);
- free(filepath);
- return -EIO;
- }
-
- fputs(ical_str, f);
- fclose(f);
- free(ical_str);
- free(filepath);
-
- return 0;
-}
-
int
journal_write(const char *path, const char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
{
pthread_mutex_lock(&entries_mutex);
- journal_entry *entry = get_entry_from_fuse_path(path);
+ struct journal_entry *entry = get_entry_from_fuse_path(path);
if (!entry) {
pthread_mutex_unlock(&entries_mutex);
return -ENOENT;
@@ -841,106 +321,6 @@ journal_write(const char *path, const char *buf, size_t size, off_t offset,
return size;
}
-journal_entry *
-copy_journal_entry(const journal_entry *src)
-{
- journal_entry *copy = xcalloc(1, sizeof(journal_entry));
-
- copy->filename = strdup(src->filename);
- copy->filename_original = strdup(src->filename_original);
- copy->component = icalcomponent_new_clone(src->component);
-
- return copy;
-}
-
-time_t
-timestamp_from_filename(const char *filename)
-{
- regmatch_t matches[3];
-
- if (regexec(®ex_fuse_file, filename, 3, matches, 0) != 0)
- return INVALID_TIME;
-
- int year, month, day, hour, minute;
-
- if (sscanf(filename + matches[1].rm_so, "%4d%2d%2d", &year, &month,
- &day) != 3)
- return INVALID_TIME;
-
- if (sscanf(filename + matches[2].rm_so, "%2d:%2d", &hour, &minute) != 2)
- return INVALID_TIME;
-
- struct tm tm_time = {.tm_year = year - 1900,
- .tm_mon = month - 1,
- .tm_mday = day,
- .tm_hour = hour,
- .tm_min = minute,
- .tm_sec = 0,
- .tm_isdst = -1};
-
- return mktime(&tm_time);
-}
-
-int
-do_journal_entry_rename(const char *old, const char *new)
-{
- char *new_copy = strdup(new);
- if (!new_copy)
- return -ENOMEM;
- char *new_filename = get_filename(new_copy);
-
- char *old_filename = get_filename(old);
- if (!old_filename)
- return -EINVAL;
-
- char *new_summary = strdup(new_filename);
-
- // Remove everything after extension
- // TODO: Store the extension as a separate field in the ics file
- char *dot = strrchr(new_summary, '.');
- if (dot) {
- *dot = '\0';
- }
- LOG("Summary is now %s", new_summary);
-
- journal_entry *old_entry = get_entry_from_fuse_path(old);
- if (!old_entry) {
- LOG("Entry not found '%s'", old);
- free(new_copy);
- free(new_summary);
- return -EIO;
- }
-
- journal_entry *new_entry = copy_journal_entry(old_entry);
- if (!new_entry) {
- free(new_copy);
- free(new_summary);
- return -EIO;
- }
-
- icalcomponent_set_summary(new_entry->component, new_summary);
- // TODO: Filename must be unique
- asprintf(&new_entry->filename, "%s", new_filename);
- LOG("New filename is now %s", new_filename);
-
- LOG("Removed original entry from fuse");
-
- LOG("Inserting %s", new_entry->filename_original);
- hashmap_insert(entries_original_ics, new_entry->filename_original,
- new_entry);
-
- hashmap_insert(entries_fuse, new_entry->filename, new_entry);
-
- // Insert original ics first to avoid use after free
- // Insert frees older entry
- hashmap_remove(entries_fuse, old_entry->filename);
-
- write_entry_to_ical_file(new_entry);
- free(new_copy);
-
- return 0;
-}
-
// Aka remove or delete
int
journal_unlink(const char *file)
@@ -953,7 +333,7 @@ journal_unlink(const char *file)
const char *keyname = strrchr(file, '/');
keyname++;
- journal_entry *entry = hashmap_get(entries_fuse, keyname);
+ struct journal_entry *entry = hashmap_get(entries_fuse, keyname);
if (!entry) {
LOG("Entry does not exist");
pthread_mutex_unlock(&entries_mutex);
@@ -1075,7 +455,8 @@ journal_create(const char *filename, mode_t mode, struct fuse_file_info *info)
char *without_extension = strdup(new_basename);
remove_file_extension(without_extension);
- journal_entry *new_entry = xcalloc(1, sizeof(journal_entry));
+ struct journal_entry *new_entry =
+ xcalloc(1, sizeof(struct journal_entry));
icalcomponent *vjournal_component =
create_vjournal_entry(without_extension);
@@ -1115,7 +496,8 @@ update_or_create_fuse_entry_from_original(const char *filename)
{
pthread_mutex_lock(&entries_mutex);
char *base_name = strrchr(filename, '/');
- journal_entry *new_entry = xcalloc(1, sizeof(journal_entry));
+ struct journal_entry *new_entry =
+ xcalloc(1, sizeof(struct journal_entry));
if (load_journal_entry_from_ics_file(base_name, new_entry) != 0) {
// Parsing failed, clean up and maybe remove old entry
@@ -1239,47 +621,12 @@ static const struct fuse_operations journal_oper = {.getattr = journal_getattr,
journal_readlink,
.rename = journal_rename};
-size_t
-load_caldavfs_environment()
-{
- char *ics_dir_env = getenv("CALDAVFS_ICS_DIR");
-
- if (ics_dir_env == NULL) {
- fprintf(
- stderr,
- "Must set CALDAVFS_ICS_DIR to the absolute path of your "
- "calendar. See README.\n");
- return -1;
- }
-
- wordexp_t expanded;
- if (wordexp(ics_dir_env, &expanded, 0) != 0 || expanded.we_wordc == 0) {
- fprintf(stderr, "Failed to expand CALDAVFS_ICS_DIR\n");
- wordfree(&expanded);
- return -1;
- }
-
- char *expanded_path = expanded.we_wordv[0];
-
- struct stat s;
- if (stat(expanded_path, &s) == 0 && S_ISDIR(s.st_mode)) {
- strlcpy(ICS_DIR, expanded_path, sizeof(ICS_DIR));
- return 0;
- }
-
- fprintf(stderr, "CALDAVFS_ICS_DIR is not a directory: %s\n",
- expanded_path);
- return -1;
-}
-
int
main(int argc, char *argv[])
{
pthread_t watcher_thread;
tzset();
- const char *tzid = getenv("TZ");
- local_ical_time = icaltimezone_get_builtin_timezone(tzid);
if (load_caldavfs_environment() != 0) {
fprintf(stderr, "Failed to load ICS directory\n");
@@ -1291,11 +638,6 @@ main(int argc, char *argv[])
return 1;
}
- if (compile_regexes(®ex_fuse_file) != 0) {
- fprintf(stderr, "Failed to compile regular expressions\n");
- return -1;
- }
-
load_journal_entries();
int ret = fuse_main(argc, argv, &journal_oper, NULL);
LOG("Cleaning up");
@@ -1310,7 +652,6 @@ main(int argc, char *argv[])
hashmap_free(entries_notes);
LOG("Hashmap freed");
- regfree(®ex_fuse_file);
LOG("Regexp freed");
exit(ret);
}
diff --git a/util.h b/util.h
@@ -1,5 +1,11 @@
+#ifndef UTIL_H
+#define UTIL_H
+
#include <stdlib.h>
+#define LOG(fmt, ...) \
+ fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
+
void *
reallocarray(void *, size_t, size_t);
void *
@@ -9,3 +15,4 @@ xmalloc(size_t);
void *
xcalloc(size_t, size_t);
+#endif