agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 92da920839dd1da8652b0c143e94a184e44ed628
parent 15bb8424d696e8fa99fc53f78389024c9b64c6d3
Author: Marc Coquand <marc@coquand.email>
Date: Sun, 1 Jun 2025 15:24:30 +0100
Cleanup and rename
Diffstat:
| M | Makefile | | | 10 | +++++----- |
| M | README.md | | | 99 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
| M | main.c | | | 145 | +++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------- |
3 files changed, 186 insertions(+), 68 deletions(-)
diff --git a/Makefile b/Makefile
@@ -3,13 +3,13 @@ CFLAGS = -Wall -O3
CFLAGS_DEBUG = -Wall -g
LIBS = `pkg-config fuse3 --cflags --libs`
-all: journalfs
+all: caldavfs
-journalfs: main.c hashmap.c
- $(CC) $(CFLAGS) main.c hashmap.c -o journalfs $(LIBS)
+caldavfs: main.c hashmap.c
+ $(CC) $(CFLAGS) main.c hashmap.c -o caldavfs $(LIBS)
debug: main.c hashmap.c
- $(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) main.c hashmap.c -o journalfs-debug $(LIBS)
+ $(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) main.c hashmap.c -o caldavfs-debug $(LIBS)
clean:
- rm -f journalfs journalfs-debug
+ rm -f caldavfs caldavfs-debug
diff --git a/README.md b/README.md
@@ -1,22 +1,99 @@
-# journalfs
+# caldavfs - Mount your caldav server as a filesystem
-A true journaling file system for humans! Allows you to keep your journal
-in your CalDAV server.
+`caldavfs` is a FUSE-based filesystem written in C. Use it to manage your journal, todos, events et.c. as plain-text files that are easy to modify.
-This fuse-based filesystem integrates with a directory of ics
-to display files.
+## Motivation
-This will mount a directory of journal entries created by
-[vdirsyncer](https://vdirsyncer.pimutils.org/en/stable/index.html) and
-allow you to create journal entries with your favorite tools. `vdirsyncer`
-then syncs these back to your caldav server.
+Caldav is great in the sense that it has a lot of support from various
+app. However, the format is not particularly human-friendly. In the iCalendar
+spec, VJOURNAL entries look like this:
-Usage:
+```
+ BEGIN:VJOURNAL
+ UID:19970901T130000Z-123405@host.com
+ DTSTAMP:19970901T1300Z
+ DTSTART;VALUE=DATE:19970317
+ SUMMARY:Staff meeting minutes
+ DESCRIPTION:1. Staff meeting: Participants include Joe\, Lisa
+ and Bob. Aurora project plans were reviewed. There is currently
+ no budget reserves for this project. Lisa will escalate to
+ management. Next meeting on Tuesday.\n
+ 2. Telephone Conference: ABC Corp. sales representative called
+ to discuss new printer. Promised to get us a demo by Friday.\n
+ 3. Henry Miller (Handsoff Insurance): Car was totaled by tree.
+ Is looking into a loaner car. 654-2323 (tel).
+ END:VJOURNAL
+```
+
+These are not really intended to be edited by users. Using `caldavfs`,
+you can mount a directory of `.ics` files. Once mounted, it will expose the
+entries in a way that is more human-readable:
+
+1997-09-01-13:00.txt
+```
+Staff meeting minutes
+---
+1. Staff meeting: Participants include Joe, Lisa and Bob. Aurora project plans were reviewed. There is currently no budget reserves for this project. Lisa will escalate to management. Next meeting on Tuesday.
+2. Telephone Conference: ABC Corp. sales representative called to discuss new printer. Promised to get us a demo by Friday.
+3. Henry Miller (Handsoff Insurance): Car was totaled by tree. Is looking into a loaner car. 654-2323 (tel).
+```
+
+Now you can open the files in your favorite editor and any changes you make
+will be reflected in the original journal. Use your favorite tools: grep, vim, ls et.c.
+
+
+## Dependencies
+
+Right now it's built with GCC, but the aim is for it using only [cproc](https://sr.ht/~mcf/cproc/). You'll also need libfuse and pkg-config.
+
+Right now you also need a tool that syncs .ics files with a caldav server,
+like [vdirsyncer](https://vdirsyncer.pimutils.org/en/stable/when.html#).
+
+Someday I hope that `caldavfs` handles it.
+
+## Why C?
+
+I started this project to improve my C skills.
+
+## Building
+
+Run
+
+```
+make
+```
+
+## Status
+
+POC stage, still very early alpha.
+
+Planned features:
+- [ ] VJOURNAL - Now the first true journaling filesystem!
+ - [ ] Create
+ - [ ] Delete
+ - [x] Read
+ - [x] Rename
+ - [x] Write
+ - [ ] Directory structure
+- [ ] VTODO
+- [ ] Categories (Maybe via symlinks?)
+- [ ] VEVENTS
+- [ ] Relationships / linking
+- [ ] VALARM
+- [ ] VFREEBUSY
+
+## Setup
+
+caldavfs works together with [vdirsyncer](https://vdirsyncer.pimutils.org/en/stable/when.html#). Vdirsyncer is used to periodically sync `.ics` files to your caldav server, allowing you to have an offline copy.
+
+In the future I might build the vdirsyncer functionality into this. But for now you will need it for this to work.
+
+
+## Usage
```
mkdir /tmp/journal
./journalfs $HOME/.journal/XXX-XXX-XXX /tmp/journal
# Add -d for debugging
```
-
Will mount journalfs
diff --git a/main.c b/main.c
@@ -22,8 +22,7 @@
#define EVENT_BUF_LEN (1024 * (sizeof(struct inotify_event) + 16))
const char *date_title_fmt = "%Y%m%d-%H:%M";
-regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod,
- regex_uid, regex_fuse_file;
+regex_t regex_dtstamp, regex_lastmod, regex_fuse_file;
time_t INVALID_TIME = (time_t)-1;
@@ -72,24 +71,65 @@ static time_t parse_datetime(const char *datetime)
return -1;
}
-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_fuse_file)
+// Folds are [\r]\n followed by ' ' or '\t'
+static int skip_fold(const char *p)
+{
+ // '\r\n' + ' ' OR '\t'
+ if (*p == '\r' && p[1] == '\n' && (p[2] == ' ' || p[2] == '\t'))
+ return 3;
+
+ // '\n' + ' ' OR '\t'
+ if (*p == '\n' && (p[1] == ' ' || p[1] == '\t'))
+ return 2;
+
+ return 0;
+}
+
+// Description is multiline and cuts of at 75 characters.
+// Once extracted it will also need to be escaped.
+// Field name should be without colon, I.E.
+// "DESCRIPTION"
+char *extract_ical_field(const char *ical, const char *field_name)
+{
+ size_t field_len = strlen(field_name);
+ const char *start = strstr(ical, field_name);
+
+ start += field_len;
+
+ // skip the colon
+ start++;
+
+ const char *p = start;
+ char *result = calloc(strlen(p) + 1, sizeof(char));
+
+ size_t len = 0;
+
+ while (*p) {
+ p += skip_fold(p);
+
+ // end of field
+ if ((*p == '\r' && p[1] == '\n') || *p == '\n') {
+ break;
+ }
+
+ result[len++] = *p++;
+ }
+
+ result[len] = '\0';
+
+ // Shrink the buffer to the actual size
+ char *final = realloc(result, len + 1);
+ return final;
+}
+
+static int compile_regexes(regex_t *regex_dtstamp, regex_t *regex_lastmod,
+ regex_t *regex_fuse_file)
{
- // Parse text after SUMMARY: up until \r\n
- if (regcomp(regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0)
- return -1;
- // Parse text after DESCRIPTION:, multiple lines up until \r\n
- if (regcomp(regex_description, "DESCRIPTION:([^\r\n]*)",
- REG_EXTENDED) != 0)
- return -1;
if (regcomp(regex_dtstamp, "DTSTAMP:([0-9T]+Z)", REG_EXTENDED) != 0)
return -1;
if (regcomp(regex_lastmod, "LAST-MODIFIED:([0-9T]+Z)", REG_EXTENDED) !=
0)
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)
@@ -138,7 +178,8 @@ static void unescape_text(char *desc)
*dst++ = *src;
break;
}
- src++;
+ if (*src)
+ src++;
}
else {
*dst++ = *src++;
@@ -152,14 +193,25 @@ done:
// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
static char *escape_text(const char *src)
{
- if (!src)
- return NULL;
-
size_t len = strlen(src);
- char *escaped = calloc(len * 2 + 1, 1);
-
+ // Safe amount
+ size_t max_len = len * 2;
+ char *escaped = calloc(max_len, 1);
char *dst = escaped;
+ int line_len = 0;
+
for (const char *p = src; *p; ++p) {
+ int esc_len =
+ (*p == '\n' || *p == '\\' || *p == ',' || *p == ';') ? 2
+ : 1;
+
+ if (line_len + esc_len > 75) {
+ *dst++ = '\r';
+ *dst++ = '\n';
+ *dst++ = ' ';
+ line_len = 1;
+ }
+
switch (*p) {
case '\n':
*dst++ = '\\';
@@ -181,9 +233,14 @@ static char *escape_text(const char *src)
*dst++ = *p;
break;
}
+
+ line_len += esc_len;
}
+
*dst = '\0';
- return escaped;
+ size_t actual_len = strlen(escaped);
+
+ return realloc(escaped, actual_len);
}
static journal_entry *get_entry_from_fuse_path(const char *path)
@@ -229,45 +286,32 @@ static int parse_ics_file(const char *file_path, journal_entry *entry)
file_content[file_size] = '\0'; // Null-terminate the string
fclose(file);
- regmatch_t matches_summary[2], matches_description[2],
- matches_dtstamp[2], matches_lastmod[2], matches_uid[2];
+ regmatch_t matches_dtstamp[2], matches_lastmod[2];
// SUMMARY
- if (regexec(®ex_summary, file_content, 2, matches_summary, 0) != 0) {
- fprintf(stderr, "SUMMARY field not found in: %s\n", file_path);
- goto cleanup;
- }
-
- entry->summary = extract_match(file_content, matches_summary[1]);
+ entry->summary = extract_ical_field(file_content, "SUMMARY");
if (!entry->summary) {
- perror("Memory allocation failed for SUMMARY");
+ fprintf(stderr, "SUMMARY field not found in: %s\n", file_path);
goto cleanup;
}
unescape_text(entry->summary);
// UID
- if (regexec(®ex_uid, file_content, 2, matches_uid, 0) == 0) {
- entry->uid = extract_match(file_content, matches_uid[1]);
- }
- else {
- // If missing, generate one (fallback)
- LOG("FAILED TO GET UID");
- asprintf(&entry->uid, "%ld@fuse-journal", time(NULL));
+ entry->uid = extract_ical_field(file_content, "UID");
+ if (!entry->uid) {
+ fprintf(stderr, "UID field not found in: %s\n", file_path);
+ goto cleanup;
}
+ unescape_text(entry->uid);
// DESCRIPTION
- if (regexec(®ex_description, file_content, 2, matches_description,
- 0) != 0) {
+ entry->description = extract_ical_field(file_content, "DESCRIPTION");
+ if (!entry->description) {
fprintf(stderr, "DESCRIPTION field not found in: %s\n",
file_path);
goto cleanup;
}
- entry->description =
- extract_match(file_content, matches_description[1]);
- if (!entry->description) {
- perror("Memory allocation failed for DESCRIPTION");
- goto cleanup;
- }
+
unescape_text(entry->description);
// Extract the DTSTAMP, our created_at
@@ -563,7 +607,7 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
goto unlock_and_return;
}
- LOG("Got entry %s", entry->description);
+ LOG("Got entry %s", entry->summary);
// Calculate required length for full content
int needed_len = get_entry_content_size(entry);
@@ -630,6 +674,7 @@ char *journal_entry_to_ical(const journal_entry *entry)
snprintf(ical, total_size,
"BEGIN:VCALENDAR\n"
+ "PRODID:caldavfs\n"
"BEGIN:VJOURNAL\n"
"UID:%s\n"
"DTSTAMP:%s\n"
@@ -990,9 +1035,8 @@ int main(int argc, char *argv[])
return 1;
}
- if (compile_regexes(®ex_summary, ®ex_description, ®ex_dtstamp,
- ®ex_lastmod, ®ex_uid,
- ®ex_fuse_file) != 0) {
+ if (compile_regexes(®ex_dtstamp, ®ex_lastmod, ®ex_fuse_file) !=
+ 0) {
fprintf(stderr, "Failed to compile regular expressions\n");
return -1;
}
@@ -1009,11 +1053,8 @@ int main(int argc, char *argv[])
hashmap_free(entries_original_key);
LOG("Hashmap freed");
- regfree(®ex_summary);
- regfree(®ex_description);
regfree(®ex_dtstamp);
regfree(®ex_lastmod);
- regfree(®ex_uid);
regfree(®ex_fuse_file);
LOG("Regexp freed");
return ret;