+ unsigned int i, version;
+ static int initialized = 0;
+
+ if (path == NULL) {
+ message = strdup ("Error: Cannot open a database for a NULL path.\n");
+ status = NOTMUCH_STATUS_NULL_POINTER;
+ goto DONE;
+ }
+
+ if (path[0] != '/') {
+ message = strdup ("Error: Database path must be absolute.\n");
+ status = NOTMUCH_STATUS_PATH_ERROR;
+ goto DONE;
+ }
+
+ if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
+ message = strdup ("Out of memory\n");
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ err = stat (notmuch_path, &st);
+ if (err) {
+ IGNORE_RESULT (asprintf (&message, "Error opening database at %s: %s\n",
+ notmuch_path, strerror (errno)));
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
+ message = strdup ("Out of memory\n");
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ /* Initialize the GLib type system and threads */
+#if !GLIB_CHECK_VERSION(2, 35, 1)
+ g_type_init ();
+#endif
+
+ /* Initialize gmime */
+ if (! initialized) {
+ g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
+ initialized = 1;
+ }
+
+ notmuch = talloc_zero (NULL, notmuch_database_t);
+ notmuch->exception_reported = FALSE;
+ notmuch->status_string = NULL;
+ notmuch->path = talloc_strdup (notmuch, path);
+
+ if (notmuch->path[strlen (notmuch->path) - 1] == '/')
+ notmuch->path[strlen (notmuch->path) - 1] = '\0';
+
+ notmuch->mode = mode;
+ notmuch->atomic_nesting = 0;
+ try {
+ string last_thread_id;
+ string last_mod;
+
+ if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
+ notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
+ Xapian::DB_CREATE_OR_OPEN);
+ } else {
+ notmuch->xapian_db = new Xapian::Database (xapian_path);
+ }
+
+ /* Check version. As of database version 3, we represent
+ * changes in terms of features, so assume a version bump
+ * means a dramatically incompatible change. */
+ version = notmuch_database_get_version (notmuch);
+ if (version > NOTMUCH_DATABASE_VERSION) {
+ IGNORE_RESULT (asprintf (&message,
+ "Error: Notmuch database at %s\n"
+ " has a newer database format version (%u) than supported by this\n"
+ " version of notmuch (%u).\n",
+ notmuch_path, version, NOTMUCH_DATABASE_VERSION));
+ notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+ notmuch_database_destroy (notmuch);
+ notmuch = NULL;
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ /* Check features. */
+ incompat_features = NULL;
+ notmuch->features = _parse_features (
+ local, notmuch->xapian_db->get_metadata ("features").c_str (),
+ version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',
+ &incompat_features);
+ if (incompat_features) {
+ IGNORE_RESULT (asprintf (&message,
+ "Error: Notmuch database at %s\n"
+ " requires features (%s)\n"
+ " not supported by this version of notmuch.\n",
+ notmuch_path, incompat_features));
+ notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+ notmuch_database_destroy (notmuch);
+ notmuch = NULL;
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
+ last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
+ if (last_thread_id.empty ()) {
+ notmuch->last_thread_id = 0;
+ } else {
+ const char *str;
+ char *end;
+
+ str = last_thread_id.c_str ();
+ notmuch->last_thread_id = strtoull (str, &end, 16);
+ if (*end != '\0')
+ INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
+ }
+
+ /* Get current highest revision number. */
+ last_mod = notmuch->xapian_db->get_value_upper_bound (
+ NOTMUCH_VALUE_LAST_MOD);
+ if (last_mod.empty ())
+ notmuch->revision = 0;
+ else
+ notmuch->revision = Xapian::sortable_unserialise (last_mod);
+ notmuch->uuid = talloc_strdup (
+ notmuch, notmuch->xapian_db->get_uuid ().c_str ());
+
+ notmuch->query_parser = new Xapian::QueryParser;
+ notmuch->term_gen = new Xapian::TermGenerator;
+ notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
+ notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+ notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+ notmuch->last_mod_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:");
+
+ notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
+ notmuch->query_parser->set_database (*notmuch->xapian_db);
+ notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
+ notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
+ notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
+ notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor);
+ notmuch->query_parser->add_valuerangeprocessor (notmuch->last_mod_range_processor);
+
+ for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
+ prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
+ notmuch->query_parser->add_boolean_prefix (prefix->name,
+ prefix->prefix);
+ }
+
+ for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
+ prefix_t *prefix = &PROBABILISTIC_PREFIX[i];
+ notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
+ }
+ } catch (const Xapian::Error &error) {
+ IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
+ error.get_msg().c_str()));
+ notmuch_database_destroy (notmuch);
+ notmuch = NULL;
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+ }
+
+ DONE:
+ talloc_free (local);
+
+ if (message) {
+ if (status_string)
+ *status_string = message;
+ else
+ free (message);
+ }
+
+ if (database)
+ *database = notmuch;
+ else
+ talloc_free (notmuch);
+ return status;
+}
+
+notmuch_status_t
+notmuch_database_close (notmuch_database_t *notmuch)
+{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+ /* Many Xapian objects (and thus notmuch objects) hold references to
+ * the database, so merely deleting the database may not suffice to
+ * close it. Thus, we explicitly close it here. */
+ if (notmuch->xapian_db != NULL) {
+ try {
+ /* If there's an outstanding transaction, it's unclear if
+ * closing the Xapian database commits everything up to
+ * that transaction, or may discard committed (but
+ * unflushed) transactions. To be certain, explicitly
+ * cancel any outstanding transaction before closing. */
+ if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE &&
+ notmuch->atomic_nesting)
+ (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))
+ ->cancel_transaction ();
+
+ /* Close the database. This implicitly flushes
+ * outstanding changes. */
+ notmuch->xapian_db->close();
+ } catch (const Xapian::Error &error) {
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+ if (! notmuch->exception_reported) {
+ _notmuch_database_log (notmuch, "Error: A Xapian exception occurred closing database: %s\n",
+ error.get_msg().c_str());
+ }
+ }
+ }
+
+ delete notmuch->term_gen;
+ notmuch->term_gen = NULL;
+ delete notmuch->query_parser;
+ notmuch->query_parser = NULL;
+ delete notmuch->xapian_db;
+ notmuch->xapian_db = NULL;
+ delete notmuch->value_range_processor;
+ notmuch->value_range_processor = NULL;
+ delete notmuch->date_range_processor;
+ notmuch->date_range_processor = NULL;
+ delete notmuch->last_mod_range_processor;
+ notmuch->last_mod_range_processor = NULL;
+
+ return status;
+}
+
+#if HAVE_XAPIAN_COMPACT
+static int
+unlink_cb (const char *path,
+ unused (const struct stat *sb),
+ unused (int type),
+ unused (struct FTW *ftw))
+{
+ return remove (path);
+}
+
+static int
+rmtree (const char *path)
+{
+ return nftw (path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
+}
+
+class NotmuchCompactor : public Xapian::Compactor
+{
+ notmuch_compact_status_cb_t status_cb;
+ void *status_closure;
+
+public:
+ NotmuchCompactor(notmuch_compact_status_cb_t cb, void *closure) :
+ status_cb (cb), status_closure (closure) { }
+
+ virtual void
+ set_status (const std::string &table, const std::string &status)
+ {
+ char *msg;
+
+ if (status_cb == NULL)
+ return;
+
+ if (status.length () == 0)
+ msg = talloc_asprintf (NULL, "compacting table %s", table.c_str());
+ else
+ msg = talloc_asprintf (NULL, " %s", status.c_str());
+
+ if (msg == NULL) {
+ return;
+ }
+
+ status_cb (msg, status_closure);
+ talloc_free (msg);
+ }
+};
+
+/* Compacts the given database, optionally saving the original database
+ * in backup_path. Additionally, a callback function can be provided to
+ * give the user feedback on the progress of the (likely long-lived)
+ * compaction process.
+ *
+ * The backup path must point to a directory on the same volume as the
+ * original database. Passing a NULL backup_path will result in the
+ * uncompacted database being deleted after compaction has finished.
+ * Note that the database write lock will be held during the
+ * compaction process to protect data integrity.
+ */
+notmuch_status_t
+notmuch_database_compact (const char *path,
+ const char *backup_path,
+ notmuch_compact_status_cb_t status_cb,
+ void *closure)
+{
+ void *local;
+ char *notmuch_path, *xapian_path, *compact_xapian_path;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ notmuch_database_t *notmuch = NULL;
+ struct stat statbuf;
+ notmuch_bool_t keep_backup;
+ char *message = NULL;
+
+ local = talloc_new (NULL);
+ if (! local)
+ return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+ ret = notmuch_database_open_verbose (path,
+ NOTMUCH_DATABASE_MODE_READ_WRITE,
+ ¬much,
+ &message);
+ if (ret) {
+ if (status_cb) status_cb (message, closure);
+ goto DONE;
+ }
+
+ if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ if (backup_path == NULL) {
+ if (! (backup_path = talloc_asprintf (local, "%s.old", xapian_path))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+ keep_backup = FALSE;
+ }
+ else {
+ keep_backup = TRUE;
+ }