agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafs| Log | Files | Refs | README | LICENSE | Mail | Website |
fuse_node.c (19941B)
1 #include "agenda_entry.h"
2 #include "arena.h"
3 #include "fuse_node_store.h"
4 #include "hashmap.h"
5 #include "ical_extra.h"
6 #include "path.h"
7 #include "tree.h"
8 #include "util.h"
9 #include <dirent.h>
10 #include <libical/ical.h>
11 #include <pthread.h>
12 #include <regex.h>
13 #include <stdbool.h>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <string.h>
17 #include <sys/inotify.h>
18 #include <sys/stat.h>
19 #include <time.h>
20 #include <unistd.h>
21 #include <uuid/uuid.h>
22 #include <wordexp.h>
23
24 struct tree_node *
25 get_node_by_uuid(arena *ar, const char *target_uuid)
26 {
27 char *filename_vdir = NULL;
28
29 // Filenames are uid.ics, so we levarage that
30 size_t wRes = rasprintf(ar, &filename_vdir, "%s.ics", target_uuid);
31 assert(wRes != -1);
32
33 LOG("Looking for UID: '%s'", target_uuid);
34 LOG("Full filename: '%s'", filename_vdir);
35
36 struct tree_node *node = get_fuse_node_from_vdir_name(filename_vdir);
37 LOG("Found it: %b", node != NULL);
38
39 return node;
40 }
41
42 struct tree_node *
43 get_node_by_path(arena *ar, const char *path)
44 {
45 if (strcmp(path, "/") == 0) {
46 LOG("Is ROOT %s", path);
47 return fuse_root;
48 }
49
50 char *segments[255];
51 size_t count = split_path(ar, path, segments);
52
53 struct tree_node *current = fuse_root;
54
55 for (size_t i = 0; i < count && current; ++i) {
56 const char *segment = segments[i];
57 struct tree_node *next = NULL;
58 LOG("Segment: %s", segment);
59
60 for (size_t j = 0; j < current->child_count; ++j) {
61
62 const char *child_name =
63 get_node_filename(current->children[j]);
64 if (strcmp(child_name, segment) == 0) {
65
66 next = current->children[j];
67 break;
68 }
69 }
70 if (next == NULL) {
71 return NULL;
72 }
73 current = next;
74 }
75 return current;
76 }
77
78 bool
79 node_is_directory(arena *ar, const struct tree_node *node,
80 icalcomponent *node_ics)
81 {
82 if (is_root_node(node) || node_has_children(node)) {
83 return true;
84 }
85 else {
86 // When user creates a directory, a directory component is
87 // stored in the ics file. So even if a node has no children
88 // it can still be that it should be shown as a directory.
89 return is_directory_component(node_ics);
90 }
91 }
92
93 size_t
94 write_ical_file(arena *ar, const struct tree_node *node, icalcomponent *ic)
95 {
96
97 // Always set last modified to the time we write
98 icalproperty *prop =
99 icalcomponent_get_first_property(ic, ICAL_LASTMODIFIED_PROPERTY);
100 if (!prop) {
101 prop = icalproperty_new_lastmodified(get_ical_now());
102 icalcomponent_add_property(ic, prop);
103 }
104 else {
105 icalproperty_set_lastmodified(prop, get_ical_now());
106 }
107
108 icalcomponent *inner = icalcomponent_get_inner(ic);
109
110 const char *descr_prop = icalcomponent_get_description(inner);
111 if (descr_prop != NULL && strcmp("", descr_prop) == 0) {
112 icalproperty *ical_descr_prop =
113 icalcomponent_get_first_property(inner,
114 ICAL_DESCRIPTION_PROPERTY);
115 icalcomponent_remove_property(inner, ical_descr_prop);
116 }
117
118 char *ical_str = ricalcomponent_as_ical_string_r(ar, ic);
119
120 int res = write_to_file(get_vdir_filepath(ar, node), ical_str);
121
122 return res;
123 }
124
125 // Owner: ctx
126 icalcomponent *
127 get_icalcomponent_from_node(arena *ar, const struct tree_node *n)
128 {
129 if (is_root_node(n)) {
130 return NULL;
131 }
132
133 const char *orig_path = get_vdir_filepath(ar, n);
134 // TODO: Cache intermediate results within context
135 icalcomponent *ic = parse_ics_file(ar, orig_path);
136 return ic;
137 }
138
139 int
140 write_parent_child_components(arena *ar, const struct tree_node *parent,
141 icalcomponent *iparent,
142 const struct tree_node *child,
143 icalcomponent *ichild)
144 {
145 LOG("Adding parent and child relation");
146 set_parent_child_relationship_to_component(iparent, ichild);
147 return write_ical_file(ar, child, ichild);
148 }
149
150 // Writes ics component summary to the filename of the tree_node
151 int
152 update_summary(arena *ar, icalcomponent *ics, const struct tree_node *node)
153 {
154 if (is_directory_component(ics)) {
155 icalcomponent_set_summary(ics, get_node_filename(node));
156 }
157 else {
158 char *filename_no_ext =
159 without_file_extension(ar, get_node_filename(node));
160 icalcomponent_set_summary(ics, filename_no_ext);
161 }
162
163 return write_ical_file(ar, node, ics);
164 }
165
166 int
167 update_node_file_extension(arena *ar, icalcomponent *ics,
168 const struct tree_node *node)
169 {
170 const char *ext = get_file_extension(ar, get_node_filename(node));
171 if (!ext) {
172 return 0;
173 }
174 icalcomponent_set_file_extension(ics, ext);
175 return write_ical_file(ar, node, ics);
176 }
177
178 struct agenda_entry *
179 parse_ics_to_agenda_entry(arena *ar, const char *vdir_filepath)
180 {
181 icalcomponent *component = parse_ics_file(ar, vdir_filepath);
182 if (component == NULL) {
183 LOG("No component");
184 return NULL;
185 }
186 icalcomponent *journal =
187 icalcomponent_get_first_real_component(component);
188
189 if (journal == NULL ||
190 icalcomponent_isa(journal) != ICAL_VJOURNAL_COMPONENT) {
191 LOG("Not journal component");
192 return NULL;
193 }
194 const char *summary = icalcomponent_get_summary(component);
195 if (summary == NULL) {
196 LOG("No summary found");
197 return NULL;
198 }
199
200 char *filename = NULL;
201 if (is_directory_component(component)) {
202 filename = rstrdup(ar, summary);
203 }
204 else {
205 LOG("GETTING FILE EXT");
206 const char *extension =
207 icalcomponent_get_file_extension(component);
208 if (!extension) {
209 LOG("Has none, setting to default");
210 extension = get_default_file_extension();
211 }
212
213 if (strcmp(extension, "") == 0) {
214 filename = rstrdup(ar, summary);
215 }
216 else {
217 rasprintf(ar, &filename, "%s.%s", summary, extension);
218 }
219 }
220
221 struct agenda_entry *e =
222 create_agenda_entry(ar, filename, get_filename(vdir_filepath));
223
224 return e;
225 }
226
227 // Owner: arena
228 struct agenda_entry *
229 load_agenda_entry_from_ics_file(arena *ar, const char *filename)
230 {
231 path *filepath = append_path(ar, VDIR, filename);
232 LOG("Filepath is %s", filepath);
233
234 struct stat fileStat;
235 if (stat(filepath, &fileStat) == -1) {
236 LOG("Can not stat %s.", filepath);
237 return NULL;
238 }
239
240 struct agenda_entry *new_entry =
241 parse_ics_to_agenda_entry(ar, filepath);
242 if (!new_entry) {
243 LOG("Could not parse entry");
244 return NULL;
245 }
246
247 LOG("Parsed %s to file %s", new_entry->filename_vdir,
248 new_entry->filename);
249 return new_entry;
250 }
251
252 void
253 load_root_node_tree()
254 {
255 entries_vdir = hashmap_new(NULL);
256 fuse_root = create_tree_node(NULL, NULL);
257 arena *ar = create_arena();
258 LOG("Loading journal entries from: %s\n", VDIR);
259
260 DIR *dir = opendir(VDIR);
261 if (!dir) {
262 perror("opendir");
263 return;
264 }
265
266 struct dirent *entry;
267 while ((entry = readdir(dir))) {
268 if (entry->d_type == DT_UNKNOWN) {
269 printf("Unsupported file type for '%s'. Skipping\n",
270 entry->d_name);
271 }
272 if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) {
273 LOG("Loading %s", entry->d_name);
274 struct agenda_entry *new_entry =
275 load_agenda_entry_from_ics_file(ar, entry->d_name);
276 if (new_entry) {
277 create_fuse_node(new_entry);
278 LOG("Inserted filename_vdir: %s, %s",
279 new_entry->filename,
280 new_entry->filename_vdir);
281 }
282 else {
283 LOG("Skipping %s", entry->d_name);
284 }
285 }
286 }
287 closedir(dir);
288 LOG("Set up directories according to parent-child");
289
290 size_t n_keys = 0;
291 char **keys = hashmap_get_keys(entries_vdir, &n_keys);
292 for (size_t i = 0; i < n_keys; i++) {
293 const char *vdirname = keys[i];
294 struct tree_node *child =
295 get_fuse_node_from_vdir_name(vdirname);
296
297 LOG("Parsing %s", vdirname);
298 icalcomponent *ic = get_icalcomponent_from_node(ar, child);
299 LOG("Success");
300
301 const char *parent_uid = get_parent_uid(ic);
302 if (parent_uid) {
303 LOG("Has parent");
304
305 struct tree_node *parent =
306 get_node_by_uuid(ar, parent_uid);
307 if (parent) {
308 icalcomponent *pic =
309 get_icalcomponent_from_node(ar, parent);
310 if (!is_directory_component(pic)) {
311 icalcomponent_mark_as_directory(pic);
312 assert(write_ical_file(ar, parent,
313 pic) == 0);
314 }
315
316 move_fuse_node(parent, child);
317 }
318 else {
319 LOG("COULD NOT FIND PARENT NODE: %s",
320 parent_uid);
321 }
322 }
323 else {
324 add_fuse_child(fuse_root, child);
325 }
326 }
327 LOG("Inserted %zu entries", n_keys);
328
329 hashmap_free_keys(keys, n_keys);
330 free_all(ar);
331
332 LOG("Done");
333 }
334
335 // Sets a validated dtstart
336 int
337 set_dtstart(arena *ar, const char *dtstart_c, const struct tree_node *node)
338 {
339 struct icaltimetype dtstart = icaltime_from_string(dtstart_c);
340 if (icaltime_compare(dtstart, icaltime_null_time()) == 0) {
341 return -EINVAL;
342 }
343
344 icalcomponent *comp = get_icalcomponent_from_node(ar, node);
345
346 icalcomponent_set_dtstart(comp, dtstart);
347
348 return write_ical_file(ar, node, comp);
349 }
350
351 const char *
352 get_dtstart(arena *ar, struct tree_node *n)
353 {
354
355 icalcomponent *comp = get_icalcomponent_from_node(ar, n);
356
357 struct icaltimetype dtstart = icalcomponent_get_dtstart(comp);
358 if (icaltime_compare(dtstart, icaltime_null_time()) == 0) {
359 return NULL;
360 }
361
362 const char *time = icaltime_as_ical_string(dtstart);
363 return time;
364 }
365
366 int
367 clear_dtstart(arena *ar, struct tree_node *node)
368 {
369 icalcomponent *comp = get_icalcomponent_from_node(ar, node);
370
371 icalproperty *p =
372 icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
373
374 if (p) {
375 icalcomponent_remove_property(comp, p);
376 }
377
378 return write_ical_file(ar, node, comp);
379 }
380
381 static int
382 append_category_to_memstream(FILE *memstream, const char *category,
383 bool is_first_category)
384 {
385 int res = 0;
386 if (!is_first_category) {
387 res = fputs(",", memstream);
388 assert(res != EOF);
389 }
390
391 res = fputs(category, memstream);
392 assert(res != EOF);
393 return 0;
394 }
395
396 // Returns comma separated list of categories for a file
397 char *
398 get_node_categories(arena *ar, const struct tree_node *node)
399 {
400 // TODO: Take this as parameter instead...
401 icalcomponent *component = get_icalcomponent_from_node(ar, node);
402 if (!component) {
403 return NULL;
404 }
405
406 FILE *memstream = NULL;
407 char *result_buffer = NULL;
408 arena_register(ar, result_buffer, free);
409 size_t buffer_size = 0;
410
411 memstream = open_memstream(&result_buffer, &buffer_size);
412
413 icalcomponent *inner = icalcomponent_get_inner(component);
414
415 icalproperty *catp =
416 icalcomponent_get_first_property(inner, ICAL_CATEGORIES_PROPERTY);
417
418 bool first_category = true;
419 while (catp) {
420 const char *category = icalproperty_get_categories(catp);
421 append_category_to_memstream(memstream, category,
422 first_category);
423 catp = icalcomponent_get_next_property(
424 inner, ICAL_CATEGORIES_PROPERTY);
425
426 first_category = false;
427 }
428
429 size_t res = fclose(memstream);
430 assert(res != EOF);
431
432 return result_buffer;
433 }
434
435 int
436 delete_node_categories(arena *ar, const struct tree_node *node)
437 {
438 icalcomponent *component = get_icalcomponent_from_node(ar, node);
439 if (!component) {
440 return 0;
441 }
442
443 icalcomponent *inner = icalcomponent_get_inner(component);
444
445 icalproperty *catp =
446 icalcomponent_get_first_property(inner, ICAL_CATEGORIES_PROPERTY);
447
448 // Delete old categories
449 while (catp) {
450 icalcomponent_remove_property(inner, catp);
451
452 catp = icalcomponent_get_next_property(
453 inner, ICAL_CATEGORIES_PROPERTY);
454 }
455
456 return write_ical_file(ar, node, component);
457 }
458
459 int
460 set_node_categories(arena *ar, const struct tree_node *node,
461 const char *new_categories, size_t s)
462 {
463 icalcomponent *component = get_icalcomponent_from_node(ar, node);
464 if (!component) {
465 return 0;
466 }
467
468 icalcomponent *inner = icalcomponent_get_inner(component);
469
470 icalproperty *catp =
471 icalcomponent_get_first_property(inner, ICAL_CATEGORIES_PROPERTY);
472
473 // Delete old categories
474 while ((catp = icalcomponent_get_first_property(
475 inner, ICAL_CATEGORIES_PROPERTY)) != NULL) {
476 icalcomponent_remove_property(inner, catp);
477 }
478
479 if (s == 0) {
480 return write_ical_file(ar, node, component);
481 }
482
483 char *null_terminated_categories = rstrndup(ar, new_categories, s);
484
485 icalproperty *new_cat_prop =
486 icalproperty_new_categories(null_terminated_categories);
487 icalcomponent_add_property(inner, new_cat_prop);
488
489 return write_ical_file(ar, node, component);
490 }
491
492 const char *
493 get_node_class(arena *ar, const struct tree_node *node)
494 {
495
496 // TODO: Take this as parameter instead...
497 icalcomponent *component = get_icalcomponent_from_node(ar, node);
498 if (!component) {
499 return 0;
500 }
501
502 icalcomponent *inner = icalcomponent_get_inner(component);
503
504 const icalproperty *classp =
505 icalcomponent_get_first_property(inner, ICAL_CLASS_PROPERTY);
506
507 if (!classp) {
508 return NULL;
509 }
510
511 const enum icalproperty_class classv = icalproperty_get_class(classp);
512 return format_ical_class(classv);
513 }
514
515 const char *
516 get_node_status(arena *ar, const struct tree_node *node)
517 {
518
519 // TODO: Take this as parameter instead...
520 icalcomponent *component = get_icalcomponent_from_node(ar, node);
521 if (!component) {
522 return 0;
523 }
524
525 icalcomponent *inner = icalcomponent_get_inner(component);
526
527 const icalproperty *statusp =
528 icalcomponent_get_first_property(inner, ICAL_STATUS_PROPERTY);
529
530 if (!statusp) {
531 LOG("No status found");
532 return NULL;
533 }
534
535 const enum icalproperty_status statusv =
536 icalproperty_get_status(statusp);
537 return format_ical_status(statusv);
538 }
539
540 int
541 delete_node_class(arena *ar, const struct tree_node *node)
542 {
543 icalcomponent *component = get_icalcomponent_from_node(ar, node);
544 if (!component) {
545 return 0;
546 }
547 icalcomponent *inner = icalcomponent_get_inner(component);
548 icalproperty *prop =
549 icalcomponent_get_first_property(inner, ICAL_CLASS_PROPERTY);
550
551 icalcomponent_remove_property(inner, prop);
552
553 return write_ical_file(ar, node, component);
554 }
555
556 int
557 set_node_class(arena *ar, const struct tree_node *node,
558 const icalproperty_class new_class)
559 {
560 icalcomponent *component = get_icalcomponent_from_node(ar, node);
561 if (!component) {
562 return 0;
563 }
564
565 icalcomponent *inner = icalcomponent_get_inner(component);
566
567 icalproperty *classp =
568 icalcomponent_get_first_property(inner, ICAL_CLASS_PROPERTY);
569
570 icalproperty_set_class(classp, new_class);
571 return write_ical_file(ar, node, component);
572 }
573
574 int
575 set_node_status(arena *ar, const struct tree_node *node,
576 const icalproperty_status new_status)
577 {
578 icalcomponent *component = get_icalcomponent_from_node(ar, node);
579 if (!component) {
580 return 0;
581 }
582
583 icalcomponent_set_status(component, new_status);
584
585 return write_ical_file(ar, node, component);
586 }
587
588 size_t
589 calculate_node_size(arena *ar, const struct tree_node *node,
590 icalcomponent *node_component)
591 {
592 if (!node_is_directory(ar, node, node_component)) {
593 return icalcomponent_get_description_size(node_component);
594 }
595
596 size_t total_size = 0;
597 for (int i = 0; i < node->child_count; i++) {
598 icalcomponent *cic =
599 get_icalcomponent_from_node(ar, node->children[i]);
600 total_size += calculate_node_size(ar, node->children[i], cic);
601 }
602 return total_size;
603 }
604
605 struct stat
606 get_node_stat(arena *ar, const struct tree_node *node,
607 icalcomponent *node_component)
608 {
609 if (is_root_node(node)) {
610 struct stat st = {0};
611 st.st_gid = getgid();
612 st.st_uid = getuid();
613 st.st_ino = 0;
614 return st;
615 }
616 else {
617 struct stat vdir_stat = {0};
618 const char *vdir_path = get_vdir_filepath(ar, node);
619 int res = stat(vdir_path, &vdir_stat);
620 assert(res == 0);
621 if (node_is_directory(ar, node, node_component)) {
622 vdir_stat.st_size = (off_t)calculate_node_size(
623 ar, node, node_component);
624 vdir_stat.st_mode = S_IFDIR | 0444;
625 vdir_stat.st_nlink = 2;
626 }
627 else {
628 vdir_stat.st_size =
629 (off_t)icalcomponent_get_description_size(
630 node_component);
631
632 vdir_stat.st_mode = S_IFREG | 0774;
633 vdir_stat.st_nlink = 1;
634 }
635
636 return vdir_stat;
637 }
638 }
639
640 int
641 insert_fuse_node_to_path(arena *ar, const char *fuse_path,
642 struct tree_node *child_node, icalcomponent *child_ics)
643 {
644 char *parent_path = get_parent_path(ar, fuse_path);
645 int status = 0;
646 LOG("Parent path is %s", parent_path);
647
648 struct tree_node *parent_node = get_node_by_path(ar, parent_path);
649 if (!parent_node) {
650 LOG("Parent does not exist");
651 return -ENOENT;
652 }
653
654 icalcomponent *parent_ics =
655 get_icalcomponent_from_node(ar, parent_node);
656
657 if (!node_is_directory(ar, parent_node, parent_ics)) {
658 LOG("Parent is not a directory");
659 return -ENOTDIR;
660 }
661
662 if (parent_ics) {
663 status = write_parent_child_components(
664 ar, parent_node, parent_ics, child_node, child_ics);
665 }
666 else {
667 write_ical_file(ar, child_node, child_ics);
668 }
669
670 add_child(parent_node, child_node);
671 return status;
672 }
673
674 enum ENTRY_TYPE { ENTRY_DIRECTORY, ENTRY_FILE };
675
676 int
677 create_entry_from_fuse(arena *ar, const char *fuse_path, enum ENTRY_TYPE etype)
678 {
679 const char *entry_prefix = "/";
680 if (strncmp(fuse_path, entry_prefix, strlen(entry_prefix)) != 0) {
681 return -EINVAL;
682 }
683
684 const char *new_filename = get_filename(fuse_path);
685 icalcomponent *new_component = NULL;
686
687 switch (etype) {
688 case (ENTRY_DIRECTORY):
689 new_component = create_vjournal_directory(ar, new_filename);
690 break;
691 case (ENTRY_FILE):
692 new_component = create_vjournal_entry(ar, new_filename);
693 break;
694 }
695
696 char *new_filname_vdir = NULL;
697 rasprintf(ar, &new_filname_vdir, "%s.ics",
698 icalcomponent_get_uid(new_component));
699
700 struct agenda_entry *new_entry =
701 create_agenda_entry(ar, new_filename, new_filname_vdir);
702
703 LOG("Inserting vjournal directory");
704
705 struct tree_node *new_node = create_fuse_node(new_entry);
706
707 return insert_fuse_node_to_path(ar, fuse_path, new_node, new_component);
708 }
709
710 int
711 update_or_create_fuse_entry_from_vdir(arena *ar, const char *filepath_vdir)
712 {
713 const char *filename_vdir = get_filename(filepath_vdir);
714 LOG("Filename original is %s", filename_vdir);
715
716 struct agenda_entry *updated_entry =
717 load_agenda_entry_from_ics_file(ar, filename_vdir);
718
719 if (!updated_entry) {
720 return -EIO;
721 }
722
723 struct tree_node *node = upsert_fuse_node(updated_entry);
724
725 icalcomponent *entry_ics = get_icalcomponent_from_node(ar, node);
726
727 const char *new_parent_uid = get_parent_uid(entry_ics);
728
729 if (new_parent_uid) {
730 LOG("Has parent");
731 struct tree_node *new_parent =
732 get_node_by_uuid(ar, new_parent_uid);
733 move_fuse_node(new_parent, node);
734
735 icalcomponent *pic =
736 get_icalcomponent_from_node(ar, new_parent);
737 if (!is_directory_component(pic)) {
738 icalcomponent_mark_as_directory(pic);
739 assert(write_ical_file(ar, new_parent, pic) == 0);
740 }
741 }
742 else {
743 LOG("Does not have parent, adding to root");
744 move_fuse_node(fuse_root, node);
745 }
746
747 LOG("Tree updated");
748 return 0;
749 }
750
751 int
752 do_agenda_rename(arena *ar, const char *old, const char *new)
753 {
754 char *new_copy = rstrdup(ar, new);
755 const char *new_filename = get_filename(new_copy);
756
757 const char *old_filename = get_filename(old);
758 if (!old_filename)
759 return -EINVAL;
760
761 struct tree_node *new_parent_node =
762 get_node_by_path(ar, get_parent_path(ar, new));
763
764 if (new_parent_node == NULL) {
765 return -EINVAL;
766 }
767
768 struct tree_node *child_node = get_node_by_path(ar, old);
769 assert(child_node);
770
771 icalcomponent *child_ics = get_icalcomponent_from_node(ar, child_node);
772 assert(child_ics);
773
774 icalcomponent *new_parent_ics =
775 get_icalcomponent_from_node(ar, new_parent_node);
776
777 struct tree_node *old_parent_node = child_node->parent;
778 icalcomponent *old_parent_ics =
779 get_icalcomponent_from_node(ar, old_parent_node);
780
781 set_node_filename(child_node, new_filename);
782 update_summary(ar, child_ics, child_node);
783 update_node_file_extension(ar, child_ics, child_node);
784
785 if (old_parent_node && old_parent_ics) {
786 remove_parent_child_relationship_from_component(old_parent_ics,
787 child_ics);
788 }
789
790 if (new_parent_ics) {
791 move_node(new_parent_node, child_node);
792 write_parent_child_components(
793 ar, new_parent_node, new_parent_ics, child_node, child_ics);
794 }
795 else {
796 move_node(fuse_root, child_node);
797 }
798 return 0;
799 }
800
801 int
802 delete_vdir_entry(arena *ar, struct tree_node *node)
803 {
804 int res = 0;
805
806 const char *filepath_original = get_vdir_filepath(ar, node);
807
808 LOG("Deleting file %s", filepath_original);
809
810 res = remove(filepath_original);
811 if (res == -1) {
812 return -EIO;
813 }
814
815 delete_fuse_node(node);
816 return res;
817 }
818
819 // Caller needs to ensure the node has no children.
820 int
821 delete_from_vdir_path(arena *ar, const char *filepath)
822 {
823
824 struct tree_node *node = get_node_by_uuid(ar, filepath);
825 if (!node) {
826 return -ENOENT;
827 }
828
829 assert(node->child_count > 0);
830 delete_fuse_node(node);
831 return 0;
832 }