+notmuch_status_t
+notmuch_database_destroy (notmuch_database_t *notmuch)
+{
+ notmuch_status_t status;
+
+ status = notmuch_database_close (notmuch);
+ talloc_free (notmuch);
+
+ return status;
+}
+
+const char *
+notmuch_database_get_path (notmuch_database_t *notmuch)
+{
+ return notmuch->path;
+}
+
+unsigned int
+notmuch_database_get_version (notmuch_database_t *notmuch)
+{
+ unsigned int version;
+ string version_string;
+ const char *str;
+ char *end;
+
+ version_string = notmuch->xapian_db->get_metadata ("version");
+ if (version_string.empty ())
+ return 0;
+
+ str = version_string.c_str ();
+ if (str == NULL || *str == '\0')
+ return 0;
+
+ version = strtoul (str, &end, 10);
+ if (*end != '\0')
+ INTERNAL_ERROR ("Malformed database version: %s", str);
+
+ return version;
+}
+
+notmuch_bool_t
+notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
+{
+ return notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE &&
+ ((NOTMUCH_FEATURES_CURRENT & ~notmuch->features) ||
+ (notmuch_database_get_version (notmuch) < NOTMUCH_DATABASE_VERSION));
+}
+
+static volatile sig_atomic_t do_progress_notify = 0;
+
+static void
+handle_sigalrm (unused (int signal))
+{
+ do_progress_notify = 1;
+}
+
+/* Upgrade the current database.
+ *
+ * After opening a database in read-write mode, the client should
+ * check if an upgrade is needed (notmuch_database_needs_upgrade) and
+ * if so, upgrade with this function before making any modifications.
+ *
+ * The optional progress_notify callback can be used by the caller to
+ * provide progress indication to the user. If non-NULL it will be
+ * called periodically with 'count' as the number of messages upgraded
+ * so far and 'total' the overall number of messages that will be
+ * converted.
+ */
+notmuch_status_t
+notmuch_database_upgrade (notmuch_database_t *notmuch,
+ void (*progress_notify) (void *closure,
+ double progress),
+ void *closure)
+{
+ void *local = talloc_new (NULL);
+ Xapian::TermIterator t, t_end;
+ Xapian::WritableDatabase *db;
+ struct sigaction action;
+ struct itimerval timerval;
+ notmuch_bool_t timer_is_active = FALSE;
+ enum _notmuch_features target_features, new_features;
+ notmuch_status_t status;
+ notmuch_private_status_t private_status;
+ unsigned int count = 0, total = 0;
+
+ status = _notmuch_database_ensure_writable (notmuch);
+ if (status)
+ return status;
+
+ db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+ target_features = notmuch->features | NOTMUCH_FEATURES_CURRENT;
+ new_features = NOTMUCH_FEATURES_CURRENT & ~notmuch->features;
+
+ if (! notmuch_database_needs_upgrade (notmuch))
+ return NOTMUCH_STATUS_SUCCESS;
+
+ if (progress_notify) {
+ /* Setup our handler for SIGALRM */
+ memset (&action, 0, sizeof (struct sigaction));
+ action.sa_handler = handle_sigalrm;
+ sigemptyset (&action.sa_mask);
+ action.sa_flags = SA_RESTART;
+ sigaction (SIGALRM, &action, NULL);
+
+ /* Then start a timer to send SIGALRM once per second. */
+ timerval.it_interval.tv_sec = 1;
+ timerval.it_interval.tv_usec = 0;
+ timerval.it_value.tv_sec = 1;
+ timerval.it_value.tv_usec = 0;
+ setitimer (ITIMER_REAL, &timerval, NULL);
+
+ timer_is_active = TRUE;
+ }
+
+ /* Figure out how much total work we need to do. */
+ if (new_features &
+ (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) {
+ notmuch_query_t *query = notmuch_query_create (notmuch, "");
+ total += notmuch_query_count_messages (query);
+ notmuch_query_destroy (query);
+ }
+ if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
+ t_end = db->allterms_end ("XTIMESTAMP");
+ for (t = db->allterms_begin ("XTIMESTAMP"); t != t_end; t++)
+ ++total;
+ }
+ if (new_features & NOTMUCH_FEATURE_GHOSTS) {
+ /* The ghost message upgrade converts all thread_id_*
+ * metadata values into ghost message documents. */
+ t_end = db->metadata_keys_end ("thread_id_");
+ for (t = db->metadata_keys_begin ("thread_id_"); t != t_end; ++t)
+ ++total;
+ }
+
+ /* Perform the upgrade in a transaction. */
+ db->begin_transaction (true);
+
+ /* Set the target features so we write out changes in the desired
+ * format. */
+ notmuch->features = target_features;
+
+ /* Perform per-message upgrades. */
+ if (new_features &
+ (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) {
+ notmuch_query_t *query = notmuch_query_create (notmuch, "");
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ char *filename;
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ if (do_progress_notify) {
+ progress_notify (closure, (double) count / total);
+ do_progress_notify = 0;
+ }
+
+ message = notmuch_messages_get (messages);
+
+ /* Before version 1, each message document had its
+ * filename in the data field. Copy that into the new
+ * format by calling notmuch_message_add_filename.
+ */
+ if (new_features & NOTMUCH_FEATURE_FILE_TERMS) {
+ filename = _notmuch_message_talloc_copy_data (message);
+ if (filename && *filename != '\0') {
+ _notmuch_message_add_filename (message, filename);
+ _notmuch_message_clear_data (message);
+ }
+ talloc_free (filename);
+ }
+
+ /* Prior to version 2, the "folder:" prefix was
+ * probabilistic and stemmed. Change it to the current
+ * boolean prefix. Add "path:" prefixes while at it.
+ */
+ if (new_features & NOTMUCH_FEATURE_BOOL_FOLDER)
+ _notmuch_message_upgrade_folder (message);
+
+ _notmuch_message_sync (message);
+
+ notmuch_message_destroy (message);
+
+ count++;
+ }
+
+ notmuch_query_destroy (query);
+ }
+
+ /* Perform per-directory upgrades. */
+
+ /* Before version 1 we stored directory timestamps in
+ * XTIMESTAMP documents instead of the current XDIRECTORY
+ * documents. So copy those as well. */
+ if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
+ t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
+
+ for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
+ t != t_end;
+ t++)
+ {
+ Xapian::PostingIterator p, p_end;
+ std::string term = *t;
+
+ p_end = notmuch->xapian_db->postlist_end (term);
+
+ for (p = notmuch->xapian_db->postlist_begin (term);
+ p != p_end;
+ p++)
+ {
+ Xapian::Document document;
+ time_t mtime;
+ notmuch_directory_t *directory;
+
+ if (do_progress_notify) {
+ progress_notify (closure, (double) count / total);
+ do_progress_notify = 0;
+ }
+
+ document = find_document_for_doc_id (notmuch, *p);
+ mtime = Xapian::sortable_unserialise (
+ document.get_value (NOTMUCH_VALUE_TIMESTAMP));
+
+ directory = _notmuch_directory_create (notmuch, term.c_str() + 10,
+ NOTMUCH_FIND_CREATE, &status);
+ notmuch_directory_set_mtime (directory, mtime);
+ notmuch_directory_destroy (directory);
+
+ db->delete_document (*p);
+ }
+
+ ++count;
+ }
+ }
+
+ /* Perform metadata upgrades. */
+
+ /* Prior to NOTMUCH_FEATURE_GHOSTS, thread IDs for missing
+ * messages were stored as database metadata. Change these to
+ * ghost messages.
+ */
+ if (new_features & NOTMUCH_FEATURE_GHOSTS) {
+ notmuch_message_t *message;
+ std::string message_id, thread_id;
+
+ t_end = db->metadata_keys_end (NOTMUCH_METADATA_THREAD_ID_PREFIX);
+ for (t = db->metadata_keys_begin (NOTMUCH_METADATA_THREAD_ID_PREFIX);
+ t != t_end; ++t) {
+ if (do_progress_notify) {
+ progress_notify (closure, (double) count / total);
+ do_progress_notify = 0;
+ }
+
+ message_id = (*t).substr (
+ strlen (NOTMUCH_METADATA_THREAD_ID_PREFIX));
+ thread_id = db->get_metadata (*t);
+
+ /* Create ghost message */
+ message = _notmuch_message_create_for_message_id (
+ notmuch, message_id.c_str (), &private_status);
+ if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+ /* Document already exists; ignore the stored thread ID */
+ } else if (private_status ==
+ NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+ private_status = _notmuch_message_initialize_ghost (
+ message, thread_id.c_str ());
+ if (! private_status)
+ _notmuch_message_sync (message);
+ }
+
+ if (private_status) {
+ _notmuch_database_log (notmuch,
+ "Upgrade failed while creating ghost messages.\n");
+ status = COERCE_STATUS (private_status, "Unexpected status from _notmuch_message_initialize_ghost");
+ goto DONE;
+ }
+
+ /* Clear saved metadata thread ID */
+ db->set_metadata (*t, "");
+
+ ++count;
+ }
+ }
+
+ status = NOTMUCH_STATUS_SUCCESS;
+ db->set_metadata ("features", _print_features (local, notmuch->features));
+ db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
+
+ DONE:
+ if (status == NOTMUCH_STATUS_SUCCESS)
+ db->commit_transaction ();
+ else
+ db->cancel_transaction ();
+
+ if (timer_is_active) {
+ /* Now stop the timer. */
+ timerval.it_interval.tv_sec = 0;
+ timerval.it_interval.tv_usec = 0;
+ timerval.it_value.tv_sec = 0;
+ timerval.it_value.tv_usec = 0;
+ setitimer (ITIMER_REAL, &timerval, NULL);
+
+ /* And disable the signal handler. */
+ action.sa_handler = SIG_IGN;
+ sigaction (SIGALRM, &action, NULL);
+ }
+
+ talloc_free (local);
+ return status;
+}
+
+notmuch_status_t
+notmuch_database_begin_atomic (notmuch_database_t *notmuch)
+{
+ if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
+ notmuch->atomic_nesting > 0)
+ goto DONE;
+
+ try {
+ (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
+ } catch (const Xapian::Error &error) {
+ _notmuch_database_log (notmuch, "A Xapian exception occurred beginning transaction: %s.\n",
+ error.get_msg().c_str());
+ notmuch->exception_reported = TRUE;
+ return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+ }
+
+DONE:
+ notmuch->atomic_nesting++;
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_database_end_atomic (notmuch_database_t *notmuch)
+{
+ Xapian::WritableDatabase *db;
+
+ if (notmuch->atomic_nesting == 0)
+ return NOTMUCH_STATUS_UNBALANCED_ATOMIC;
+
+ if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
+ notmuch->atomic_nesting > 1)
+ goto DONE;
+
+ db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+ try {
+ db->commit_transaction ();
+
+ /* This is a hack for testing. Xapian never flushes on a
+ * non-flushed commit, even if the flush threshold is 1.
+ * However, we rely on flushing to test atomicity. */
+ const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
+ if (thresh && atoi (thresh) == 1)
+ db->flush ();
+ } catch (const Xapian::Error &error) {
+ _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n",
+ error.get_msg().c_str());
+ notmuch->exception_reported = TRUE;
+ return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+ }
+
+DONE:
+ notmuch->atomic_nesting--;
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* We allow the user to use arbitrarily long paths for directories. But
+ * we have a term-length limit. So if we exceed that, we'll use the
+ * SHA-1 of the path for the database term.
+ *
+ * Note: This function may return the original value of 'path'. If it
+ * does not, then the caller is responsible to free() the returned
+ * value.
+ */
+const char *
+_notmuch_database_get_directory_db_path (const char *path)
+{
+ int term_len = strlen (_find_prefix ("directory")) + strlen (path);
+
+ if (term_len > NOTMUCH_TERM_MAX)
+ return _notmuch_sha1_of_string (path);
+ else
+ return path;
+}
+
+/* Given a path, split it into two parts: the directory part is all
+ * components except for the last, and the basename is that last
+ * component. Getting the return-value for either part is optional
+ * (the caller can pass NULL).
+ *
+ * The original 'path' can represent either a regular file or a
+ * directory---the splitting will be carried out in the same way in
+ * either case. Trailing slashes on 'path' will be ignored, and any
+ * cases of multiple '/' characters appearing in series will be
+ * treated as a single '/'.
+ *
+ * Allocation (if any) will have 'ctx' as the talloc owner. But
+ * pointers will be returned within the original path string whenever
+ * possible.
+ *
+ * Note: If 'path' is non-empty and contains no non-trailing slash,
+ * (that is, consists of a filename with no parent directory), then
+ * the directory returned will be an empty string. However, if 'path'
+ * is an empty string, then both directory and basename will be
+ * returned as NULL.
+ */
+notmuch_status_t
+_notmuch_database_split_path (void *ctx,
+ const char *path,
+ const char **directory,
+ const char **basename)
+{
+ const char *slash;
+
+ if (path == NULL || *path == '\0') {
+ if (directory)
+ *directory = NULL;
+ if (basename)
+ *basename = NULL;
+ return NOTMUCH_STATUS_SUCCESS;
+ }
+
+ /* Find the last slash (not counting a trailing slash), if any. */
+
+ slash = path + strlen (path) - 1;
+
+ /* First, skip trailing slashes. */
+ while (slash != path) {
+ if (*slash != '/')
+ break;
+
+ --slash;
+ }
+
+ /* Then, find a slash. */
+ while (slash != path) {
+ if (*slash == '/')
+ break;
+
+ if (basename)
+ *basename = slash;
+
+ --slash;
+ }
+
+ /* Finally, skip multiple slashes. */
+ while (slash != path) {
+ if (*slash != '/')
+ break;
+
+ --slash;
+ }
+
+ if (slash == path) {
+ if (directory)
+ *directory = talloc_strdup (ctx, "");
+ if (basename)
+ *basename = path;
+ } else {
+ if (directory)
+ *directory = talloc_strndup (ctx, path, slash - path + 1);
+ }
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Find the document ID of the specified directory.