]> git.notmuchmail.org Git - notmuch/blob - lib/open.cc
lib/open: support XDG_DATA_HOME as a fallback database location.
[notmuch] / lib / open.cc
1 #include <unistd.h>
2 #include "database-private.h"
3 #include "parse-time-vrp.h"
4
5 #if HAVE_XAPIAN_DB_RETRY_LOCK
6 #define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK)
7 #else
8 #define DB_ACTION Xapian::DB_CREATE_OR_OPEN
9 #endif
10
11 notmuch_status_t
12 notmuch_database_open (const char *path,
13                        notmuch_database_mode_t mode,
14                        notmuch_database_t **database)
15 {
16     char *status_string = NULL;
17     notmuch_status_t status;
18
19     status = notmuch_database_open_verbose (path, mode, database,
20                                             &status_string);
21
22     if (status_string) {
23         fputs (status_string, stderr);
24         free (status_string);
25     }
26
27     return status;
28 }
29
30 notmuch_status_t
31 notmuch_database_open_verbose (const char *path,
32                                notmuch_database_mode_t mode,
33                                notmuch_database_t **database,
34                                char **status_string)
35 {
36     return notmuch_database_open_with_config (path, mode, "", NULL,
37                                               database, status_string);
38 }
39
40 static const char *
41 _xdg_dir (void *ctx,
42           const char *xdg_root_variable,
43           const char *xdg_prefix,
44           const char *profile_name)
45 {
46     const char *xdg_root = getenv (xdg_root_variable);
47
48     if (! xdg_root) {
49         const char *home = getenv ("HOME");
50
51         if (! home) return NULL;
52
53         xdg_root = talloc_asprintf (ctx,
54                                     "%s/%s",
55                                     home,
56                                     xdg_prefix);
57     }
58
59     if (! profile_name)
60         profile_name = getenv ("NOTMUCH_PROFILE");
61
62     if (! profile_name)
63         profile_name = "default";
64
65     return talloc_asprintf (ctx,
66                             "%s/notmuch/%s",
67                             xdg_root,
68                             profile_name);
69 }
70
71 static notmuch_status_t
72 _choose_hook_dir (notmuch_database_t *notmuch,
73                   const char *profile,
74                   char **message)
75 {
76     const char *config;
77     const char *hook_dir;
78     struct stat st;
79     int err;
80
81     hook_dir = notmuch_config_get (notmuch, NOTMUCH_CONFIG_HOOK_DIR);
82
83     if (hook_dir)
84         return NOTMUCH_STATUS_SUCCESS;
85
86     config = _xdg_dir (notmuch, "XDG_CONFIG_HOME", ".config", profile);
87     if (! config)
88         return NOTMUCH_STATUS_PATH_ERROR;
89
90     hook_dir = talloc_asprintf (notmuch, "%s/hooks", config);
91
92     err = stat (hook_dir, &st);
93     if (err) {
94         if (errno == ENOENT) {
95             const char *database_path = notmuch_database_get_path (notmuch);
96             hook_dir = talloc_asprintf (notmuch, "%s/.notmuch/hooks", database_path);
97         } else {
98             IGNORE_RESULT (asprintf (message, "Error: Cannot stat %s: %s.\n",
99                                      hook_dir, strerror (errno)));
100             return NOTMUCH_STATUS_FILE_ERROR;
101         }
102     }
103
104     _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_HOOK_DIR, hook_dir);
105
106     return NOTMUCH_STATUS_SUCCESS;
107 }
108
109 static notmuch_status_t
110 _load_key_file (const char *path,
111                 const char *profile,
112                 GKeyFile **key_file)
113 {
114     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
115     void *local = talloc_new (NULL);
116
117     if (path && EMPTY_STRING (path))
118         goto DONE;
119
120     if (! path)
121         path = getenv ("NOTMUCH_CONFIG");
122
123     if (! path) {
124         const char *dir = _xdg_dir (local, "XDG_CONFIG_HOME", ".config", profile);
125
126         if (dir) {
127             path = talloc_asprintf (local, "%s/config", dir);
128             if (access (path, R_OK) != 0)
129                 path = NULL;
130         }
131     }
132
133     if (! path) {
134         const char *home = getenv ("HOME");
135
136         path = talloc_asprintf (local, "%s/.notmuch-config", home);
137
138         if (! profile)
139             profile = getenv ("NOTMUCH_PROFILE");
140
141         if (profile)
142             path = talloc_asprintf (local, "%s.%s", path, profile);
143     }
144
145     *key_file = g_key_file_new ();
146     if (! g_key_file_load_from_file (*key_file, path, G_KEY_FILE_NONE, NULL)) {
147         status = NOTMUCH_STATUS_NO_CONFIG;
148     }
149
150   DONE:
151     talloc_free (local);
152     return status;
153 }
154
155 static notmuch_status_t
156 _db_dir_exists (const char *database_path, char **message)
157 {
158     struct stat st;
159     int err;
160
161     err = stat (database_path, &st);
162     if (err) {
163         IGNORE_RESULT (asprintf (message, "Error: Cannot open database at %s: %s.\n",
164                                  database_path, strerror (errno)));
165         return NOTMUCH_STATUS_FILE_ERROR;
166     }
167
168     if (! S_ISDIR (st.st_mode)) {
169         IGNORE_RESULT (asprintf (message, "Error: Cannot open database at %s: "
170                                  "Not a directory.\n",
171                                  database_path));
172         return NOTMUCH_STATUS_FILE_ERROR;
173     }
174
175     return NOTMUCH_STATUS_SUCCESS;
176 }
177
178 static notmuch_status_t
179 _choose_database_path (void *ctx,
180                        const char *config_path,
181                        const char *profile,
182                        GKeyFile **key_file,
183                        const char **database_path,
184                        bool *split,
185                        char **message)
186 {
187     notmuch_status_t status;
188
189     status = _load_key_file (config_path, profile, key_file);
190     if (status) {
191         *message = strdup ("Error: cannot load config file.\n");
192         return status;
193     }
194
195     if (! *database_path) {
196         *database_path = getenv ("NOTMUCH_DATABASE");
197     }
198
199     if (! *database_path && *key_file) {
200         char *path = g_key_file_get_value (*key_file, "database", "path", NULL);
201         if (path) {
202             *database_path = talloc_strdup (ctx, path);
203             g_free (path);
204         }
205     }
206
207     if (! *database_path) {
208         *database_path = _xdg_dir (ctx, "XDG_DATA_HOME", ".local/share", profile);
209         *split = true;
210     }
211
212     if (*database_path == NULL) {
213         *message = strdup ("Error: Cannot open a database for a NULL path.\n");
214         return NOTMUCH_STATUS_NULL_POINTER;
215     }
216
217     if (*database_path[0] != '/') {
218         *message = strdup ("Error: Database path must be absolute.\n");
219         return NOTMUCH_STATUS_PATH_ERROR;
220     }
221     return NOTMUCH_STATUS_SUCCESS;
222 }
223
224 notmuch_database_t *
225 _alloc_notmuch ()
226 {
227     notmuch_database_t *notmuch;
228
229     notmuch = talloc_zero (NULL, notmuch_database_t);
230     if (! notmuch)
231         return NULL;
232
233     notmuch->exception_reported = false;
234     notmuch->status_string = NULL;
235     notmuch->writable_xapian_db = NULL;
236     notmuch->atomic_nesting = 0;
237     notmuch->view = 1;
238     return notmuch;
239 }
240
241 static notmuch_status_t
242 _trial_open (const char *xapian_path, char **message_ptr)
243 {
244     try {
245         Xapian::Database db (xapian_path);
246     } catch (const Xapian::DatabaseOpeningError &error) {
247         IGNORE_RESULT (asprintf (message_ptr,
248                                  "Cannot open Xapian database at %s: %s\n",
249                                  xapian_path,
250                                  error.get_msg ().c_str ()));
251         return NOTMUCH_STATUS_PATH_ERROR;
252     } catch (const Xapian::Error &error) {
253         IGNORE_RESULT (asprintf (message_ptr,
254                                  "A Xapian exception occurred opening database: %s\n",
255                                  error.get_msg ().c_str ()));
256         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
257     }
258     return NOTMUCH_STATUS_SUCCESS;
259 }
260
261 static notmuch_status_t
262 _choose_xapian_path (void *ctx, const char *database_path, const char **xapian_path,
263                      char **message_ptr)
264 {
265     notmuch_status_t status;
266     const char *trial_path, *notmuch_path;
267
268     status = _db_dir_exists (database_path, message_ptr);
269     if (status)
270         goto DONE;
271
272     trial_path = talloc_asprintf (ctx, "%s/xapian", database_path);
273     status = _trial_open (trial_path, message_ptr);
274     if (status != NOTMUCH_STATUS_PATH_ERROR)
275         goto DONE;
276
277     notmuch_path = talloc_asprintf (ctx, "%s/.notmuch", database_path);
278     status = _db_dir_exists (notmuch_path, message_ptr);
279     if (status)
280         goto DONE;
281
282     trial_path = talloc_asprintf (ctx, "%s/xapian", notmuch_path);
283     status = _trial_open (trial_path, message_ptr);
284
285   DONE:
286     if (status == NOTMUCH_STATUS_SUCCESS)
287         *xapian_path = trial_path;
288     return status;
289 }
290
291 static void
292 _set_database_path (notmuch_database_t *notmuch,
293                     const char *database_path)
294 {
295     char *path = talloc_strdup (notmuch, database_path);
296
297     strip_trailing (path, '/');
298
299     _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path);
300 }
301
302 static void
303 _init_libs ()
304 {
305
306     static int initialized = 0;
307
308     /* Initialize the GLib type system and threads */
309 #if ! GLIB_CHECK_VERSION (2, 35, 1)
310     g_type_init ();
311 #endif
312
313     /* Initialize gmime */
314     if (! initialized) {
315         g_mime_init ();
316         initialized = 1;
317     }
318 }
319
320 static notmuch_status_t
321 _finish_open (notmuch_database_t *notmuch,
322               const char *profile,
323               notmuch_database_mode_t mode,
324               GKeyFile *key_file,
325               char **message_ptr)
326 {
327     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
328     char *incompat_features;
329     char *message = NULL;
330     unsigned int version;
331     const char *database_path = notmuch_database_get_path (notmuch);
332
333     try {
334         std::string last_thread_id;
335         std::string last_mod;
336
337         if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
338             notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path,
339                                                                         DB_ACTION);
340             notmuch->xapian_db = notmuch->writable_xapian_db;
341         } else {
342             notmuch->xapian_db = new Xapian::Database (notmuch->xapian_path);
343         }
344
345         /* Check version.  As of database version 3, we represent
346          * changes in terms of features, so assume a version bump
347          * means a dramatically incompatible change. */
348         version = notmuch_database_get_version (notmuch);
349         if (version > NOTMUCH_DATABASE_VERSION) {
350             IGNORE_RESULT (asprintf (&message,
351                                      "Error: Notmuch database at %s\n"
352                                      "       has a newer database format version (%u) than supported by this\n"
353                                      "       version of notmuch (%u).\n",
354                                      database_path, version, NOTMUCH_DATABASE_VERSION));
355             notmuch_database_destroy (notmuch);
356             notmuch = NULL;
357             status = NOTMUCH_STATUS_FILE_ERROR;
358             goto DONE;
359         }
360
361         /* Check features. */
362         incompat_features = NULL;
363         notmuch->features = _notmuch_database_parse_features (
364             notmuch, notmuch->xapian_db->get_metadata ("features").c_str (),
365             version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',
366             &incompat_features);
367         if (incompat_features) {
368             IGNORE_RESULT (asprintf (&message,
369                                      "Error: Notmuch database at %s\n"
370                                      "       requires features (%s)\n"
371                                      "       not supported by this version of notmuch.\n",
372                                      database_path, incompat_features));
373             notmuch_database_destroy (notmuch);
374             notmuch = NULL;
375             status = NOTMUCH_STATUS_FILE_ERROR;
376             goto DONE;
377         }
378
379         notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
380         last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
381         if (last_thread_id.empty ()) {
382             notmuch->last_thread_id = 0;
383         } else {
384             const char *str;
385             char *end;
386
387             str = last_thread_id.c_str ();
388             notmuch->last_thread_id = strtoull (str, &end, 16);
389             if (*end != '\0')
390                 INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
391         }
392
393         /* Get current highest revision number. */
394         last_mod = notmuch->xapian_db->get_value_upper_bound (
395             NOTMUCH_VALUE_LAST_MOD);
396         if (last_mod.empty ())
397             notmuch->revision = 0;
398         else
399             notmuch->revision = Xapian::sortable_unserialise (last_mod);
400         notmuch->uuid = talloc_strdup (
401             notmuch, notmuch->xapian_db->get_uuid ().c_str ());
402
403         notmuch->query_parser = new Xapian::QueryParser;
404         notmuch->term_gen = new Xapian::TermGenerator;
405         notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
406         notmuch->value_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
407         notmuch->date_range_processor = new ParseTimeRangeProcessor (NOTMUCH_VALUE_TIMESTAMP,
408                                                                      "date:");
409         notmuch->last_mod_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_LAST_MOD,
410                                                                               "lastmod:");
411         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
412         notmuch->query_parser->set_database (*notmuch->xapian_db);
413         notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
414         notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
415         notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor);
416         notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor);
417         notmuch->query_parser->add_rangeprocessor (notmuch->last_mod_range_processor);
418
419         /* Configuration information is needed to set up query parser */
420         status = _notmuch_config_load_from_database (notmuch);
421         if (status)
422             goto DONE;
423
424         if (key_file)
425             status = _notmuch_config_load_from_file (notmuch, key_file);
426         if (status)
427             goto DONE;
428
429         status = _choose_hook_dir (notmuch, profile, &message);
430         if (status)
431             goto DONE;
432
433         status = _notmuch_config_load_defaults (notmuch);
434         if (status)
435             goto DONE;
436
437         status = _notmuch_database_setup_standard_query_fields (notmuch);
438         if (status)
439             goto DONE;
440
441         status = _notmuch_database_setup_user_query_fields (notmuch);
442         if (status)
443             goto DONE;
444
445     } catch (const Xapian::Error &error) {
446         IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
447                                  error.get_msg ().c_str ()));
448         notmuch_database_destroy (notmuch);
449         notmuch = NULL;
450         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
451     }
452   DONE:
453     if (message_ptr)
454         *message_ptr = message;
455     return status;
456 }
457
458 notmuch_status_t
459 notmuch_database_open_with_config (const char *database_path,
460                                    notmuch_database_mode_t mode,
461                                    const char *config_path,
462                                    const char *profile,
463                                    notmuch_database_t **database,
464                                    char **status_string)
465 {
466     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
467     void *local = talloc_new (NULL);
468     notmuch_database_t *notmuch = NULL;
469     char *message = NULL;
470     GKeyFile *key_file = NULL;
471     bool split = false;
472
473     _init_libs ();
474
475     notmuch = _alloc_notmuch ();
476     if (! notmuch) {
477         status = NOTMUCH_STATUS_OUT_OF_MEMORY;
478         goto DONE;
479     }
480
481     if ((status = _choose_database_path (local, config_path, profile,
482                                          &key_file, &database_path, &split,
483                                          &message)))
484         goto DONE;
485
486     status = _db_dir_exists (database_path, &message);
487     if (status)
488         goto DONE;
489
490     _set_database_path (notmuch, database_path);
491
492     status = _choose_xapian_path (notmuch, database_path, &notmuch->xapian_path, &message);
493     if (status)
494         goto DONE;
495
496     status = _finish_open (notmuch, profile, mode, key_file, &message);
497
498   DONE:
499     talloc_free (local);
500
501     if (key_file)
502         g_key_file_free (key_file);
503
504     if (message) {
505         if (status_string)
506             *status_string = message;
507         else
508             free (message);
509     }
510
511     if (database)
512         *database = notmuch;
513     else
514         talloc_free (notmuch);
515
516     if (notmuch)
517         notmuch->open = true;
518
519     return status;
520 }
521
522 notmuch_status_t
523 notmuch_database_create (const char *path, notmuch_database_t **database)
524 {
525     char *status_string = NULL;
526     notmuch_status_t status;
527
528     status = notmuch_database_create_verbose (path, database,
529                                               &status_string);
530
531     if (status_string) {
532         fputs (status_string, stderr);
533         free (status_string);
534     }
535
536     return status;
537 }
538
539 notmuch_status_t
540 notmuch_database_create_verbose (const char *path,
541                                  notmuch_database_t **database,
542                                  char **status_string)
543 {
544     return notmuch_database_create_with_config (path, "", NULL, database, status_string);
545 }
546
547 notmuch_status_t
548 notmuch_database_create_with_config (const char *database_path,
549                                      const char *config_path,
550                                      const char *profile,
551                                      notmuch_database_t **database,
552                                      char **status_string)
553 {
554     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
555     notmuch_database_t *notmuch = NULL;
556     const char *notmuch_path = NULL;
557     char *message = NULL;
558     GKeyFile *key_file = NULL;
559     void *local = talloc_new (NULL);
560     int err;
561     bool split = false;
562
563     _init_libs ();
564
565     notmuch = _alloc_notmuch ();
566     if (! notmuch) {
567         status = NOTMUCH_STATUS_OUT_OF_MEMORY;
568         goto DONE;
569     }
570
571     _init_libs ();
572
573     if ((status = _choose_database_path (local, config_path, profile,
574                                          &key_file, &database_path, &split,
575                                          &message)))
576         goto DONE;
577
578     status = _db_dir_exists (database_path, &message);
579     if (status)
580         goto DONE;
581
582     _set_database_path (notmuch, database_path);
583
584     if (key_file && ! split) {
585         char *mail_root = canonicalize_file_name (
586             g_key_file_get_value (key_file, "database", "mail_root", NULL));
587         char *db_path = canonicalize_file_name (database_path);
588
589         split = (mail_root && (0 != strcmp (mail_root, db_path)));
590
591         free (mail_root);
592         free (db_path);
593     }
594
595     if (split) {
596         notmuch_path = database_path;
597     } else {
598         if (! (notmuch_path = talloc_asprintf (local, "%s/%s", database_path, ".notmuch"))) {
599             status = NOTMUCH_STATUS_OUT_OF_MEMORY;
600             goto DONE;
601         }
602
603         err = mkdir (notmuch_path, 0755);
604         if (err) {
605             if (errno == EEXIST) {
606                 status = NOTMUCH_STATUS_DATABASE_EXISTS;
607                 talloc_free (notmuch);
608                 notmuch = NULL;
609             } else {
610                 IGNORE_RESULT (asprintf (&message, "Error: Cannot create directory %s: %s.\n",
611                                          notmuch_path, strerror (errno)));
612                 status = NOTMUCH_STATUS_FILE_ERROR;
613             }
614             goto DONE;
615         }
616     }
617
618     if (! (notmuch->xapian_path = talloc_asprintf (notmuch, "%s/%s", notmuch_path, "xapian"))) {
619         status = NOTMUCH_STATUS_OUT_OF_MEMORY;
620         goto DONE;
621     }
622
623     status = _trial_open (notmuch->xapian_path, &message);
624     if (status == NOTMUCH_STATUS_SUCCESS) {
625         notmuch_database_destroy (notmuch);
626         notmuch = NULL;
627         status = NOTMUCH_STATUS_DATABASE_EXISTS;
628         goto DONE;
629     }
630
631     status = _finish_open (notmuch,
632                            profile,
633                            NOTMUCH_DATABASE_MODE_READ_WRITE,
634                            key_file,
635                            &message);
636     if (status)
637         goto DONE;
638
639     /* Upgrade doesn't add these feature to existing databases, but
640      * new databases have them. */
641     notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES;
642     notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES;
643     notmuch->features |= NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY;
644
645     status = notmuch_database_upgrade (notmuch, NULL, NULL);
646     if (status) {
647         notmuch_database_close (notmuch);
648         notmuch = NULL;
649     }
650
651   DONE:
652     talloc_free (local);
653
654     if (key_file)
655         g_key_file_free (key_file);
656
657     if (message) {
658         if (status_string)
659             *status_string = message;
660         else
661             free (message);
662     }
663     if (database)
664         *database = notmuch;
665     else
666         talloc_free (notmuch);
667     return status;
668 }
669
670 notmuch_status_t
671 notmuch_database_reopen (notmuch_database_t *notmuch,
672                          notmuch_database_mode_t new_mode)
673 {
674     notmuch_database_mode_t cur_mode = _notmuch_database_mode (notmuch);
675
676     if (notmuch->xapian_db == NULL) {
677         _notmuch_database_log (notmuch, "Cannot reopen closed or nonexistent database\n");
678         return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
679     }
680
681     try {
682         if (cur_mode == new_mode &&
683             new_mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
684             notmuch->xapian_db->reopen ();
685         } else {
686             notmuch->xapian_db->close ();
687
688             delete notmuch->xapian_db;
689             notmuch->xapian_db = NULL;
690             /* no need to free the same object twice */
691             notmuch->writable_xapian_db = NULL;
692
693             if (new_mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
694                 notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path,
695                                                                             DB_ACTION);
696                 notmuch->xapian_db = notmuch->writable_xapian_db;
697             } else {
698                 notmuch->xapian_db = new Xapian::Database (notmuch->xapian_path,
699                                                            DB_ACTION);
700             }
701         }
702     } catch (const Xapian::Error &error) {
703         if (! notmuch->exception_reported) {
704             _notmuch_database_log (notmuch, "Error: A Xapian exception reopening database: %s\n",
705                                    error.get_msg ().c_str ());
706             notmuch->exception_reported = true;
707         }
708         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
709     }
710
711     notmuch->view++;
712     notmuch->open = true;
713     return NOTMUCH_STATUS_SUCCESS;
714 }