]> git.notmuchmail.org Git - notmuch/blob - lib/database.cc
d2ccdd580b764189f700c8f5dd9b063e37a8560b
[notmuch] / lib / database.cc
1 /* database.cc - The database interfaces of the notmuch mail library
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see https://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "database-private.h"
22 #include "string-util.h"
23
24 #include <iostream>
25
26 #include <sys/time.h>
27 #include <sys/stat.h>
28 #include <signal.h>
29 #include <ftw.h>
30
31 #include <glib.h>               /* g_free, GPtrArray, GHashTable */
32 #include <glib-object.h>        /* g_type_init */
33
34 #include <gmime/gmime.h>        /* g_mime_init */
35
36 using namespace std;
37
38 typedef struct {
39     const char *name;
40     const char *prefix;
41     notmuch_field_flag_t flags;
42 } prefix_t;
43
44 #define NOTMUCH_DATABASE_VERSION 3
45
46 #define STRINGIFY(s) _SUB_STRINGIFY (s)
47 #define _SUB_STRINGIFY(s) #s
48
49 #define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error)
50
51 static void
52 _log_xapian_exception (const char *where, notmuch_database_t *notmuch,  const Xapian::Error error)
53 {
54     _notmuch_database_log (notmuch,
55                            "A Xapian exception occurred at %s: %s\n",
56                            where,
57                            error.get_msg ().c_str ());
58     notmuch->exception_reported = true;
59 }
60
61 notmuch_database_mode_t
62 _notmuch_database_mode (notmuch_database_t *notmuch)
63 {
64     if (notmuch->writable_xapian_db)
65         return NOTMUCH_DATABASE_MODE_READ_WRITE;
66     else
67         return NOTMUCH_DATABASE_MODE_READ_ONLY;
68 }
69
70 /* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
71  *
72  * We currently have three different types of documents (mail, ghost,
73  * and directory) and also some metadata.
74  *
75  * There are two kinds of prefixes used in notmuch. There are the
76  * human friendly 'prefix names' like "thread:", which are also used
77  * in the query parser, and the actual prefix terms in the database
78  * (e.g. "G"). The correspondence is maintained in the file scope data
79  * structure 'prefix_table'.
80  *
81  * Mail document
82  * -------------
83  * A mail document is associated with a particular email message. It
84  * is stored in one or more files on disk and is uniquely identified
85  * by its "id" field (which is generally the message ID). It is
86  * indexed with the following prefixed terms which the database uses
87  * to construct threads, etc.:
88  *
89  *    Single terms of given prefix:
90  *
91  *      type:   mail
92  *
93  *      id:     Unique ID of mail. This is from the Message-ID header
94  *              if present and not too long (see NOTMUCH_MESSAGE_ID_MAX).
95  *              If it's present and too long, then we use
96  *              "notmuch-sha1-<sha1_sum_of_message_id>".
97  *              If this header is not present, we use
98  *              "notmuch-sha1-<sha1_sum_of_entire_file>".
99  *
100  *      thread: The ID of the thread to which the mail belongs
101  *
102  *      replyto: The ID from the In-Reply-To header of the mail (if any).
103  *
104  *    Multiple terms of given prefix:
105  *
106  *      reference: All message IDs from In-Reply-To and References
107  *                 headers in the message.
108  *
109  *      tag:       Any tags associated with this message by the user.
110  *
111  *      file-direntry:  A colon-separated pair of values
112  *                      (INTEGER:STRING), where INTEGER is the
113  *                      document ID of a directory document, and
114  *                      STRING is the name of a file within that
115  *                      directory for this mail message.
116  *
117  *      property:       Has a property with key=value
118  *                 FIXME: if no = is present, should match on any value
119  *
120  *    A mail document also has four values:
121  *
122  *      TIMESTAMP:      The time_t value corresponding to the message's
123  *                      Date header.
124  *
125  *      MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
126  *
127  *      FROM:           The value of the "From" header
128  *
129  *      SUBJECT:        The value of the "Subject" header
130  *
131  *      LAST_MOD:       The revision number as of the last tag or
132  *                      filename change.
133  *
134  * The prefixed terms described above are also searchable without an
135  * explicit field name, but as of notmuch 0.29 this is due to
136  * query-parser setup, not extra terms in the database.  In addition,
137  * terms from the content of the message are added without a prefix
138  * for use by the user in searching. Note that the prefix name "body"
139  * is used to refer to the empty prefix string in the database.
140  *
141  * The path of the containing folder is added with the "folder" prefix
142  * (see _notmuch_message_add_folder_terms).  Sub-paths of the the path
143  * of the mail message are added with the "path" prefix.
144  *
145  * The data portion of a mail document is empty.
146  *
147  * Ghost mail document [if NOTMUCH_FEATURE_GHOSTS]
148  * -----------------------------------------------
149  * A ghost mail document is like a mail document, but where we don't
150  * have the message content.  These are used to track thread reference
151  * information for messages we haven't received.
152  *
153  * A ghost mail document has type: ghost; id and thread fields that
154  * are identical to the mail document fields; and a MESSAGE_ID value.
155  *
156  * Directory document
157  * ------------------
158  * A directory document is used by a client of the notmuch library to
159  * maintain data necessary to allow for efficient polling of mail
160  * directories.
161  *
162  * All directory documents contain one term:
163  *
164  *      directory:      The directory path (relative to the database path)
165  *                      Or the SHA1 sum of the directory path (if the
166  *                      path itself is too long to fit in a Xapian
167  *                      term).
168  *
169  * And all directory documents for directories other than top-level
170  * directories also contain the following term:
171  *
172  *      directory-direntry: A colon-separated pair of values
173  *                          (INTEGER:STRING), where INTEGER is the
174  *                          document ID of the parent directory
175  *                          document, and STRING is the name of this
176  *                          directory within that parent.
177  *
178  * All directory documents have a single value:
179  *
180  *      TIMESTAMP:      The mtime of the directory (at last scan)
181  *
182  * The data portion of a directory document contains the path of the
183  * directory (relative to the database path).
184  *
185  * Database metadata
186  * -----------------
187  * Xapian allows us to store arbitrary name-value pairs as
188  * "metadata". We currently use the following metadata names with the
189  * given meanings:
190  *
191  *      version         The database schema version, (which is distinct
192  *                      from both the notmuch package version (see
193  *                      notmuch --version) and the libnotmuch library
194  *                      version. The version is stored as an base-10
195  *                      ASCII integer. The initial database version
196  *                      was 1, (though a schema existed before that
197  *                      were no "version" database value existed at
198  *                      all). Successive versions are allocated as
199  *                      changes are made to the database (such as by
200  *                      indexing new fields).
201  *
202  *      features        The set of features supported by this
203  *                      database. This consists of a set of
204  *                      '\n'-separated lines, where each is a feature
205  *                      name, a '\t', and compatibility flags.  If the
206  *                      compatibility flags contain 'w', then the
207  *                      opener must support this feature to safely
208  *                      write this database.  If the compatibility
209  *                      flags contain 'r', then the opener must
210  *                      support this feature to read this database.
211  *                      Introduced in database version 3.
212  *
213  *      last_thread_id  The last thread ID generated. This is stored
214  *                      as a 16-byte hexadecimal ASCII representation
215  *                      of a 64-bit unsigned integer. The first ID
216  *                      generated is 1 and the value will be
217  *                      incremented for each thread ID.
218  *
219  *      C*              metadata keys starting with C indicate
220  *                      configuration data. It can be managed with the
221  *                      n_database_*config* API.  There is a convention
222  *                      of hierarchical keys separated by '.' (e.g.
223  *                      query.notmuch stores the value for the named
224  *                      query 'notmuch'), but it is not enforced by the
225  *                      API.
226  *
227  * Obsolete metadata
228  * -----------------
229  *
230  * If ! NOTMUCH_FEATURE_GHOSTS, there are no ghost mail documents.
231  * Instead, the database has the following additional database
232  * metadata:
233  *
234  *      thread_id_*     A pre-allocated thread ID for a particular
235  *                      message. This is actually an arbitrarily large
236  *                      family of metadata name. Any particular name is
237  *                      formed by concatenating "thread_id_" with a message
238  *                      ID (or the SHA1 sum of a message ID if it is very
239  *                      long---see description of 'id' in the mail
240  *                      document). The value stored is a thread ID.
241  *
242  *                      These thread ID metadata values are stored
243  *                      whenever a message references a parent message
244  *                      that does not yet exist in the database. A
245  *                      thread ID will be allocated and stored, and if
246  *                      the message is later added, the stored thread
247  *                      ID will be used (and the metadata value will
248  *                      be cleared).
249  *
250  *                      Even before a message is added, it's
251  *                      pre-allocated thread ID is useful so that all
252  *                      descendant messages that reference this common
253  *                      parent can be recognized as belonging to the
254  *                      same thread.
255  */
256
257
258 notmuch_string_map_iterator_t *
259 _notmuch_database_user_headers (notmuch_database_t *notmuch)
260 {
261     return _notmuch_string_map_iterator_create (notmuch->user_header, "", false);
262 }
263
264 const char *
265 notmuch_status_to_string (notmuch_status_t status)
266 {
267     switch (status) {
268     case NOTMUCH_STATUS_SUCCESS:
269         return "No error occurred";
270     case NOTMUCH_STATUS_OUT_OF_MEMORY:
271         return "Out of memory";
272     case NOTMUCH_STATUS_READ_ONLY_DATABASE:
273         return "Attempt to write to a read-only database";
274     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
275         return "A Xapian exception occurred";
276     case NOTMUCH_STATUS_FILE_ERROR:
277         return "Something went wrong trying to read or write a file";
278     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
279         return "File is not an email";
280     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
281         return "Message ID is identical to a message in database";
282     case NOTMUCH_STATUS_NULL_POINTER:
283         return "Erroneous NULL pointer";
284     case NOTMUCH_STATUS_TAG_TOO_LONG:
285         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
286     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
287         return "Unbalanced number of calls to notmuch_message_freeze/thaw";
288     case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
289         return "Unbalanced number of calls to notmuch_database_begin_atomic/end_atomic";
290     case NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
291         return "Unsupported operation";
292     case NOTMUCH_STATUS_UPGRADE_REQUIRED:
293         return "Operation requires a database upgrade";
294     case NOTMUCH_STATUS_PATH_ERROR:
295         return "Path supplied is illegal for this function";
296     case NOTMUCH_STATUS_IGNORED:
297         return "Argument was ignored";
298     case NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
299         return "Illegal argument for function";
300     case NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL:
301         return "Crypto protocol missing, malformed, or unintelligible";
302     case NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION:
303         return "Crypto engine initialization failure";
304     case NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL:
305         return "Unknown crypto protocol";
306     case NOTMUCH_STATUS_NO_CONFIG:
307         return "No configuration file found";
308     case NOTMUCH_STATUS_DATABASE_EXISTS:
309         return "Database exists, not recreated";
310     default:
311     case NOTMUCH_STATUS_LAST_STATUS:
312         return "Unknown error status value";
313     }
314 }
315
316 void
317 _notmuch_database_log (notmuch_database_t *notmuch,
318                        const char *format,
319                        ...)
320 {
321     va_list va_args;
322
323     va_start (va_args, format);
324
325     if (notmuch->status_string)
326         talloc_free (notmuch->status_string);
327
328     notmuch->status_string = talloc_vasprintf (notmuch, format, va_args);
329     va_end (va_args);
330 }
331
332 void
333 _notmuch_database_log_append (notmuch_database_t *notmuch,
334                               const char *format,
335                               ...)
336 {
337     va_list va_args;
338
339     va_start (va_args, format);
340
341     if (notmuch->status_string)
342         notmuch->status_string = talloc_vasprintf_append (notmuch->status_string, format, va_args);
343     else
344         notmuch->status_string = talloc_vasprintf (notmuch, format, va_args);
345
346     va_end (va_args);
347 }
348
349 static void
350 find_doc_ids_for_term (notmuch_database_t *notmuch,
351                        const char *term,
352                        Xapian::PostingIterator *begin,
353                        Xapian::PostingIterator *end)
354 {
355     *begin = notmuch->xapian_db->postlist_begin (term);
356
357     *end = notmuch->xapian_db->postlist_end (term);
358 }
359
360 void
361 _notmuch_database_find_doc_ids (notmuch_database_t *notmuch,
362                                 const char *prefix_name,
363                                 const char *value,
364                                 Xapian::PostingIterator *begin,
365                                 Xapian::PostingIterator *end)
366 {
367     char *term;
368
369     term = talloc_asprintf (notmuch, "%s%s",
370                             _find_prefix (prefix_name), value);
371
372     find_doc_ids_for_term (notmuch, term, begin, end);
373
374     talloc_free (term);
375 }
376
377 notmuch_private_status_t
378 _notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
379                                       const char *prefix_name,
380                                       const char *value,
381                                       unsigned int *doc_id)
382 {
383     Xapian::PostingIterator i, end;
384
385     _notmuch_database_find_doc_ids (notmuch, prefix_name, value, &i, &end);
386
387     if (i == end) {
388         *doc_id = 0;
389         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
390     }
391
392     *doc_id = *i;
393
394 #if DEBUG_DATABASE_SANITY
395     i++;
396
397     if (i != end)
398         INTERNAL_ERROR ("Term %s:%s is not unique as expected.\n",
399                         prefix_name, value);
400 #endif
401
402     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
403 }
404
405 static Xapian::Document
406 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
407 {
408     return notmuch->xapian_db->get_document (doc_id);
409 }
410
411 /* Generate a compressed version of 'message_id' of the form:
412  *
413  *      notmuch-sha1-<sha1_sum_of_message_id>
414  */
415 char *
416 _notmuch_message_id_compressed (void *ctx, const char *message_id)
417 {
418     char *sha1, *compressed;
419
420     sha1 = _notmuch_sha1_of_string (message_id);
421
422     compressed = talloc_asprintf (ctx, "notmuch-sha1-%s", sha1);
423     free (sha1);
424
425     return compressed;
426 }
427
428 notmuch_status_t
429 notmuch_database_find_message (notmuch_database_t *notmuch,
430                                const char *message_id,
431                                notmuch_message_t **message_ret)
432 {
433     notmuch_private_status_t status;
434     unsigned int doc_id;
435
436     if (message_ret == NULL)
437         return NOTMUCH_STATUS_NULL_POINTER;
438
439     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
440         message_id = _notmuch_message_id_compressed (notmuch, message_id);
441
442     try {
443         status = _notmuch_database_find_unique_doc_id (notmuch, "id",
444                                                        message_id, &doc_id);
445
446         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
447             *message_ret = NULL;
448         else {
449             *message_ret = _notmuch_message_create (notmuch, notmuch, doc_id,
450                                                     NULL);
451             if (*message_ret == NULL)
452                 return NOTMUCH_STATUS_OUT_OF_MEMORY;
453         }
454
455         return NOTMUCH_STATUS_SUCCESS;
456     } catch (const Xapian::Error &error) {
457         _notmuch_database_log (notmuch, "A Xapian exception occurred finding message: %s.\n",
458                                error.get_msg ().c_str ());
459         notmuch->exception_reported = true;
460         *message_ret = NULL;
461         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
462     }
463 }
464
465 notmuch_status_t
466 _notmuch_database_ensure_writable (notmuch_database_t *notmuch)
467 {
468     if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY) {
469         _notmuch_database_log (notmuch, "Cannot write to a read-only database.\n");
470         return NOTMUCH_STATUS_READ_ONLY_DATABASE;
471     }
472
473     return NOTMUCH_STATUS_SUCCESS;
474 }
475
476 /* Allocate a revision number for the next change. */
477 unsigned long
478 _notmuch_database_new_revision (notmuch_database_t *notmuch)
479 {
480     unsigned long new_revision = notmuch->revision + 1;
481
482     /* If we're in an atomic section, hold off on updating the
483      * committed revision number until we commit the atomic section.
484      */
485     if (notmuch->atomic_nesting)
486         notmuch->atomic_dirty = true;
487     else
488         notmuch->revision = new_revision;
489
490     return new_revision;
491 }
492
493 notmuch_status_t
494 notmuch_database_close (notmuch_database_t *notmuch)
495 {
496     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
497
498     /* Many Xapian objects (and thus notmuch objects) hold references to
499      * the database, so merely deleting the database may not suffice to
500      * close it.  Thus, we explicitly close it here. */
501     if (notmuch->open) {
502         try {
503             /* If there's an outstanding transaction, it's unclear if
504              * closing the Xapian database commits everything up to
505              * that transaction, or may discard committed (but
506              * unflushed) transactions.  To be certain, explicitly
507              * cancel any outstanding transaction before closing. */
508             if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_WRITE &&
509                 notmuch->atomic_nesting)
510                 notmuch->writable_xapian_db->cancel_transaction ();
511
512             /* Close the database.  This implicitly flushes
513              * outstanding changes. */
514             notmuch->xapian_db->close ();
515         } catch (const Xapian::Error &error) {
516             status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
517             if (! notmuch->exception_reported) {
518                 _notmuch_database_log (notmuch,
519                                        "Error: A Xapian exception occurred closing database: %s\n",
520                                        error.get_msg ().c_str ());
521             }
522         }
523     }
524     notmuch->open = false;
525     return status;
526 }
527
528 static int
529 unlink_cb (const char *path,
530            unused (const struct stat *sb),
531            unused (int type),
532            unused (struct FTW *ftw))
533 {
534     return remove (path);
535 }
536
537 static int
538 rmtree (const char *path)
539 {
540     return nftw (path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
541 }
542
543 class NotmuchCompactor : public Xapian::Compactor
544 {
545     notmuch_compact_status_cb_t status_cb;
546     void *status_closure;
547
548 public:
549     NotmuchCompactor(notmuch_compact_status_cb_t cb, void *closure) :
550         status_cb (cb), status_closure (closure)
551     {
552     }
553
554     virtual void
555     set_status (const std::string &table, const std::string &status)
556     {
557         char *msg;
558
559         if (status_cb == NULL)
560             return;
561
562         if (status.length () == 0)
563             msg = talloc_asprintf (NULL, "compacting table %s", table.c_str ());
564         else
565             msg = talloc_asprintf (NULL, "     %s", status.c_str ());
566
567         if (msg == NULL) {
568             return;
569         }
570
571         status_cb (msg, status_closure);
572         talloc_free (msg);
573     }
574 };
575
576 /* Compacts the given database, optionally saving the original database
577  * in backup_path. Additionally, a callback function can be provided to
578  * give the user feedback on the progress of the (likely long-lived)
579  * compaction process.
580  *
581  * The backup path must point to a directory on the same volume as the
582  * original database. Passing a NULL backup_path will result in the
583  * uncompacted database being deleted after compaction has finished.
584  * Note that the database write lock will be held during the
585  * compaction process to protect data integrity.
586  */
587 notmuch_status_t
588 notmuch_database_compact (const char *path,
589                           const char *backup_path,
590                           notmuch_compact_status_cb_t status_cb,
591                           void *closure)
592 {
593     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
594     notmuch_database_t *notmuch = NULL;
595     char *message = NULL;
596
597     ret = notmuch_database_open_verbose (path,
598                                          NOTMUCH_DATABASE_MODE_READ_WRITE,
599                                          &notmuch,
600                                          &message);
601     if (ret) {
602         if (status_cb) status_cb (message, closure);
603         return ret;
604     }
605
606     _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path);
607
608     return notmuch_database_compact_db (notmuch,
609                                         backup_path,
610                                         status_cb,
611                                         closure);
612 }
613
614 notmuch_status_t
615 notmuch_database_compact_db (notmuch_database_t *notmuch,
616                              const char *backup_path,
617                              notmuch_compact_status_cb_t status_cb,
618                              void *closure)
619 {
620     void *local;
621     const char *xapian_path, *compact_xapian_path;
622     const char *path;
623     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
624     struct stat statbuf;
625     bool keep_backup;
626     char *message;
627
628     ret = _notmuch_database_ensure_writable (notmuch);
629     if (ret)
630         return ret;
631
632     path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
633     if (! path)
634         return NOTMUCH_STATUS_PATH_ERROR;
635
636     local = talloc_new (NULL);
637     if (! local)
638         return NOTMUCH_STATUS_OUT_OF_MEMORY;
639
640     ret = _notmuch_choose_xapian_path (local, path, &xapian_path, &message);
641     if (ret)
642         goto DONE;
643
644     if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) {
645         ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
646         goto DONE;
647     }
648
649     if (backup_path == NULL) {
650         if (! (backup_path = talloc_asprintf (local, "%s.old", xapian_path))) {
651             ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
652             goto DONE;
653         }
654         keep_backup = false;
655     } else {
656         keep_backup = true;
657     }
658
659     if (stat (backup_path, &statbuf) != -1) {
660         _notmuch_database_log (notmuch, "Path already exists: %s\n", backup_path);
661         ret = NOTMUCH_STATUS_FILE_ERROR;
662         goto DONE;
663     }
664     if (errno != ENOENT) {
665         _notmuch_database_log (notmuch, "Unknown error while stat()ing path: %s\n",
666                                strerror (errno));
667         ret = NOTMUCH_STATUS_FILE_ERROR;
668         goto DONE;
669     }
670
671     /* Unconditionally attempt to remove old work-in-progress database (if
672      * any). This is "protected" by database lock. If this fails due to write
673      * errors (etc), the following code will fail and provide error message.
674      */
675     (void) rmtree (compact_xapian_path);
676
677     try {
678         NotmuchCompactor compactor (status_cb, closure);
679         notmuch->xapian_db->compact (compact_xapian_path, Xapian::DBCOMPACT_NO_RENUMBER, 0,
680                                      compactor);
681     } catch (const Xapian::Error &error) {
682         _notmuch_database_log (notmuch, "Error while compacting: %s\n", error.get_msg ().c_str ());
683         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
684         goto DONE;
685     }
686
687     if (rename (xapian_path, backup_path)) {
688         _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
689                                xapian_path, backup_path, strerror (errno));
690         ret = NOTMUCH_STATUS_FILE_ERROR;
691         goto DONE;
692     }
693
694     if (rename (compact_xapian_path, xapian_path)) {
695         _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
696                                compact_xapian_path, xapian_path, strerror (errno));
697         ret = NOTMUCH_STATUS_FILE_ERROR;
698         goto DONE;
699     }
700
701     if (! keep_backup) {
702         if (rmtree (backup_path)) {
703             _notmuch_database_log (notmuch, "Error removing old database %s: %s\n",
704                                    backup_path, strerror (errno));
705             ret = NOTMUCH_STATUS_FILE_ERROR;
706             goto DONE;
707         }
708     }
709
710   DONE:
711     if (notmuch) {
712         notmuch_status_t ret2;
713
714         const char *str = notmuch_database_status_string (notmuch);
715         if (status_cb && str)
716             status_cb (str, closure);
717
718         ret2 = notmuch_database_destroy (notmuch);
719
720         /* don't clobber previous error status */
721         if (ret == NOTMUCH_STATUS_SUCCESS && ret2 != NOTMUCH_STATUS_SUCCESS)
722             ret = ret2;
723     }
724
725     talloc_free (local);
726
727     return ret;
728 }
729
730 notmuch_status_t
731 notmuch_database_destroy (notmuch_database_t *notmuch)
732 {
733     notmuch_status_t status;
734
735     status = notmuch_database_close (notmuch);
736
737     delete notmuch->term_gen;
738     notmuch->term_gen = NULL;
739     delete notmuch->query_parser;
740     notmuch->query_parser = NULL;
741     delete notmuch->xapian_db;
742     notmuch->xapian_db = NULL;
743     delete notmuch->value_range_processor;
744     notmuch->value_range_processor = NULL;
745     delete notmuch->date_range_processor;
746     notmuch->date_range_processor = NULL;
747     delete notmuch->last_mod_range_processor;
748     notmuch->last_mod_range_processor = NULL;
749
750     talloc_free (notmuch);
751
752     return status;
753 }
754
755 const char *
756 notmuch_database_get_path (notmuch_database_t *notmuch)
757 {
758     return notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
759 }
760
761 unsigned int
762 notmuch_database_get_version (notmuch_database_t *notmuch)
763 {
764     unsigned int version;
765     string version_string;
766     const char *str;
767     char *end;
768
769     try {
770         version_string = notmuch->xapian_db->get_metadata ("version");
771     } catch (const Xapian::Error &error) {
772         LOG_XAPIAN_EXCEPTION (notmuch, error);
773         return 0;
774     }
775
776     if (version_string.empty ())
777         return 0;
778
779     str = version_string.c_str ();
780     if (str == NULL || *str == '\0')
781         return 0;
782
783     version = strtoul (str, &end, 10);
784     if (*end != '\0')
785         INTERNAL_ERROR ("Malformed database version: %s", str);
786
787     return version;
788 }
789
790 notmuch_bool_t
791 notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
792 {
793     unsigned int version;
794
795     if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE)
796         return FALSE;
797
798     if (NOTMUCH_FEATURES_CURRENT & ~notmuch->features)
799         return TRUE;
800
801     version = notmuch_database_get_version (notmuch);
802
803     return (version > 0 && version < NOTMUCH_DATABASE_VERSION);
804 }
805
806 static volatile sig_atomic_t do_progress_notify = 0;
807
808 static void
809 handle_sigalrm (unused (int signal))
810 {
811     do_progress_notify = 1;
812 }
813
814 /* Upgrade the current database.
815  *
816  * After opening a database in read-write mode, the client should
817  * check if an upgrade is needed (notmuch_database_needs_upgrade) and
818  * if so, upgrade with this function before making any modifications.
819  *
820  * The optional progress_notify callback can be used by the caller to
821  * provide progress indication to the user. If non-NULL it will be
822  * called periodically with 'count' as the number of messages upgraded
823  * so far and 'total' the overall number of messages that will be
824  * converted.
825  */
826 notmuch_status_t
827 notmuch_database_upgrade (notmuch_database_t *notmuch,
828                           void (*progress_notify)(void *closure,
829                                                   double progress),
830                           void *closure)
831 {
832     void *local = talloc_new (NULL);
833     Xapian::TermIterator t, t_end;
834     Xapian::WritableDatabase *db;
835     struct sigaction action;
836     struct itimerval timerval;
837     bool timer_is_active = false;
838     enum _notmuch_features target_features, new_features;
839     notmuch_status_t status;
840     notmuch_private_status_t private_status;
841     notmuch_query_t *query = NULL;
842     unsigned int count = 0, total = 0;
843
844     status = _notmuch_database_ensure_writable (notmuch);
845     if (status)
846         return status;
847
848     db = notmuch->writable_xapian_db;
849
850     target_features = notmuch->features | NOTMUCH_FEATURES_CURRENT;
851     new_features = NOTMUCH_FEATURES_CURRENT & ~notmuch->features;
852
853     if (! notmuch_database_needs_upgrade (notmuch))
854         return NOTMUCH_STATUS_SUCCESS;
855
856     if (progress_notify) {
857         /* Set up our handler for SIGALRM */
858         memset (&action, 0, sizeof (struct sigaction));
859         action.sa_handler = handle_sigalrm;
860         sigemptyset (&action.sa_mask);
861         action.sa_flags = SA_RESTART;
862         sigaction (SIGALRM, &action, NULL);
863
864         /* Then start a timer to send SIGALRM once per second. */
865         timerval.it_interval.tv_sec = 1;
866         timerval.it_interval.tv_usec = 0;
867         timerval.it_value.tv_sec = 1;
868         timerval.it_value.tv_usec = 0;
869         setitimer (ITIMER_REAL, &timerval, NULL);
870
871         timer_is_active = true;
872     }
873
874     /* Figure out how much total work we need to do. */
875     if (new_features &
876         (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER |
877          NOTMUCH_FEATURE_LAST_MOD)) {
878         query = notmuch_query_create (notmuch, "");
879         unsigned msg_count;
880
881         status = notmuch_query_count_messages (query, &msg_count);
882         if (status)
883             goto DONE;
884
885         total += msg_count;
886         notmuch_query_destroy (query);
887         query = NULL;
888     }
889     if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
890         t_end = db->allterms_end ("XTIMESTAMP");
891         for (t = db->allterms_begin ("XTIMESTAMP"); t != t_end; t++)
892             ++total;
893     }
894     if (new_features & NOTMUCH_FEATURE_GHOSTS) {
895         /* The ghost message upgrade converts all thread_id_*
896          * metadata values into ghost message documents. */
897         t_end = db->metadata_keys_end ("thread_id_");
898         for (t = db->metadata_keys_begin ("thread_id_"); t != t_end; ++t)
899             ++total;
900     }
901
902     /* Perform the upgrade in a transaction. */
903     db->begin_transaction (true);
904
905     /* Set the target features so we write out changes in the desired
906      * format. */
907     notmuch->features = target_features;
908
909     /* Perform per-message upgrades. */
910     if (new_features &
911         (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER |
912          NOTMUCH_FEATURE_LAST_MOD)) {
913         notmuch_messages_t *messages;
914         notmuch_message_t *message;
915         char *filename;
916
917         query = notmuch_query_create (notmuch, "");
918
919         status = notmuch_query_search_messages (query, &messages);
920         if (status)
921             goto DONE;
922         for (;
923              notmuch_messages_valid (messages);
924              notmuch_messages_move_to_next (messages)) {
925             if (do_progress_notify) {
926                 progress_notify (closure, (double) count / total);
927                 do_progress_notify = 0;
928             }
929
930             message = notmuch_messages_get (messages);
931
932             /* Before version 1, each message document had its
933              * filename in the data field. Copy that into the new
934              * format by calling notmuch_message_add_filename.
935              */
936             if (new_features & NOTMUCH_FEATURE_FILE_TERMS) {
937                 filename = _notmuch_message_talloc_copy_data (message);
938                 if (filename && *filename != '\0') {
939                     _notmuch_message_add_filename (message, filename);
940                     _notmuch_message_clear_data (message);
941                 }
942                 talloc_free (filename);
943             }
944
945             /* Prior to version 2, the "folder:" prefix was
946              * probabilistic and stemmed. Change it to the current
947              * boolean prefix. Add "path:" prefixes while at it.
948              */
949             if (new_features & NOTMUCH_FEATURE_BOOL_FOLDER)
950                 _notmuch_message_upgrade_folder (message);
951
952             /* Prior to NOTMUCH_FEATURE_LAST_MOD, messages did not
953              * track modification revisions.  Give all messages the
954              * next available revision; since we just started tracking
955              * revisions for this database, that will be 1.
956              */
957             if (new_features & NOTMUCH_FEATURE_LAST_MOD)
958                 _notmuch_message_upgrade_last_mod (message);
959
960             _notmuch_message_sync (message);
961
962             notmuch_message_destroy (message);
963
964             count++;
965         }
966
967         notmuch_query_destroy (query);
968         query = NULL;
969     }
970
971     /* Perform per-directory upgrades. */
972
973     /* Before version 1 we stored directory timestamps in
974      * XTIMESTAMP documents instead of the current XDIRECTORY
975      * documents. So copy those as well. */
976     if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
977         t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
978
979         for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
980              t != t_end;
981              t++) {
982             Xapian::PostingIterator p, p_end;
983             std::string term = *t;
984
985             p_end = notmuch->xapian_db->postlist_end (term);
986
987             for (p = notmuch->xapian_db->postlist_begin (term);
988                  p != p_end;
989                  p++) {
990                 Xapian::Document document;
991                 time_t mtime;
992                 notmuch_directory_t *directory;
993
994                 if (do_progress_notify) {
995                     progress_notify (closure, (double) count / total);
996                     do_progress_notify = 0;
997                 }
998
999                 document = find_document_for_doc_id (notmuch, *p);
1000                 mtime = Xapian::sortable_unserialise (
1001                     document.get_value (NOTMUCH_VALUE_TIMESTAMP));
1002
1003                 directory = _notmuch_directory_find_or_create (notmuch, term.c_str () + 10,
1004                                                                NOTMUCH_FIND_CREATE, &status);
1005                 notmuch_directory_set_mtime (directory, mtime);
1006                 notmuch_directory_destroy (directory);
1007
1008                 db->delete_document (*p);
1009             }
1010
1011             ++count;
1012         }
1013     }
1014
1015     /* Perform metadata upgrades. */
1016
1017     /* Prior to NOTMUCH_FEATURE_GHOSTS, thread IDs for missing
1018      * messages were stored as database metadata. Change these to
1019      * ghost messages.
1020      */
1021     if (new_features & NOTMUCH_FEATURE_GHOSTS) {
1022         notmuch_message_t *message;
1023         std::string message_id, thread_id;
1024
1025         t_end = db->metadata_keys_end (NOTMUCH_METADATA_THREAD_ID_PREFIX);
1026         for (t = db->metadata_keys_begin (NOTMUCH_METADATA_THREAD_ID_PREFIX);
1027              t != t_end; ++t) {
1028             if (do_progress_notify) {
1029                 progress_notify (closure, (double) count / total);
1030                 do_progress_notify = 0;
1031             }
1032
1033             message_id = (*t).substr (
1034                 strlen (NOTMUCH_METADATA_THREAD_ID_PREFIX));
1035             thread_id = db->get_metadata (*t);
1036
1037             /* Create ghost message */
1038             message = _notmuch_message_create_for_message_id (
1039                 notmuch, message_id.c_str (), &private_status);
1040             if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
1041                 /* Document already exists; ignore the stored thread ID */
1042             } else if (private_status ==
1043                        NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1044                 private_status = _notmuch_message_initialize_ghost (
1045                     message, thread_id.c_str ());
1046                 if (! private_status)
1047                     _notmuch_message_sync (message);
1048             }
1049
1050             if (private_status) {
1051                 _notmuch_database_log (notmuch,
1052                                        "Upgrade failed while creating ghost messages.\n");
1053                 status = COERCE_STATUS (private_status,
1054                                         "Unexpected status from _notmuch_message_initialize_ghost");
1055                 goto DONE;
1056             }
1057
1058             /* Clear saved metadata thread ID */
1059             db->set_metadata (*t, "");
1060
1061             ++count;
1062         }
1063     }
1064
1065     status = NOTMUCH_STATUS_SUCCESS;
1066     db->set_metadata ("features", _notmuch_database_print_features (local, notmuch->features));
1067     db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
1068
1069   DONE:
1070     if (status == NOTMUCH_STATUS_SUCCESS)
1071         db->commit_transaction ();
1072     else
1073         db->cancel_transaction ();
1074
1075     if (timer_is_active) {
1076         /* Now stop the timer. */
1077         timerval.it_interval.tv_sec = 0;
1078         timerval.it_interval.tv_usec = 0;
1079         timerval.it_value.tv_sec = 0;
1080         timerval.it_value.tv_usec = 0;
1081         setitimer (ITIMER_REAL, &timerval, NULL);
1082
1083         /* And disable the signal handler. */
1084         action.sa_handler = SIG_IGN;
1085         sigaction (SIGALRM, &action, NULL);
1086     }
1087
1088     if (query)
1089         notmuch_query_destroy (query);
1090
1091     talloc_free (local);
1092     return status;
1093 }
1094
1095 notmuch_status_t
1096 notmuch_database_begin_atomic (notmuch_database_t *notmuch)
1097 {
1098     if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY ||
1099         notmuch->atomic_nesting > 0)
1100         goto DONE;
1101
1102     if (notmuch_database_needs_upgrade (notmuch))
1103         return NOTMUCH_STATUS_UPGRADE_REQUIRED;
1104
1105     try {
1106         notmuch->writable_xapian_db->begin_transaction (false);
1107     } catch (const Xapian::Error &error) {
1108         _notmuch_database_log (notmuch, "A Xapian exception occurred beginning transaction: %s.\n",
1109                                error.get_msg ().c_str ());
1110         notmuch->exception_reported = true;
1111         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1112     }
1113
1114   DONE:
1115     notmuch->atomic_nesting++;
1116     return NOTMUCH_STATUS_SUCCESS;
1117 }
1118
1119 notmuch_status_t
1120 notmuch_database_end_atomic (notmuch_database_t *notmuch)
1121 {
1122     Xapian::WritableDatabase *db;
1123
1124     if (notmuch->atomic_nesting == 0)
1125         return NOTMUCH_STATUS_UNBALANCED_ATOMIC;
1126
1127     if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY ||
1128         notmuch->atomic_nesting > 1)
1129         goto DONE;
1130
1131     db = notmuch->writable_xapian_db;
1132     try {
1133         db->commit_transaction ();
1134
1135         /* This is a hack for testing.  Xapian never flushes on a
1136          * non-flushed commit, even if the flush threshold is 1.
1137          * However, we rely on flushing to test atomicity. */
1138         const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
1139         if (thresh && atoi (thresh) == 1)
1140             db->commit ();
1141     } catch (const Xapian::Error &error) {
1142         _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n",
1143                                error.get_msg ().c_str ());
1144         notmuch->exception_reported = true;
1145         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1146     }
1147
1148     if (notmuch->atomic_dirty) {
1149         ++notmuch->revision;
1150         notmuch->atomic_dirty = false;
1151     }
1152
1153   DONE:
1154     notmuch->atomic_nesting--;
1155     return NOTMUCH_STATUS_SUCCESS;
1156 }
1157
1158 unsigned long
1159 notmuch_database_get_revision (notmuch_database_t *notmuch,
1160                                const char **uuid)
1161 {
1162     if (uuid)
1163         *uuid = notmuch->uuid;
1164     return notmuch->revision;
1165 }
1166
1167 /* We allow the user to use arbitrarily long paths for directories. But
1168  * we have a term-length limit. So if we exceed that, we'll use the
1169  * SHA-1 of the path for the database term.
1170  *
1171  * Note: This function may return the original value of 'path'. If it
1172  * does not, then the caller is responsible to free() the returned
1173  * value.
1174  */
1175 const char *
1176 _notmuch_database_get_directory_db_path (const char *path)
1177 {
1178     int term_len = strlen (_find_prefix ("directory")) + strlen (path);
1179
1180     if (term_len > NOTMUCH_TERM_MAX)
1181         return _notmuch_sha1_of_string (path);
1182     else
1183         return path;
1184 }
1185
1186 /* Given a path, split it into two parts: the directory part is all
1187  * components except for the last, and the basename is that last
1188  * component. Getting the return-value for either part is optional
1189  * (the caller can pass NULL).
1190  *
1191  * The original 'path' can represent either a regular file or a
1192  * directory---the splitting will be carried out in the same way in
1193  * either case. Trailing slashes on 'path' will be ignored, and any
1194  * cases of multiple '/' characters appearing in series will be
1195  * treated as a single '/'.
1196  *
1197  * Allocation (if any) will have 'ctx' as the talloc owner. But
1198  * pointers will be returned within the original path string whenever
1199  * possible.
1200  *
1201  * Note: If 'path' is non-empty and contains no non-trailing slash,
1202  * (that is, consists of a filename with no parent directory), then
1203  * the directory returned will be an empty string. However, if 'path'
1204  * is an empty string, then both directory and basename will be
1205  * returned as NULL.
1206  */
1207 notmuch_status_t
1208 _notmuch_database_split_path (void *ctx,
1209                               const char *path,
1210                               const char **directory,
1211                               const char **basename)
1212 {
1213     const char *slash;
1214
1215     if (path == NULL || *path == '\0') {
1216         if (directory)
1217             *directory = NULL;
1218         if (basename)
1219             *basename = NULL;
1220         return NOTMUCH_STATUS_SUCCESS;
1221     }
1222
1223     /* Find the last slash (not counting a trailing slash), if any. */
1224
1225     slash = path + strlen (path) - 1;
1226
1227     /* First, skip trailing slashes. */
1228     while (slash != path && *slash == '/')
1229         --slash;
1230
1231     /* Then, find a slash. */
1232     while (slash != path && *slash != '/') {
1233         if (basename)
1234             *basename = slash;
1235
1236         --slash;
1237     }
1238
1239     /* Finally, skip multiple slashes. */
1240     while (slash != path && *(slash - 1) == '/')
1241         --slash;
1242
1243     if (slash == path) {
1244         if (directory)
1245             *directory = talloc_strdup (ctx, "");
1246         if (basename)
1247             *basename = path;
1248     } else {
1249         if (directory)
1250             *directory = talloc_strndup (ctx, path, slash - path);
1251     }
1252
1253     return NOTMUCH_STATUS_SUCCESS;
1254 }
1255
1256 /* Find the document ID of the specified directory.
1257  *
1258  * If (flags & NOTMUCH_FIND_CREATE), a new directory document will be
1259  * created if one does not exist for 'path'.  Otherwise, if the
1260  * directory document does not exist, this sets *directory_id to
1261  * ((unsigned int)-1) and returns NOTMUCH_STATUS_SUCCESS.
1262  */
1263 notmuch_status_t
1264 _notmuch_database_find_directory_id (notmuch_database_t *notmuch,
1265                                      const char *path,
1266                                      notmuch_find_flags_t flags,
1267                                      unsigned int *directory_id)
1268 {
1269     notmuch_directory_t *directory;
1270     notmuch_status_t status;
1271
1272     if (path == NULL) {
1273         *directory_id = 0;
1274         return NOTMUCH_STATUS_SUCCESS;
1275     }
1276
1277     directory = _notmuch_directory_find_or_create (notmuch, path, flags, &status);
1278     if (status || ! directory) {
1279         *directory_id = -1;
1280         return status;
1281     }
1282
1283     *directory_id = _notmuch_directory_get_document_id (directory);
1284
1285     notmuch_directory_destroy (directory);
1286
1287     return NOTMUCH_STATUS_SUCCESS;
1288 }
1289
1290 const char *
1291 _notmuch_database_get_directory_path (void *ctx,
1292                                       notmuch_database_t *notmuch,
1293                                       unsigned int doc_id)
1294 {
1295     Xapian::Document document;
1296
1297     document = find_document_for_doc_id (notmuch, doc_id);
1298
1299     return talloc_strdup (ctx, document.get_data ().c_str ());
1300 }
1301
1302 /* Given a legal 'filename' for the database, (either relative to
1303  * database path or absolute with initial components identical to
1304  * database path), return a new string (with 'ctx' as the talloc
1305  * owner) suitable for use as a direntry term value.
1306  *
1307  * If (flags & NOTMUCH_FIND_CREATE), the necessary directory documents
1308  * will be created in the database as needed.  Otherwise, if the
1309  * necessary directory documents do not exist, this sets
1310  * *direntry to NULL and returns NOTMUCH_STATUS_SUCCESS.
1311  */
1312 notmuch_status_t
1313 _notmuch_database_filename_to_direntry (void *ctx,
1314                                         notmuch_database_t *notmuch,
1315                                         const char *filename,
1316                                         notmuch_find_flags_t flags,
1317                                         char **direntry)
1318 {
1319     const char *relative, *directory, *basename;
1320     Xapian::docid directory_id;
1321     notmuch_status_t status;
1322
1323     relative = _notmuch_database_relative_path (notmuch, filename);
1324
1325     status = _notmuch_database_split_path (ctx, relative,
1326                                            &directory, &basename);
1327     if (status)
1328         return status;
1329
1330     status = _notmuch_database_find_directory_id (notmuch, directory, flags,
1331                                                   &directory_id);
1332     if (status || directory_id == (unsigned int) -1) {
1333         *direntry = NULL;
1334         return status;
1335     }
1336
1337     *direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
1338
1339     return NOTMUCH_STATUS_SUCCESS;
1340 }
1341
1342 /* Given a legal 'path' for the database, return the relative path.
1343  *
1344  * The return value will be a pointer to the original path contents,
1345  * and will be either the original string (if 'path' was relative) or
1346  * a portion of the string (if path was absolute and begins with the
1347  * database path).
1348  */
1349 const char *
1350 _notmuch_database_relative_path (notmuch_database_t *notmuch,
1351                                  const char *path)
1352 {
1353     const char *db_path, *relative;
1354     unsigned int db_path_len;
1355
1356     db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
1357     db_path_len = strlen (db_path);
1358
1359     relative = path;
1360
1361     if (*relative == '/') {
1362         while (*relative == '/' && *(relative + 1) == '/')
1363             relative++;
1364
1365         if (strncmp (relative, db_path, db_path_len) == 0) {
1366             relative += db_path_len;
1367             while (*relative == '/')
1368                 relative++;
1369         }
1370     }
1371
1372     return relative;
1373 }
1374
1375 notmuch_status_t
1376 notmuch_database_get_directory (notmuch_database_t *notmuch,
1377                                 const char *path,
1378                                 notmuch_directory_t **directory)
1379 {
1380     notmuch_status_t status;
1381
1382     if (directory == NULL)
1383         return NOTMUCH_STATUS_NULL_POINTER;
1384     *directory = NULL;
1385
1386     try {
1387         *directory = _notmuch_directory_find_or_create (notmuch, path,
1388                                                         NOTMUCH_FIND_LOOKUP, &status);
1389     } catch (const Xapian::Error &error) {
1390         _notmuch_database_log (notmuch, "A Xapian exception occurred getting directory: %s.\n",
1391                                error.get_msg ().c_str ());
1392         notmuch->exception_reported = true;
1393         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1394     }
1395     return status;
1396 }
1397
1398 /* Allocate a document ID that satisfies the following criteria:
1399  *
1400  * 1. The ID does not exist for any document in the Xapian database
1401  *
1402  * 2. The ID was not previously returned from this function
1403  *
1404  * 3. The ID is the smallest integer satisfying (1) and (2)
1405  *
1406  * This function will trigger an internal error if these constraints
1407  * cannot all be satisfied, (that is, the pool of available document
1408  * IDs has been exhausted).
1409  */
1410 unsigned int
1411 _notmuch_database_generate_doc_id (notmuch_database_t *notmuch)
1412 {
1413     assert (notmuch->last_doc_id >= notmuch->xapian_db->get_lastdocid ());
1414
1415     notmuch->last_doc_id++;
1416
1417     if (notmuch->last_doc_id == 0)
1418         INTERNAL_ERROR ("Xapian document IDs are exhausted.\n");
1419
1420     return notmuch->last_doc_id;
1421 }
1422
1423 notmuch_status_t
1424 notmuch_database_remove_message (notmuch_database_t *notmuch,
1425                                  const char *filename)
1426 {
1427     notmuch_status_t status;
1428     notmuch_message_t *message;
1429
1430     status = notmuch_database_find_message_by_filename (notmuch, filename,
1431                                                         &message);
1432
1433     if (status == NOTMUCH_STATUS_SUCCESS && message) {
1434         status = _notmuch_message_remove_filename (message, filename);
1435         if (status == NOTMUCH_STATUS_SUCCESS)
1436             _notmuch_message_delete (message);
1437         else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
1438             _notmuch_message_sync (message);
1439
1440         notmuch_message_destroy (message);
1441     }
1442
1443     return status;
1444 }
1445
1446 notmuch_status_t
1447 notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
1448                                            const char *filename,
1449                                            notmuch_message_t **message_ret)
1450 {
1451     void *local;
1452     const char *prefix = _find_prefix ("file-direntry");
1453     char *direntry, *term;
1454     Xapian::PostingIterator i, end;
1455     notmuch_status_t status;
1456
1457     if (message_ret == NULL)
1458         return NOTMUCH_STATUS_NULL_POINTER;
1459
1460     if (! (notmuch->features & NOTMUCH_FEATURE_FILE_TERMS))
1461         return NOTMUCH_STATUS_UPGRADE_REQUIRED;
1462
1463     /* return NULL on any failure */
1464     *message_ret = NULL;
1465
1466     local = talloc_new (notmuch);
1467
1468     try {
1469         status = _notmuch_database_filename_to_direntry (
1470             local, notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
1471         if (status || ! direntry)
1472             goto DONE;
1473
1474         term = talloc_asprintf (local, "%s%s", prefix, direntry);
1475
1476         find_doc_ids_for_term (notmuch, term, &i, &end);
1477
1478         if (i != end) {
1479             notmuch_private_status_t private_status;
1480
1481             *message_ret = _notmuch_message_create (notmuch, notmuch, *i,
1482                                                     &private_status);
1483             if (*message_ret == NULL)
1484                 status = NOTMUCH_STATUS_OUT_OF_MEMORY;
1485         }
1486     } catch (const Xapian::Error &error) {
1487         _notmuch_database_log (notmuch,
1488                                "Error: A Xapian exception occurred finding message by filename: %s\n",
1489                                error.get_msg ().c_str ());
1490         notmuch->exception_reported = true;
1491         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1492     }
1493
1494   DONE:
1495     talloc_free (local);
1496
1497     if (status && *message_ret) {
1498         notmuch_message_destroy (*message_ret);
1499         *message_ret = NULL;
1500     }
1501     return status;
1502 }
1503
1504 notmuch_string_list_t *
1505 _notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i,
1506                                          Xapian::TermIterator &end,
1507                                          const char *prefix)
1508 {
1509     int prefix_len = strlen (prefix);
1510     notmuch_string_list_t *list;
1511
1512     list = _notmuch_string_list_create (ctx);
1513     if (unlikely (list == NULL))
1514         return NULL;
1515
1516     for (i.skip_to (prefix); i != end; i++) {
1517         /* Terminate loop at first term without desired prefix. */
1518         if (strncmp ((*i).c_str (), prefix, prefix_len))
1519             break;
1520
1521         _notmuch_string_list_append (list, (*i).c_str () + prefix_len);
1522     }
1523
1524     return list;
1525 }
1526
1527 notmuch_tags_t *
1528 notmuch_database_get_all_tags (notmuch_database_t *db)
1529 {
1530     Xapian::TermIterator i, end;
1531     notmuch_string_list_t *tags;
1532
1533     try {
1534         i = db->xapian_db->allterms_begin ();
1535         end = db->xapian_db->allterms_end ();
1536         tags = _notmuch_database_get_terms_with_prefix (db, i, end,
1537                                                         _find_prefix ("tag"));
1538         _notmuch_string_list_sort (tags);
1539         return _notmuch_tags_create (db, tags);
1540     } catch (const Xapian::Error &error) {
1541         _notmuch_database_log (db, "A Xapian exception occurred getting tags: %s.\n",
1542                                error.get_msg ().c_str ());
1543         db->exception_reported = true;
1544         return NULL;
1545     }
1546 }
1547
1548 const char *
1549 notmuch_database_status_string (const notmuch_database_t *notmuch)
1550 {
1551     return notmuch->status_string;
1552 }