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