Merge branch 'upstream'

This commit is contained in:
martin f. krafft 2010-01-21 13:58:55 +13:00
commit 0ea5f3fc0e
17 changed files with 1881 additions and 516 deletions

6
TODO
View file

@ -74,6 +74,8 @@ for selecting what gets printed).
Add a "--count-only" (or so?) option to "notmuch search" for returning
the count of search results.
Add documented syntax for searching all threads/messages.
Give "notmuch restore" some progress indicator. Until we get the
Xapian bugs fixed that are making this operation slow, we really need
to let the user know that things are still moving.
@ -146,6 +148,10 @@ notmuch initially sees all changes as interesting, and quickly learns
from the user which changes are not interesting (such as the very
common mailing-list footer).
Fix notmuch_query_count_messages to share code with
notmuch_query_search_messages rather than duplicating code. (And
consider renaming it as well.)
General
-------
Audit everything for dealing with out-of-memory (and drop xutil.c).

View file

@ -1,5 +1,5 @@
all:
$(MAKE) -C .. all
clean:
$(MAKE) -C .. clean
.DEFAULT:
$(MAKE) -C .. $@

26
configure vendored
View file

@ -6,6 +6,7 @@ CC=${CC:-gcc}
CXX=${CXX:-g++}
CFLAGS=${CFLAGS:--O2}
CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)}
XAPIAN_CONFIG=${XAPIAN_CONFIG:-xapian-config-1.1 xapian-config}
# Set the defaults for values the user can specify with command-line
# options.
@ -37,6 +38,13 @@ First, some common variables can specified via environment variables:
Each of these values can further be controlled by specifying them
later on the "make" command line.
Other environment variables can be used to control configure itself,
(and for which there is no equivalent build-time control):
XAPIAN_CONFIG The program to use to determine flags for
compiling and linking against the Xapian
library. [$XAPIAN_CONFIG]
Additionally, various options can be specified on the configure
command line.
@ -91,14 +99,18 @@ else
fi
printf "Checking for Xapian development files... "
if xapian-config --version > /dev/null 2>&1; then
printf "Yes.\n"
have_xapian=1
xapian_cxxflags=$(xapian-config --cxxflags)
xapian_ldflags=$(xapian-config --libs)
else
have_xapian=0
for xapian_config in ${XAPIAN_CONFIG}; do
if ${xapian_config} --version > /dev/null 2>&1; then
printf "Yes.\n"
have_xapian=1
xapian_cxxflags=$(${xapian_config} --cxxflags)
xapian_ldflags=$(${xapian_config} --libs)
break
fi
done
if [ ${have_xapian} = "0" ]; then
printf "No.\n"
have_xapian=0
errors=$((errors + 1))
fi

View file

@ -1,5 +1,7 @@
# See Makfefile.local for the list of files to be compiled in this
# directory.
all:
$(MAKE) -C .. all
clean:
$(MAKE) -C .. clean
.DEFAULT:
$(MAKE) -C .. $@

View file

@ -11,6 +11,7 @@ libnotmuch_c_srcs = \
libnotmuch_cxx_srcs = \
$(dir)/database.cc \
$(dir)/directory.cc \
$(dir)/index.cc \
$(dir)/message.cc \
$(dir)/query.cc \

View file

@ -33,6 +33,8 @@ struct _notmuch_database {
Xapian::QueryParser *query_parser;
Xapian::TermGenerator *term_gen;
Xapian::ValueRangeProcessor *value_range_processor;
notmuch_bool_t needs_upgrade;
};
/* Convert tags from Xapian internal format to notmuch format.

View file

@ -22,6 +22,8 @@
#include <iostream>
#include <sys/time.h>
#include <signal.h>
#include <xapian.h>
#include <glib.h> /* g_free, GPtrArray, GHashTable */
@ -35,9 +37,14 @@ typedef struct {
const char *prefix;
} prefix_t;
/* Here's the current schema for our database:
#define NOTMUCH_DATABASE_VERSION 1
#define STRINGIFY(s) _SUB_STRINGIFY(s)
#define _SUB_STRINGIFY(s) #s
/* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
*
* We currently have two different types of documents: mail and timestamps.
* We currently have two different types of documents: mail and directory.
*
* Mail document
* -------------
@ -63,6 +70,12 @@ typedef struct {
*
* tag: Any tags associated with this message by the user.
*
* file-direntry: A colon-separated pair of values
* (INTEGER:STRING), where INTEGER is the
* document ID of a directory document, and
* STRING is the name of a file within that
* directory for this mail message.
*
* A mail document also has two values:
*
* TIMESTAMP: The time_t value corresponding to the message's
@ -75,21 +88,36 @@ typedef struct {
* user in searching. But the database doesn't really care itself
* about any of these.
*
* Timestamp document
* The data portion of a mail document is empty.
*
* Directory document
* ------------------
* A timestamp document is used by a client of the notmuch library to
* A directory document is used by a client of the notmuch library to
* maintain data necessary to allow for efficient polling of mail
* directories. The notmuch library does no interpretation of
* timestamps, but merely allows the user to store and retrieve
* timestamps as name/value pairs.
* directories.
*
* The timestamp document is indexed with a single prefixed term:
* All directory documents contain one term:
*
* timestamp: The user's key value (likely a directory name)
* directory: The directory path (relative to the database path)
* Or the SHA1 sum of the directory path (if the
* path itself is too long to fit in a Xapian
* term).
*
* and has a single value:
* And all directory documents for directories other than top-level
* directories also contain the following term:
*
* TIMESTAMP: The time_t value from the user.
* directory-direntry: A colon-separated pair of values
* (INTEGER:STRING), where INTEGER is the
* document ID of the parent directory
* document, and STRING is the name of this
* directory within that parent.
*
* All directory documents have a single value:
*
* TIMESTAMP: The mtime of the directory (at last scan)
*
* The data portion of a directory document contains the path of the
* directory (relative to the database path).
*/
/* With these prefix values we follow the conventions published here:
@ -108,23 +136,25 @@ typedef struct {
*/
prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
{ "type", "T" },
{ "reference", "XREFERENCE" },
{ "replyto", "XREPLYTO" },
{ "timestamp", "XTIMESTAMP" },
{ "type", "T" },
{ "reference", "XREFERENCE" },
{ "replyto", "XREPLYTO" },
{ "directory", "XDIRECTORY" },
{ "file-direntry", "XFDIRENTRY" },
{ "directory-direntry", "XDDIRENTRY" },
};
prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
{ "thread", "G" },
{ "tag", "K" },
{ "id", "Q" }
{ "thread", "G" },
{ "tag", "K" },
{ "id", "Q" }
};
prefix_t PROBABILISTIC_PREFIX[]= {
{ "from", "XFROM" },
{ "to", "XTO" },
{ "attachment", "XATTACHMENT" },
{ "subject", "XSUBJECT"}
{ "from", "XFROM" },
{ "to", "XTO" },
{ "attachment", "XATTACHMENT" },
{ "subject", "XSUBJECT"}
};
int
@ -175,8 +205,8 @@ notmuch_status_to_string (notmuch_status_t status)
return "No error occurred";
case NOTMUCH_STATUS_OUT_OF_MEMORY:
return "Out of memory";
case NOTMUCH_STATUS_READONLY_DATABASE:
return "The database is read-only";
case NOTMUCH_STATUS_READ_ONLY_DATABASE:
return "Attempt to write to a read-only database";
case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
return "A Xapian exception occurred";
case NOTMUCH_STATUS_FILE_ERROR:
@ -197,6 +227,17 @@ notmuch_status_to_string (notmuch_status_t status)
}
}
static void
find_doc_ids_for_term (notmuch_database_t *notmuch,
const char *term,
Xapian::PostingIterator *begin,
Xapian::PostingIterator *end)
{
*begin = notmuch->xapian_db->postlist_begin (term);
*end = notmuch->xapian_db->postlist_end (term);
}
static void
find_doc_ids (notmuch_database_t *notmuch,
const char *prefix_name,
@ -204,24 +245,21 @@ find_doc_ids (notmuch_database_t *notmuch,
Xapian::PostingIterator *begin,
Xapian::PostingIterator *end)
{
Xapian::PostingIterator i;
char *term;
term = talloc_asprintf (notmuch, "%s%s",
_find_prefix (prefix_name), value);
*begin = notmuch->xapian_db->postlist_begin (term);
*end = notmuch->xapian_db->postlist_end (term);
find_doc_ids_for_term (notmuch, term, begin, end);
talloc_free (term);
}
static notmuch_private_status_t
find_unique_doc_id (notmuch_database_t *notmuch,
const char *prefix_name,
const char *value,
unsigned int *doc_id)
notmuch_private_status_t
_notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
const char *prefix_name,
const char *value,
unsigned int *doc_id)
{
Xapian::PostingIterator i, end;
@ -230,10 +268,19 @@ find_unique_doc_id (notmuch_database_t *notmuch,
if (i == end) {
*doc_id = 0;
return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
} else {
*doc_id = *i;
return NOTMUCH_PRIVATE_STATUS_SUCCESS;
}
*doc_id = *i;
#if DEBUG_DATABASE_SANITY
i++;
if (i != end)
INTERNAL_ERROR ("Term %s:%s is not unique as expected.\n",
prefix_name, value);
#endif
return NOTMUCH_PRIVATE_STATUS_SUCCESS;
}
static Xapian::Document
@ -242,26 +289,6 @@ find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
return notmuch->xapian_db->get_document (doc_id);
}
static notmuch_private_status_t
find_unique_document (notmuch_database_t *notmuch,
const char *prefix_name,
const char *value,
Xapian::Document *document,
unsigned int *doc_id)
{
notmuch_private_status_t status;
status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
if (status) {
*document = Xapian::Document ();
return status;
}
*document = find_document_for_doc_id (notmuch, *doc_id);
return NOTMUCH_PRIVATE_STATUS_SUCCESS;
}
notmuch_message_t *
notmuch_database_find_message (notmuch_database_t *notmuch,
const char *message_id)
@ -269,7 +296,8 @@ notmuch_database_find_message (notmuch_database_t *notmuch,
notmuch_private_status_t status;
unsigned int doc_id;
status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
status = _notmuch_database_find_unique_doc_id (notmuch, "id",
message_id, &doc_id);
if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
return NULL;
@ -446,6 +474,7 @@ notmuch_database_create (const char *path)
notmuch = notmuch_database_open (path,
NOTMUCH_DATABASE_MODE_READ_WRITE);
notmuch_database_upgrade (notmuch, NULL, NULL);
DONE:
if (notmuch_path)
@ -454,6 +483,17 @@ notmuch_database_create (const char *path)
return notmuch;
}
notmuch_status_t
_notmuch_database_ensure_writable (notmuch_database_t *notmuch)
{
if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
fprintf (stderr, "Cannot write to a read-only database.\n");
return NOTMUCH_STATUS_READ_ONLY_DATABASE;
}
return NOTMUCH_STATUS_SUCCESS;
}
notmuch_database_t *
notmuch_database_open (const char *path,
notmuch_database_mode_t mode)
@ -462,7 +502,7 @@ notmuch_database_open (const char *path,
char *notmuch_path = NULL, *xapian_path = NULL;
struct stat st;
int err;
unsigned int i;
unsigned int i, version;
if (asprintf (&notmuch_path, "%s/%s", path, ".notmuch") == -1) {
notmuch_path = NULL;
@ -490,13 +530,42 @@ notmuch_database_open (const char *path,
if (notmuch->path[strlen (notmuch->path) - 1] == '/')
notmuch->path[strlen (notmuch->path) - 1] = '\0';
notmuch->needs_upgrade = FALSE;
notmuch->mode = mode;
try {
if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
Xapian::DB_CREATE_OR_OPEN);
version = notmuch_database_get_version (notmuch);
if (version > NOTMUCH_DATABASE_VERSION) {
fprintf (stderr,
"Error: Notmuch database at %s\n"
" has a newer database format version (%u) than supported by this\n"
" version of notmuch (%u). Refusing to open this database in\n"
" read-write mode.\n",
notmuch_path, version, NOTMUCH_DATABASE_VERSION);
notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
notmuch_database_close (notmuch);
notmuch = NULL;
goto DONE;
}
if (version < NOTMUCH_DATABASE_VERSION)
notmuch->needs_upgrade = TRUE;
} else {
notmuch->xapian_db = new Xapian::Database (xapian_path);
version = notmuch_database_get_version (notmuch);
if (version > NOTMUCH_DATABASE_VERSION)
{
fprintf (stderr,
"Warning: Notmuch database at %s\n"
" has a newer database format version (%u) than supported by this\n"
" version of notmuch (%u). Some operations may behave incorrectly,\n"
" (but the database will not be harmed since it is being opened\n"
" in read-only mode).\n",
notmuch_path, version, NOTMUCH_DATABASE_VERSION);
}
}
notmuch->query_parser = new Xapian::QueryParser;
notmuch->term_gen = new Xapian::TermGenerator;
@ -560,110 +629,469 @@ notmuch_database_get_path (notmuch_database_t *notmuch)
return notmuch->path;
}
static notmuch_private_status_t
find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
Xapian::Document *doc, unsigned int *doc_id)
unsigned int
notmuch_database_get_version (notmuch_database_t *notmuch)
{
return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
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;
}
/* We allow the user to use arbitrarily long keys for timestamps,
* (they're for filesystem paths after all, which have no limit we
* know about). But we have a term-length limit. So if we exceed that,
* we'll use the SHA-1 of the user's key as the actual key for
* constructing a database term.
*
* Caution: This function returns a newly allocated string which the
* caller should free() when finished.
*/
static char *
timestamp_db_key (const char *key)
notmuch_bool_t
notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
{
int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
return notmuch->needs_upgrade;
}
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)
{
Xapian::WritableDatabase *db;
struct sigaction action;
struct itimerval timerval;
notmuch_bool_t timer_is_active = FALSE;
unsigned int version;
notmuch_status_t 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);
version = notmuch_database_get_version (notmuch);
if (version >= NOTMUCH_DATABASE_VERSION)
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;
}
/* 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 (version < 1) {
notmuch_query_t *query = notmuch_query_create (notmuch, "");
notmuch_messages_t *messages;
notmuch_message_t *message;
char *filename;
Xapian::TermIterator t, t_end;
total = notmuch_query_count_messages (query);
for (messages = notmuch_query_search_messages (query);
notmuch_messages_has_more (messages);
notmuch_messages_advance (messages))
{
if (do_progress_notify) {
progress_notify (closure, (double) count / total);
do_progress_notify = 0;
}
message = notmuch_messages_get (messages);
filename = _notmuch_message_talloc_copy_data (message);
if (filename && *filename != '\0') {
_notmuch_message_add_filename (message, filename);
_notmuch_message_sync (message);
}
talloc_free (filename);
notmuch_message_destroy (message);
count++;
}
notmuch_query_destroy (query);
/* Also, before version 1 we stored directory timestamps in
* XTIMESTAMP documents instead of the current XDIRECTORY
* documents. So copy those as well. */
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_database_get_directory (notmuch,
term.c_str() + 10);
notmuch_directory_set_mtime (directory, mtime);
notmuch_directory_destroy (directory);
}
}
}
db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
db->flush ();
/* Now that the upgrade is complete we can remove the old data
* and documents that are no longer needed. */
if (version < 1) {
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_has_more (messages);
notmuch_messages_advance (messages))
{
if (do_progress_notify) {
progress_notify (closure, (double) count / total);
do_progress_notify = 0;
}
message = notmuch_messages_get (messages);
filename = _notmuch_message_talloc_copy_data (message);
if (filename && *filename != '\0') {
_notmuch_message_clear_data (message);
_notmuch_message_sync (message);
}
talloc_free (filename);
notmuch_message_destroy (message);
}
notmuch_query_destroy (query);
}
if (version < 1) {
Xapian::TermIterator t, t_end;
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++)
{
if (do_progress_notify) {
progress_notify (closure, (double) count / total);
do_progress_notify = 0;
}
db->delete_document (*p);
}
}
}
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);
}
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 (key);
return notmuch_sha1_of_string (path);
else
return strdup (key);
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;
}
notmuch_status_t
notmuch_database_set_timestamp (notmuch_database_t *notmuch,
const char *key, time_t timestamp)
_notmuch_database_find_directory_id (notmuch_database_t *notmuch,
const char *path,
unsigned int *directory_id)
{
Xapian::Document doc;
Xapian::WritableDatabase *db;
unsigned int doc_id;
notmuch_private_status_t status;
notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
char *db_key = NULL;
notmuch_directory_t *directory;
notmuch_status_t status;
if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
fprintf (stderr, "Attempted to update a read-only database.\n");
return NOTMUCH_STATUS_READONLY_DATABASE;
if (path == NULL) {
*directory_id = 0;
return NOTMUCH_STATUS_SUCCESS;
}
db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
db_key = timestamp_db_key (key);
try {
status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
Xapian::sortable_serialise (timestamp));
if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
char *term = talloc_asprintf (NULL, "%s%s",
_find_prefix ("timestamp"), db_key);
doc.add_term (term);
talloc_free (term);
db->add_document (doc);
} else {
db->replace_document (doc_id, doc);
}
} catch (const Xapian::Error &error) {
fprintf (stderr, "A Xapian exception occurred setting timestamp: %s.\n",
error.get_msg().c_str());
notmuch->exception_reported = TRUE;
ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
directory = _notmuch_directory_create (notmuch, path, &status);
if (status) {
*directory_id = -1;
return status;
}
if (db_key)
free (db_key);
*directory_id = _notmuch_directory_get_document_id (directory);
return ret;
notmuch_directory_destroy (directory);
return NOTMUCH_STATUS_SUCCESS;
}
time_t
notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
const char *
_notmuch_database_get_directory_path (void *ctx,
notmuch_database_t *notmuch,
unsigned int doc_id)
{
Xapian::Document doc;
unsigned int doc_id;
notmuch_private_status_t status;
char *db_key = NULL;
time_t ret = 0;
Xapian::Document document;
db_key = timestamp_db_key (key);
document = find_document_for_doc_id (notmuch, doc_id);
try {
status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
return talloc_strdup (ctx, document.get_data ().c_str ());
}
if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
goto DONE;
/* Given a legal 'filename' for the database, (either relative to
* database path or absolute with initial components identical to
* database path), return a new string (with 'ctx' as the talloc
* owner) suitable for use as a direntry term value.
*
* The necessary directory documents will be created in the database
* as needed.
*/
notmuch_status_t
_notmuch_database_filename_to_direntry (void *ctx,
notmuch_database_t *notmuch,
const char *filename,
char **direntry)
{
const char *relative, *directory, *basename;
Xapian::docid directory_id;
notmuch_status_t status;
ret = Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
} catch (Xapian::Error &error) {
ret = 0;
goto DONE;
relative = _notmuch_database_relative_path (notmuch, filename);
status = _notmuch_database_split_path (ctx, relative,
&directory, &basename);
if (status)
return status;
status = _notmuch_database_find_directory_id (notmuch, directory,
&directory_id);
if (status)
return status;
*direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
return NOTMUCH_STATUS_SUCCESS;
}
/* Given a legal 'path' for the database, return the relative path.
*
* The return value will be a pointer to the originl path contents,
* and will be either the original string (if 'path' was relative) or
* a portion of the string (if path was absolute and begins with the
* database path).
*/
const char *
_notmuch_database_relative_path (notmuch_database_t *notmuch,
const char *path)
{
const char *db_path, *relative;
unsigned int db_path_len;
db_path = notmuch_database_get_path (notmuch);
db_path_len = strlen (db_path);
relative = path;
if (*relative == '/') {
while (*relative == '/' && *(relative+1) == '/')
relative++;
if (strncmp (relative, db_path, db_path_len) == 0)
{
relative += db_path_len;
while (*relative == '/')
relative++;
}
}
DONE:
if (db_key)
free (db_key);
return relative;
}
return ret;
notmuch_directory_t *
notmuch_database_get_directory (notmuch_database_t *notmuch,
const char *path)
{
notmuch_status_t status;
return _notmuch_directory_create (notmuch, path, &status);
}
/* Find the thread ID to which the message with 'message_id' belongs.
@ -903,11 +1331,13 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
if (message_ret)
*message_ret = NULL;
ret = _notmuch_database_ensure_writable (notmuch);
if (ret)
return ret;
message_file = notmuch_message_file_open (filename);
if (message_file == NULL) {
ret = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
if (message_file == NULL)
return NOTMUCH_STATUS_FILE_ERROR;
notmuch_message_file_restrict_headers (message_file,
"date",
@ -988,24 +1418,25 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
goto DONE;
}
_notmuch_message_add_filename (message, filename);
/* Is this a newly created message object? */
if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
_notmuch_message_set_filename (message, filename);
_notmuch_message_add_term (message, "type", "mail");
ret = _notmuch_database_link_message (notmuch, message,
message_file);
if (ret)
goto DONE;
date = notmuch_message_file_get_header (message_file, "date");
_notmuch_message_set_date (message, date);
_notmuch_message_index_file (message, filename);
} else {
ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
goto DONE;
}
ret = _notmuch_database_link_message (notmuch, message, message_file);
if (ret)
goto DONE;
date = notmuch_message_file_get_header (message_file, "date");
_notmuch_message_set_date (message, date);
_notmuch_message_index_file (message, filename);
_notmuch_message_sync (message);
} catch (const Xapian::Error &error) {
fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
@ -1029,6 +1460,60 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
return ret;
}
notmuch_status_t
notmuch_database_remove_message (notmuch_database_t *notmuch,
const char *filename)
{
Xapian::WritableDatabase *db;
void *local = talloc_new (notmuch);
const char *prefix = _find_prefix ("file-direntry");
char *direntry, *term;
Xapian::PostingIterator i, end;
Xapian::Document document;
notmuch_status_t status;
status = _notmuch_database_ensure_writable (notmuch);
if (status)
return status;
db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
status = _notmuch_database_filename_to_direntry (local, notmuch,
filename, &direntry);
if (status)
return status;
term = talloc_asprintf (notmuch, "%s%s", prefix, direntry);
find_doc_ids_for_term (notmuch, term, &i, &end);
for ( ; i != end; i++) {
Xapian::TermIterator j;
document = find_document_for_doc_id (notmuch, *i);
document.remove_term (term);
j = document.termlist_begin ();
j.skip_to (prefix);
/* Was this the last file-direntry in the message? */
if (j == document.termlist_end () ||
strncmp ((*j).c_str (), prefix, strlen (prefix)))
{
db->delete_document (document.get_docid ());
status = NOTMUCH_STATUS_SUCCESS;
} else {
db->replace_document (document.get_docid (), document);
status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
}
}
talloc_free (local);
return status;
}
notmuch_tags_t *
_notmuch_convert_tags (void *ctx, Xapian::TermIterator &i,
Xapian::TermIterator &end)

338
lib/directory.cc Normal file
View file

@ -0,0 +1,338 @@
/* directory.cc - Results of directory-based searches from a notmuch database
*
* Copyright © 2009 Carl Worth
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/ .
*
* Author: Carl Worth <cworth@cworth.org>
*/
#include "notmuch-private.h"
#include "database-private.h"
#include <xapian.h>
struct _notmuch_filenames {
Xapian::TermIterator iterator;
Xapian::TermIterator end;
int prefix_len;
char *filename;
};
/* We end up having to call the destructors explicitly because we had
* to use "placement new" in order to initialize C++ objects within a
* block that we allocated with talloc. So C++ is making talloc
* slightly less simple to use, (we wouldn't need
* talloc_set_destructor at all otherwise).
*/
static int
_notmuch_filenames_destructor (notmuch_filenames_t *filenames)
{
filenames->iterator.~TermIterator ();
filenames->end.~TermIterator ();
return 0;
}
/* Create an iterator to iterate over the basenames of files (or
* directories) that all share a common parent directory.
*
* The code here is general enough to be reused for any case of
* iterating over the non-prefixed portion of terms sharing a common
* prefix.
*/
notmuch_filenames_t *
_notmuch_filenames_create (void *ctx,
notmuch_database_t *notmuch,
const char *prefix)
{
notmuch_filenames_t *filenames;
filenames = talloc (ctx, notmuch_filenames_t);
if (unlikely (filenames == NULL))
return NULL;
new (&filenames->iterator) Xapian::TermIterator ();
new (&filenames->end) Xapian::TermIterator ();
talloc_set_destructor (filenames, _notmuch_filenames_destructor);
filenames->iterator = notmuch->xapian_db->allterms_begin (prefix);
filenames->end = notmuch->xapian_db->allterms_end (prefix);
filenames->prefix_len = strlen (prefix);
filenames->filename = NULL;
return filenames;
}
notmuch_bool_t
notmuch_filenames_has_more (notmuch_filenames_t *filenames)
{
if (filenames == NULL)
return NULL;
return (filenames->iterator != filenames->end);
}
const char *
notmuch_filenames_get (notmuch_filenames_t *filenames)
{
if (filenames == NULL || filenames->iterator == filenames->end)
return NULL;
if (filenames->filename == NULL) {
std::string term = *filenames->iterator;
filenames->filename = talloc_strdup (filenames,
term.c_str () +
filenames->prefix_len);
}
return filenames->filename;
}
void
notmuch_filenames_advance (notmuch_filenames_t *filenames)
{
if (filenames == NULL)
return;
if (filenames->filename) {
talloc_free (filenames->filename);
filenames->filename = NULL;
}
if (filenames->iterator != filenames->end)
filenames->iterator++;
}
void
notmuch_filenames_destroy (notmuch_filenames_t *filenames)
{
if (filenames == NULL)
return;
talloc_free (filenames);
}
struct _notmuch_directory {
notmuch_database_t *notmuch;
Xapian::docid document_id;
Xapian::Document doc;
time_t mtime;
};
/* We end up having to call the destructor explicitly because we had
* to use "placement new" in order to initialize C++ objects within a
* block that we allocated with talloc. So C++ is making talloc
* slightly less simple to use, (we wouldn't need
* talloc_set_destructor at all otherwise).
*/
static int
_notmuch_directory_destructor (notmuch_directory_t *directory)
{
directory->doc.~Document ();
return 0;
}
static notmuch_private_status_t
find_directory_document (notmuch_database_t *notmuch,
const char *db_path,
Xapian::Document *document)
{
notmuch_private_status_t status;
Xapian::docid doc_id;
status = _notmuch_database_find_unique_doc_id (notmuch, "directory",
db_path, &doc_id);
if (status) {
*document = Xapian::Document ();
return status;
}
*document = notmuch->xapian_db->get_document (doc_id);
return NOTMUCH_PRIVATE_STATUS_SUCCESS;
}
notmuch_directory_t *
_notmuch_directory_create (notmuch_database_t *notmuch,
const char *path,
notmuch_status_t *status_ret)
{
Xapian::WritableDatabase *db;
notmuch_directory_t *directory;
notmuch_private_status_t private_status;
const char *db_path;
*status_ret = NOTMUCH_STATUS_SUCCESS;
path = _notmuch_database_relative_path (notmuch, path);
if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
INTERNAL_ERROR ("Failure to ensure database is writable");
db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
directory = talloc (notmuch, notmuch_directory_t);
if (unlikely (directory == NULL))
return NULL;
directory->notmuch = notmuch;
/* "placement new"---not actually allocating memory */
new (&directory->doc) Xapian::Document;
talloc_set_destructor (directory, _notmuch_directory_destructor);
db_path = _notmuch_database_get_directory_db_path (path);
try {
Xapian::TermIterator i, end;
private_status = find_directory_document (notmuch, db_path,
&directory->doc);
directory->document_id = directory->doc.get_docid ();
if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
void *local = talloc_new (directory);
const char *parent, *basename;
Xapian::docid parent_id;
char *term = talloc_asprintf (local, "%s%s",
_find_prefix ("directory"), db_path);
directory->doc.add_term (term, 0);
directory->doc.set_data (path);
_notmuch_database_split_path (local, path, &parent, &basename);
_notmuch_database_find_directory_id (notmuch, parent, &parent_id);
if (basename) {
term = talloc_asprintf (local, "%s%u:%s",
_find_prefix ("directory-direntry"),
parent_id, basename);
directory->doc.add_term (term, 0);
}
directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
Xapian::sortable_serialise (0));
directory->document_id = db->add_document (directory->doc);
talloc_free (local);
}
directory->mtime = Xapian::sortable_unserialise (
directory->doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
} catch (const Xapian::Error &error) {
fprintf (stderr,
"A Xapian exception occurred creating a directory: %s.\n",
error.get_msg().c_str());
notmuch->exception_reported = TRUE;
notmuch_directory_destroy (directory);
*status_ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
return NULL;
}
if (db_path != path)
free ((char *) db_path);
return directory;
}
unsigned int
_notmuch_directory_get_document_id (notmuch_directory_t *directory)
{
return directory->document_id;
}
notmuch_status_t
notmuch_directory_set_mtime (notmuch_directory_t *directory,
time_t mtime)
{
notmuch_database_t *notmuch = directory->notmuch;
Xapian::WritableDatabase *db;
notmuch_status_t status;
status = _notmuch_database_ensure_writable (notmuch);
if (status)
return status;
db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
try {
directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
Xapian::sortable_serialise (mtime));
db->replace_document (directory->document_id, directory->doc);
} catch (const Xapian::Error &error) {
fprintf (stderr,
"A Xapian exception occurred setting directory mtime: %s.\n",
error.get_msg().c_str());
notmuch->exception_reported = TRUE;
return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
}
return NOTMUCH_STATUS_SUCCESS;
}
time_t
notmuch_directory_get_mtime (notmuch_directory_t *directory)
{
return directory->mtime;
}
notmuch_filenames_t *
notmuch_directory_get_child_files (notmuch_directory_t *directory)
{
char *term;
notmuch_filenames_t *child_files;
term = talloc_asprintf (directory, "%s%u:",
_find_prefix ("file-direntry"),
directory->document_id);
child_files = _notmuch_filenames_create (directory,
directory->notmuch, term);
talloc_free (term);
return child_files;
}
notmuch_filenames_t *
notmuch_directory_get_child_directories (notmuch_directory_t *directory)
{
char *term;
notmuch_filenames_t *child_directories;
term = talloc_asprintf (directory, "%s%u:",
_find_prefix ("directory-direntry"),
directory->document_id);
child_directories = _notmuch_filenames_create (directory,
directory->notmuch, term);
talloc_free (term);
return child_directories;
}
void
notmuch_directory_destroy (notmuch_directory_t *directory)
{
talloc_free (directory);
}

View file

@ -31,7 +31,7 @@ _index_address_mailbox (notmuch_message_t *message,
{
InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address);
const char *name, *addr;
void *local = talloc_new (NULL);
void *local = talloc_new (message);
name = internet_address_get_name (address);
addr = internet_address_mailbox_get_addr (mailbox);
@ -123,60 +123,6 @@ skip_re_in_subject (const char *subject)
return s;
}
/* Given a string representing the body of a message, generate terms
* for it, (skipping quoted portions and signatures).
*
* This function is evil in that it modifies the string passed to it,
* (changing some newlines into '\0').
*/
static void
_index_body_text (notmuch_message_t *message, char *body)
{
char *line, *line_end, *next_line;
if (body == NULL)
return;
next_line = body;
while (1) {
line = next_line;
if (*line == '\0')
break;
next_line = strchr (line, '\n');
if (next_line == NULL) {
next_line = line + strlen (line);
}
line_end = next_line - 1;
/* Get to the next non-blank line. */
while (*next_line == '\n')
next_line++;
/* Skip blank lines. */
if (line_end < line)
continue;
/* Skip lines that are quotes. */
if (*line == '>')
continue;
/* Also skip lines introducing a quote on the next line. */
if (*line_end == ':' && *next_line == '>')
continue;
/* Finally, bail as soon as we see a signature. */
/* XXX: Should only do this if "near" the end of the message. */
if (strncmp (line, "-- ", 3) == 0)
break;
*(line_end + 1) = '\0';
_notmuch_message_gen_terms (message, NULL, line);
}
}
/* Callback to generate terms for each mime part of a message. */
static void
_index_mime_part (notmuch_message_t *message,
@ -249,9 +195,11 @@ _index_mime_part (notmuch_message_t *message,
g_byte_array_append (byte_array, (guint8 *) "\0", 1);
body = (char *) g_byte_array_free (byte_array, FALSE);
_index_body_text (message, body);
if (body) {
_notmuch_message_gen_terms (message, NULL, body);
free (body);
free (body);
}
}
notmuch_status_t

View file

@ -174,11 +174,6 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
unsigned int doc_id;
char *term;
if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
*status_ret = NOTMUCH_PRIVATE_STATUS_READONLY_DATABASE;
return NULL;
}
*status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS;
message = notmuch_database_find_message (notmuch, message_id);
@ -192,9 +187,12 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
return NULL;
}
if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
INTERNAL_ERROR ("Failure to ensure database is writable.");
db = static_cast<Xapian::WritableDatabase *> (notmuch->xapian_db);
try {
doc.add_term (term);
doc.add_term (term, 0);
talloc_free (term);
doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
@ -385,20 +383,17 @@ notmuch_message_get_replies (notmuch_message_t *message)
return _notmuch_messages_create (message->replies);
}
/* Set the filename for 'message' to 'filename'.
*
* XXX: We should still figure out if we think it's important to store
* multiple filenames for email messages with identical message IDs.
/* Add an additional 'filename' for 'message'.
*
* This change will not be reflected in the database until the next
* call to _notmuch_message_set_sync. */
void
_notmuch_message_set_filename (notmuch_message_t *message,
notmuch_status_t
_notmuch_message_add_filename (notmuch_message_t *message,
const char *filename)
{
const char *s;
const char *db_path;
unsigned int db_path_len;
notmuch_status_t status;
void *local = talloc_new (message);
char *direntry;
if (message->filename) {
talloc_free (message->filename);
@ -408,41 +403,98 @@ _notmuch_message_set_filename (notmuch_message_t *message,
if (filename == NULL)
INTERNAL_ERROR ("Message filename cannot be NULL.");
s = filename;
status = _notmuch_database_filename_to_direntry (local,
message->notmuch,
filename, &direntry);
if (status)
return status;
db_path = notmuch_database_get_path (message->notmuch);
db_path_len = strlen (db_path);
_notmuch_message_add_term (message, "file-direntry", direntry);
if (*s == '/' && strlen (s) > db_path_len
&& strncmp (s, db_path, db_path_len) == 0)
{
s += db_path_len;
while (*s == '/') s++;
talloc_free (local);
if (!*s)
INTERNAL_ERROR ("Message filename was same as db prefix.");
}
return NOTMUCH_STATUS_SUCCESS;
}
message->doc.set_data (s);
char *
_notmuch_message_talloc_copy_data (notmuch_message_t *message)
{
return talloc_strdup (message, message->doc.get_data ().c_str ());
}
void
_notmuch_message_clear_data (notmuch_message_t *message)
{
message->doc.set_data ("");
}
const char *
notmuch_message_get_filename (notmuch_message_t *message)
{
std::string filename_str;
const char *db_path;
const char *prefix = _find_prefix ("file-direntry");
int prefix_len = strlen (prefix);
Xapian::TermIterator i;
char *direntry, *colon;
const char *db_path, *directory, *basename;
unsigned int directory_id;
void *local = talloc_new (message);
if (message->filename)
return message->filename;
filename_str = message->doc.get_data ();
i = message->doc.termlist_begin ();
i.skip_to (prefix);
if (i != message->doc.termlist_end ())
direntry = talloc_strdup (local, (*i).c_str ());
if (i == message->doc.termlist_end () ||
strncmp (direntry, prefix, prefix_len))
{
/* A message document created by an old version of notmuch
* (prior to rename support) will have the filename in the
* data of the document rather than as a file-direntry term.
*
* It would be nice to do the upgrade of the document directly
* here, but the database is likely open in read-only mode. */
const char *data;
data = message->doc.get_data ().c_str ();
if (data == NULL)
INTERNAL_ERROR ("message with no filename");
message->filename = talloc_strdup (message, data);
return message->filename;
}
direntry += prefix_len;
directory_id = strtol (direntry, &colon, 10);
if (colon == NULL || *colon != ':')
INTERNAL_ERROR ("malformed direntry");
basename = colon + 1;
*colon = '\0';
db_path = notmuch_database_get_path (message->notmuch);
if (filename_str[0] != '/')
message->filename = talloc_asprintf (message, "%s/%s", db_path,
filename_str.c_str ());
directory = _notmuch_database_get_directory_path (local,
message->notmuch,
directory_id);
if (strlen (directory))
message->filename = talloc_asprintf (message, "%s/%s/%s",
db_path, directory, basename);
else
message->filename = talloc_strdup (message, filename_str.c_str ());
message->filename = talloc_asprintf (message, "%s/%s",
db_path, basename);
talloc_free ((void *) directory);
talloc_free (local);
return message->filename;
}
@ -594,7 +646,7 @@ _notmuch_message_add_term (notmuch_message_t *message,
if (strlen (term) > NOTMUCH_TERM_MAX)
return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
message->doc.add_term (term);
message->doc.add_term (term, 0);
talloc_free (term);
@ -667,7 +719,12 @@ _notmuch_message_remove_term (notmuch_message_t *message,
notmuch_status_t
notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
{
notmuch_private_status_t status;
notmuch_private_status_t private_status;
notmuch_status_t status;
status = _notmuch_database_ensure_writable (message->notmuch);
if (status)
return status;
if (tag == NULL)
return NOTMUCH_STATUS_NULL_POINTER;
@ -675,10 +732,10 @@ notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
if (strlen (tag) > NOTMUCH_TAG_MAX)
return NOTMUCH_STATUS_TAG_TOO_LONG;
status = _notmuch_message_add_term (message, "tag", tag);
if (status) {
private_status = _notmuch_message_add_term (message, "tag", tag);
if (private_status) {
INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
status);
private_status);
}
if (! message->frozen)
@ -690,7 +747,12 @@ notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
notmuch_status_t
notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
{
notmuch_private_status_t status;
notmuch_private_status_t private_status;
notmuch_status_t status;
status = _notmuch_database_ensure_writable (message->notmuch);
if (status)
return status;
if (tag == NULL)
return NOTMUCH_STATUS_NULL_POINTER;
@ -698,10 +760,10 @@ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
if (strlen (tag) > NOTMUCH_TAG_MAX)
return NOTMUCH_STATUS_TAG_TOO_LONG;
status = _notmuch_message_remove_term (message, "tag", tag);
if (status) {
private_status = _notmuch_message_remove_term (message, "tag", tag);
if (private_status) {
INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
status);
private_status);
}
if (! message->frozen)
@ -710,39 +772,60 @@ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
return NOTMUCH_STATUS_SUCCESS;
}
void
notmuch_status_t
notmuch_message_remove_all_tags (notmuch_message_t *message)
{
notmuch_private_status_t status;
notmuch_private_status_t private_status;
notmuch_status_t status;
notmuch_tags_t *tags;
const char *tag;
status = _notmuch_database_ensure_writable (message->notmuch);
if (status)
return status;
for (tags = notmuch_message_get_tags (message);
notmuch_tags_has_more (tags);
notmuch_tags_advance (tags))
{
tag = notmuch_tags_get (tags);
status = _notmuch_message_remove_term (message, "tag", tag);
if (status) {
private_status = _notmuch_message_remove_term (message, "tag", tag);
if (private_status) {
INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
status);
private_status);
}
}
if (! message->frozen)
_notmuch_message_sync (message);
return NOTMUCH_STATUS_SUCCESS;
}
void
notmuch_status_t
notmuch_message_freeze (notmuch_message_t *message)
{
notmuch_status_t status;
status = _notmuch_database_ensure_writable (message->notmuch);
if (status)
return status;
message->frozen++;
return NOTMUCH_STATUS_SUCCESS;
}
notmuch_status_t
notmuch_message_thaw (notmuch_message_t *message)
{
notmuch_status_t status;
status = _notmuch_database_ensure_writable (message->notmuch);
if (status)
return status;
if (message->frozen > 0) {
message->frozen--;
if (message->frozen == 0)

View file

@ -112,14 +112,15 @@ typedef enum _notmuch_private_status {
/* First, copy all the public status values. */
NOTMUCH_PRIVATE_STATUS_SUCCESS = NOTMUCH_STATUS_SUCCESS,
NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY,
NOTMUCH_PRIVATE_STATUS_READONLY_DATABASE = NOTMUCH_STATUS_READONLY_DATABASE,
NOTMUCH_PRIVATE_STATUS_READ_ONLY_DATABASE = NOTMUCH_STATUS_READ_ONLY_DATABASE,
NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION = NOTMUCH_STATUS_XAPIAN_EXCEPTION,
NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL = NOTMUCH_STATUS_FILE_NOT_EMAIL,
NOTMUCH_PRIVATE_STATUS_NULL_POINTER = NOTMUCH_STATUS_NULL_POINTER,
NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG = NOTMUCH_STATUS_TAG_TOO_LONG,
NOTMUCH_PRIVATE_STATUS_UNBALANCED_FREEZE_THAW = NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
/* Then add our own private values. */
NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG,
NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG = NOTMUCH_STATUS_LAST_STATUS,
NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND,
NOTMUCH_PRIVATE_STATUS_LAST_STATUS
@ -150,6 +151,54 @@ typedef enum _notmuch_private_status {
const char *
_find_prefix (const char *name);
notmuch_status_t
_notmuch_database_ensure_writable (notmuch_database_t *notmuch);
const char *
_notmuch_database_relative_path (notmuch_database_t *notmuch,
const char *path);
notmuch_status_t
_notmuch_database_split_path (void *ctx,
const char *path,
const char **directory,
const char **basename);
const char *
_notmuch_database_get_directory_db_path (const char *path);
notmuch_private_status_t
_notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
const char *prefix_name,
const char *value,
unsigned int *doc_id);
notmuch_status_t
_notmuch_database_find_directory_id (notmuch_database_t *database,
const char *path,
unsigned int *directory_id);
const char *
_notmuch_database_get_directory_path (void *ctx,
notmuch_database_t *notmuch,
unsigned int doc_id);
notmuch_status_t
_notmuch_database_filename_to_direntry (void *ctx,
notmuch_database_t *notmuch,
const char *filename,
char **direntry);
/* directory.cc */
notmuch_directory_t *
_notmuch_directory_create (notmuch_database_t *notmuch,
const char *path,
notmuch_status_t *status_ret);
unsigned int
_notmuch_directory_get_document_id (notmuch_directory_t *directory);
/* thread.cc */
notmuch_thread_t *
@ -190,7 +239,10 @@ _notmuch_message_gen_terms (notmuch_message_t *message,
const char *text);
void
_notmuch_message_set_filename (notmuch_message_t *message,
_notmuch_message_upgrade_filename_storage (notmuch_message_t *message);
notmuch_status_t
_notmuch_message_add_filename (notmuch_message_t *message,
const char *filename);
void
@ -206,6 +258,22 @@ _notmuch_message_sync (notmuch_message_t *message);
void
_notmuch_message_close (notmuch_message_t *message);
/* Get a copy of the data in this message document.
*
* Caller should talloc_free the result when done.
*
* This function is intended to support database upgrade and really
* shouldn't be used otherwise. */
char *
_notmuch_message_talloc_copy_data (notmuch_message_t *message);
/* Clear the data in this message document.
*
* This function is intended to support database upgrade and really
* shouldn't be used otherwise. */
void
_notmuch_message_clear_data (notmuch_message_t *message);
/* index.cc */
notmuch_status_t

View file

@ -57,6 +57,9 @@ typedef int notmuch_bool_t;
* value. Instead we should map to things like DATABASE_LOCKED or
* whatever.
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: An attempt was made to write to
* a database opened in read-only mode.
*
* NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred
*
* NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to read or
@ -86,7 +89,7 @@ typedef int notmuch_bool_t;
typedef enum _notmuch_status {
NOTMUCH_STATUS_SUCCESS = 0,
NOTMUCH_STATUS_OUT_OF_MEMORY,
NOTMUCH_STATUS_READONLY_DATABASE,
NOTMUCH_STATUS_READ_ONLY_DATABASE,
NOTMUCH_STATUS_XAPIAN_EXCEPTION,
NOTMUCH_STATUS_FILE_ERROR,
NOTMUCH_STATUS_FILE_NOT_EMAIL,
@ -114,6 +117,8 @@ typedef struct _notmuch_thread notmuch_thread_t;
typedef struct _notmuch_messages notmuch_messages_t;
typedef struct _notmuch_message notmuch_message_t;
typedef struct _notmuch_tags notmuch_tags_t;
typedef struct _notmuch_directory notmuch_directory_t;
typedef struct _notmuch_filenames notmuch_filenames_t;
/* Create a new, empty notmuch database located at 'path'.
*
@ -178,56 +183,46 @@ notmuch_database_close (notmuch_database_t *database);
const char *
notmuch_database_get_path (notmuch_database_t *database);
/* Store a timestamp within the database.
/* Return the database format version of the given database. */
unsigned int
notmuch_database_get_version (notmuch_database_t *database);
/* Does this database need to be upgraded before writing to it?
*
* The Notmuch database will not interpret this key nor the timestamp
* values at all. It will merely store them together and return the
* timestamp when notmuch_database_get_timestamp is called with the
* same value for 'key'.
* If this function returns TRUE then no functions that modify the
* database (notmuch_database_add_message, notmuch_message_add_tag,
* notmuch_directory_set_mtime, etc.) will work unless the function
* notmuch_database_upgrade is called successfully first. */
notmuch_bool_t
notmuch_database_needs_upgrade (notmuch_database_t *database);
/* Upgrade the current database.
*
* The intention is for the caller to use the timestamp to allow
* efficient identification of new messages to be added to the
* database. The recommended usage is as follows:
* 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.
*
* o Read the mtime of a directory from the filesystem
*
* o Call add_message for all mail files in the directory
*
* o Call notmuch_database_set_timestamp with the path of the
* directory as 'key' and the originally read mtime as 'value'.
*
* Then, when wanting to check for updates to the directory in the
* future, the client can call notmuch_database_get_timestamp and know
* that it only needs to add files if the mtime of the directory and
* files are newer than the stored timestamp.
*
* Note: The notmuch_database_get_timestamp function does not allow
* the caller to distinguish a timestamp of 0 from a non-existent
* timestamp. So don't store a timestamp of 0 unless you are
* comfortable with that.
*
* Return value:
*
* NOTMUCH_STATUS_SUCCESS: Timestamp successfully stored in database.
*
* NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception
* occurred. Timestamp not stored.
* 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 'progress' as a floating-point value in
* the range of [0.0 .. 1.0] indicating the progress made so far in
* the upgrade process.
*/
notmuch_status_t
notmuch_database_set_timestamp (notmuch_database_t *database,
const char *key, time_t timestamp);
notmuch_database_upgrade (notmuch_database_t *database,
void (*progress_notify) (void *closure,
double progress),
void *closure);
/* Retrieve a timestamp from the database.
/* Retrieve a directory object from the database for 'path'.
*
* Returns the timestamp value previously stored by calling
* notmuch_database_set_timestamp with the same value for 'key'.
*
* Returns 0 if no timestamp is stored for 'key' or if any error
* occurred querying the database.
* Here, 'path' should be a path relative to the path of 'database'
* (see notmuch_database_get_path), or else should be an absolute path
* with initial components that match the path of 'database'.
*/
time_t
notmuch_database_get_timestamp (notmuch_database_t *database,
const char *key);
notmuch_directory_t *
notmuch_database_get_directory (notmuch_database_t *database,
const char *path);
/* Add a new message to the given notmuch database.
*
@ -252,8 +247,8 @@ notmuch_database_get_timestamp (notmuch_database_t *database,
* NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
*
* NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
* ID as another message already in the database. Nothing added
* to the database.
* ID as another message already in the database. The new filename
* was successfully added to the message in the database.
*
* NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the
* file, (such as permission denied, or file not found,
@ -261,12 +256,40 @@ notmuch_database_get_timestamp (notmuch_database_t *database,
*
* NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look
* like an email message. Nothing added to the database.
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so no message can be added.
*/
notmuch_status_t
notmuch_database_add_message (notmuch_database_t *database,
const char *filename,
notmuch_message_t **message);
/* Remove a message from the given notmuch database.
*
* Note that only this particular filename association is removed from
* the database. If the same message (as determined by the message ID)
* is still available via other filenames, then the message will
* persist in the database for those filenames. When the last filename
* is removed for a particular message, the database content for that
* message will be entirely removed.
*
* Return value:
*
* NOTMUCH_STATUS_SUCCESS: The last filename was removed and the
* message was removed from the database.
*
* NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: This filename was removed but
* the message persists in the database with at least one other
* filename.
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so no message can be removed.
*/
notmuch_status_t
notmuch_database_remove_message (notmuch_database_t *database,
const char *filename);
/* Find a message with the given message_id.
*
* If the database contains a message with the given message_id, then
@ -698,14 +721,20 @@ notmuch_message_get_thread_id (notmuch_message_t *message);
notmuch_messages_t *
notmuch_message_get_replies (notmuch_message_t *message);
/* Get the filename for the email corresponding to 'message'.
/* Get a filename for the email corresponding to 'message'.
*
* The returned filename is an absolute filename, (the initial
* component will match notmuch_database_get_path() ).
*
* The returned string belongs to the message so should not be
* modified or freed by the caller (nor should it be referenced after
* the message is destroyed). */
* the message is destroyed).
*
* Note: If this message corresponds to multiple files in the mail
* store, (that is, multiple files contain identical message IDs),
* this function will arbitrarily return a single one of those
* filenames.
*/
const char *
notmuch_message_get_filename (notmuch_message_t *message);
@ -793,6 +822,9 @@ notmuch_message_get_tags (notmuch_message_t *message);
*
* NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
* (exceeds NOTMUCH_TAG_MAX)
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so message cannot be modified.
*/
notmuch_status_t
notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
@ -807,6 +839,9 @@ notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
*
* NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
* (exceeds NOTMUCH_TAG_MAX)
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so message cannot be modified.
*/
notmuch_status_t
notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
@ -815,8 +850,11 @@ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
*
* See notmuch_message_freeze for an example showing how to safely
* replace tag values.
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so message cannot be modified.
*/
void
notmuch_status_t
notmuch_message_remove_all_tags (notmuch_message_t *message);
/* Freeze the current state of 'message' within the database.
@ -851,8 +889,15 @@ notmuch_message_remove_all_tags (notmuch_message_t *message);
* somehow getting interrupted. This could result in the message being
* left with no tags if the interruption happened after
* notmuch_message_remove_all_tags but before notmuch_message_add_tag.
*
* Return value:
*
* NOTMUCH_STATUS_SUCCESS: Message successfully frozen.
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so message cannot be modified.
*/
void
notmuch_status_t
notmuch_message_freeze (notmuch_message_t *message);
/* Thaw the current 'message', synchronizing any changes that may have
@ -929,6 +974,118 @@ notmuch_tags_advance (notmuch_tags_t *tags);
void
notmuch_tags_destroy (notmuch_tags_t *tags);
/* Store an mtime within the database for 'directory'.
*
* The 'directory' should be an object retrieved from the database
* with notmuch_database_get_directory for a particular path.
*
* The intention is for the caller to use the mtime to allow efficient
* identification of new messages to be added to the database. The
* recommended usage is as follows:
*
* o Read the mtime of a directory from the filesystem
*
* o Call add_message for all mail files in the directory
*
* o Call notmuch_directory_set_mtime with the mtime read from the
* filesystem.
*
* Then, when wanting to check for updates to the directory in the
* future, the client can call notmuch_directory_get_mtime and know
* that it only needs to add files if the mtime of the directory and
* files are newer than the stored timestamp.
*
* Note: The notmuch_directory_get_mtime function does not allow the
* caller to distinguish a timestamp of 0 from a non-existent
* timestamp. So don't store a timestamp of 0 unless you are
* comfortable with that.
*
* Return value:
*
* NOTMUCH_STATUS_SUCCESS: mtime successfully stored in database.
*
* NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception
* occurred, mtime not stored.
*
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so directory mtime cannot be modified.
*/
notmuch_status_t
notmuch_directory_set_mtime (notmuch_directory_t *directory,
time_t mtime);
/* Get the mtime of a directory, (as previously stored with
* notmuch_directory_set_mtime).
*
* Returns 0 if no mtime has previously been stored for this
* directory.*/
time_t
notmuch_directory_get_mtime (notmuch_directory_t *directory);
/* Get a notmuch_filenames_t iterator listing all the filenames of
* messages in the database within the given directory.
*
* The returned filenames will be the basename-entries only (not
* complete paths). */
notmuch_filenames_t *
notmuch_directory_get_child_files (notmuch_directory_t *directory);
/* Get a notmuch_filenams_t iterator listing all the filenames of
* sub-directories in the database within the given directory.
*
* The returned filenames will be the basename-entries only (not
* complete paths). */
notmuch_filenames_t *
notmuch_directory_get_child_directories (notmuch_directory_t *directory);
/* Destroy a notmuch_directory_t object. */
void
notmuch_directory_destroy (notmuch_directory_t *directory);
/* Does the given notmuch_filenames_t object contain any more
* filenames.
*
* When this function returns TRUE, notmuch_filenames_get will return
* a valid string. Whereas when this function returns FALSE,
* notmuch_filenames_get will return NULL.
*
* It is acceptable to pass NULL for 'filenames', in which case this
* function will always return FALSE.
*/
notmuch_bool_t
notmuch_filenames_has_more (notmuch_filenames_t *filenames);
/* Get the current filename from 'filenames' as a string.
*
* Note: The returned string belongs to 'filenames' and has a lifetime
* identical to it (and the directory to which it ultimately belongs).
*
* It is acceptable to pass NULL for 'filenames', in which case this
* function will always return NULL.
*/
const char *
notmuch_filenames_get (notmuch_filenames_t *filenames);
/* Advance the 'filenames' iterator to the next filename.
*
* It is acceptable to pass NULL for 'filenames', in which case this
* function will do nothing.
*/
void
notmuch_filenames_advance (notmuch_filenames_t *filenames);
/* Destroy a notmuch_filenames_t object.
*
* It's not strictly necessary to call this function. All memory from
* the notmuch_filenames_t object will be reclaimed when the
* containing directory object is destroyed.
*
* It is acceptable to pass NULL for 'filenames', in which case this
* function will do nothing.
*/
void
notmuch_filenames_destroy (notmuch_filenames_t *filenames);
NOTMUCH_END_DECLS
#endif

View file

@ -134,6 +134,8 @@ notmuch_query_search_messages (notmuch_query_t *query)
mail_query, string_query);
}
enquire.set_weighting_scheme (Xapian::BoolWeight());
switch (query->sort) {
case NOTMUCH_SORT_OLDEST_FIRST:
enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE);

View file

@ -70,22 +70,6 @@
#define STRNCMP_LITERAL(var, literal) \
strncmp ((var), (literal), sizeof (literal) - 1)
typedef void (*add_files_callback_t) (notmuch_message_t *message);
typedef struct {
int ignore_read_only_directories;
int saw_read_only_directory;
int output_is_a_tty;
int verbose;
int total_files;
int processed_files;
int added_messages;
struct timeval tv_start;
add_files_callback_t callback;
} add_files_state_t;
static inline void
chomp_newline (char *str)
{
@ -132,10 +116,6 @@ notmuch_time_print_formatted_seconds (double seconds);
double
notmuch_time_elapsed (struct timeval start, struct timeval end);
notmuch_status_t
add_files (notmuch_database_t *notmuch, const char *path,
add_files_state_t *state);
char *
query_string_from_args (void *ctx, int argc, char *argv[]);

View file

@ -22,6 +22,29 @@
#include <unistd.h>
typedef struct _filename_node {
char *filename;
struct _filename_node *next;
} _filename_node_t;
typedef struct _filename_list {
_filename_node_t *head;
_filename_node_t **tail;
} _filename_list_t;
typedef struct {
int output_is_a_tty;
int verbose;
int total_files;
int processed_files;
int added_messages;
struct timeval tv_start;
_filename_list_t *removed_files;
_filename_list_t *removed_directories;
} add_files_state_t;
static volatile sig_atomic_t do_add_files_print_progress = 0;
static void
@ -42,6 +65,34 @@ handle_sigint (unused (int sig))
interrupted = 1;
}
static _filename_list_t *
_filename_list_create (const void *ctx)
{
_filename_list_t *list;
list = talloc (ctx, _filename_list_t);
if (list == NULL)
return NULL;
list->head = NULL;
list->tail = &list->head;
return list;
}
static void
_filename_list_add (_filename_list_t *list,
const char *filename)
{
_filename_node_t *node = talloc (list, _filename_node_t);
node->filename = talloc_strdup (list, filename);
node->next = NULL;
*(list->tail) = node;
list->tail = &node->next;
}
static void
tag_inbox_and_unread (notmuch_message_t *message)
{
@ -77,11 +128,18 @@ add_files_print_progress (add_files_state_t *state)
fflush (stdout);
}
static int ino_cmp(const struct dirent **a, const struct dirent **b)
static int
dirent_sort_inode (const struct dirent **a, const struct dirent **b)
{
return ((*a)->d_ino < (*b)->d_ino) ? -1 : 1;
}
static int
dirent_sort_strcmp_name (const struct dirent **a, const struct dirent **b)
{
return strcmp ((*a)->d_name, (*b)->d_name);
}
/* Test if the directory looks like a Maildir directory.
*
* Search through the array of directory entries to see if we can find all
@ -90,12 +148,14 @@ static int ino_cmp(const struct dirent **a, const struct dirent **b)
* Return 1 if the directory looks like a Maildir and 0 otherwise.
*/
static int
is_maildir (struct dirent **entries, int count)
_entries_resemble_maildir (struct dirent **entries, int count)
{
int i, found = 0;
for (i = 0; i < count; i++) {
if (entries[i]->d_type != DT_DIR) continue;
if (entries[i]->d_type != DT_DIR)
continue;
if (strcmp(entries[i]->d_name, "new") == 0 ||
strcmp(entries[i]->d_name, "cur") == 0 ||
strcmp(entries[i]->d_name, "tmp") == 0)
@ -111,186 +171,301 @@ is_maildir (struct dirent **entries, int count)
/* Examine 'path' recursively as follows:
*
* o Ask the filesystem for the mtime of 'path' (path_mtime)
* o Ask the filesystem for the mtime of 'path' (fs_mtime)
* o Ask the database for its timestamp of 'path' (db_mtime)
*
* o Ask the database for its timestamp of 'path' (path_dbtime)
* o Ask the filesystem for files and directories within 'path'
* (via scandir and stored in fs_entries)
* o Ask the database for files and directories within 'path'
* (db_files and db_subdirs)
*
* o If 'path_mtime' > 'path_dbtime'
* o Pass 1: For each directory in fs_entries, recursively call into
* this same function.
*
* o For each regular file in 'path' with mtime newer than the
* 'path_dbtime' call add_message to add the file to the
* database.
* o Pass 2: If 'fs_mtime' > 'db_mtime', then walk fs_entries
* simultaneously with db_files and db_subdirs. Look for one of
* three interesting cases:
*
* o For each sub-directory of path, recursively call into this
* same function.
* 1. Regular file in fs_entries and not in db_files
* This is a new file to add_message into the database.
*
* o Tell the database to update its time of 'path' to 'path_mtime'
* 2. Filename in db_files not in fs_entries.
* This is a file that has been removed from the mail store.
*
* The 'struct stat *st' must point to a structure that has already
* been initialized for 'path' by calling stat().
* 3. Directory in db_subdirs not in fs_entries
* This is a directory that has been removed from the mail store.
*
* Note that the addition of a directory is not interesting here,
* since that will have been taken care of in pass 1. Also, we
* don't immediately act on file/directory removal since we must
* ensure that in the case of a rename that the new filename is
* added before the old filename is removed, (so that no
* information is lost from the database).
*
* o Tell the database to update its time of 'path' to 'fs_mtime'
*/
static notmuch_status_t
add_files_recursive (notmuch_database_t *notmuch,
const char *path,
struct stat *st,
add_files_state_t *state)
{
DIR *dir = NULL;
struct dirent *entry = NULL;
char *next = NULL;
time_t path_mtime, path_dbtime;
time_t fs_mtime, db_mtime;
notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
notmuch_message_t *message = NULL;
struct dirent **namelist = NULL;
int num_entries;
struct dirent **fs_entries = NULL;
int i, num_fs_entries;
notmuch_directory_t *directory;
notmuch_filenames_t *db_files = NULL;
notmuch_filenames_t *db_subdirs = NULL;
struct stat st;
notmuch_bool_t is_maildir, new_directory;
/* If we're told to, we bail out on encountering a read-only
* directory, (with this being a clear clue from the user to
* Notmuch that new mail won't be arriving there and we need not
* look. */
if (state->ignore_read_only_directories &&
(st->st_mode & S_IWUSR) == 0)
{
state->saw_read_only_directory = TRUE;
goto DONE;
if (stat (path, &st)) {
fprintf (stderr, "Error reading directory %s: %s\n",
path, strerror (errno));
return NOTMUCH_STATUS_FILE_ERROR;
}
path_mtime = st->st_mtime;
/* This is not an error since we may have recursed based on a
* symlink to a regular file, not a directory, and we don't know
* that until this stat. */
if (! S_ISDIR (st.st_mode))
return NOTMUCH_STATUS_SUCCESS;
path_dbtime = notmuch_database_get_timestamp (notmuch, path);
num_entries = scandir (path, &namelist, 0, ino_cmp);
fs_mtime = st.st_mtime;
if (num_entries == -1) {
directory = notmuch_database_get_directory (notmuch, path);
db_mtime = notmuch_directory_get_mtime (directory);
if (db_mtime == 0) {
new_directory = TRUE;
db_files = NULL;
db_subdirs = NULL;
} else {
new_directory = FALSE;
db_files = notmuch_directory_get_child_files (directory);
db_subdirs = notmuch_directory_get_child_directories (directory);
}
/* If the database knows about this directory, then we sort based
* on strcmp to match the database sorting. Otherwise, we can do
* inode-based sorting for faster filesystem operation. */
num_fs_entries = scandir (path, &fs_entries, 0,
new_directory ?
dirent_sort_inode : dirent_sort_strcmp_name);
if (num_fs_entries == -1) {
fprintf (stderr, "Error opening directory %s: %s\n",
path, strerror (errno));
ret = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
int i=0;
/* Pass 1: Recurse into all sub-directories. */
is_maildir = _entries_resemble_maildir (fs_entries, num_fs_entries);
while (!interrupted) {
if (i == num_entries)
for (i = 0; i < num_fs_entries; i++) {
if (interrupted)
break;
entry= namelist[i++];
entry = fs_entries[i];
/* If this directory hasn't been modified since the last
* add_files, then we only need to look further for
* sub-directories. */
if (path_mtime <= path_dbtime && entry->d_type == DT_REG)
if (entry->d_type != DT_DIR && entry->d_type != DT_LNK)
continue;
/* Ignore special directories to avoid infinite recursion.
* Also ignore the .notmuch directory.
* Also ignore the .notmuch directory and any "tmp" directory
* that appears within a maildir.
*/
/* XXX: Eventually we'll want more sophistication to let the
* user specify files to be ignored. */
if (strcmp (entry->d_name, ".") == 0 ||
strcmp (entry->d_name, "..") == 0 ||
(entry->d_type == DT_DIR &&
(strcmp (entry->d_name, "tmp") == 0) &&
is_maildir (namelist, num_entries)) ||
(is_maildir && strcmp (entry->d_name, "tmp") == 0) ||
strcmp (entry->d_name, ".notmuch") ==0)
{
continue;
}
next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
status = add_files_recursive (notmuch, next, state);
if (status && ret == NOTMUCH_STATUS_SUCCESS)
ret = status;
talloc_free (next);
next = NULL;
}
if (stat (next, st)) {
int err = errno;
/* If this directory hasn't been modified since the last
* "notmuch new", then we can skip the second pass entirely. */
if (fs_mtime <= db_mtime)
goto DONE;
switch (err) {
case ENOENT:
/* The file was removed between scandir and now... */
case EPERM:
case EACCES:
/* We can't read this file so don't add it to the cache. */
continue;
/* Pass 2: Scan for new files, removed files, and removed directories. */
for (i = 0; i < num_fs_entries; i++)
{
if (interrupted)
break;
entry = fs_entries[i];
/* Check if we've walked past any names in db_files or
* db_subdirs. If so, these have been deleted. */
while (notmuch_filenames_has_more (db_files) &&
strcmp (notmuch_filenames_get (db_files), entry->d_name) < 0)
{
char *absolute = talloc_asprintf (state->removed_files,
"%s/%s", path,
notmuch_filenames_get (db_files));
_filename_list_add (state->removed_files, absolute);
notmuch_filenames_advance (db_files);
}
while (notmuch_filenames_has_more (db_subdirs) &&
strcmp (notmuch_filenames_get (db_subdirs), entry->d_name) <= 0)
{
const char *filename = notmuch_filenames_get (db_subdirs);
if (strcmp (filename, entry->d_name) < 0)
{
char *absolute = talloc_asprintf (state->removed_directories,
"%s/%s", path, filename);
_filename_list_add (state->removed_directories, absolute);
}
fprintf (stderr, "Error reading %s: %s\n",
next, strerror (errno));
ret = NOTMUCH_STATUS_FILE_ERROR;
notmuch_filenames_advance (db_subdirs);
}
/* If we're looking at a symlink, we only want to add it if it
* links to a regular file, (and not to a directory, say). */
if (entry->d_type == DT_LNK) {
int err;
next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
err = stat (next, &st);
talloc_free (next);
next = NULL;
/* Don't emit an error for a link pointing nowhere, since
* the directory-traversal pass will have already done
* that. */
if (err)
continue;
if (! S_ISREG (st.st_mode))
continue;
} else if (entry->d_type != DT_REG) {
continue;
}
/* Don't add a file that we've added before. */
if (notmuch_filenames_has_more (db_files) &&
strcmp (notmuch_filenames_get (db_files), entry->d_name) == 0)
{
notmuch_filenames_advance (db_files);
continue;
}
/* We're now looking at a regular file that doesn't yet exist
* in the database, so add it. */
next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
state->processed_files++;
if (state->verbose) {
if (state->output_is_a_tty)
printf("\r\033[K");
printf ("%i/%i: %s",
state->processed_files,
state->total_files,
next);
putchar((state->output_is_a_tty) ? '\r' : '\n');
fflush (stdout);
}
status = notmuch_database_add_message (notmuch, next, &message);
switch (status) {
/* success */
case NOTMUCH_STATUS_SUCCESS:
state->added_messages++;
tag_inbox_and_unread (message);
break;
/* Non-fatal issues (go on to next file) */
case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
/* Stay silent on this one. */
break;
case NOTMUCH_STATUS_FILE_NOT_EMAIL:
fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
next);
break;
/* Fatal issues. Don't process anymore. */
case NOTMUCH_STATUS_READ_ONLY_DATABASE:
case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
case NOTMUCH_STATUS_OUT_OF_MEMORY:
fprintf (stderr, "Error: %s. Halting processing.\n",
notmuch_status_to_string (status));
ret = status;
goto DONE;
default:
case NOTMUCH_STATUS_FILE_ERROR:
case NOTMUCH_STATUS_NULL_POINTER:
case NOTMUCH_STATUS_TAG_TOO_LONG:
case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
case NOTMUCH_STATUS_LAST_STATUS:
INTERNAL_ERROR ("add_message returned unexpected value: %d", status);
goto DONE;
}
if (S_ISREG (st->st_mode)) {
/* If the file hasn't been modified since the last
* add_files, then we need not look at it. */
if (path_dbtime == 0 || st->st_mtime > path_dbtime) {
state->processed_files++;
if (message) {
notmuch_message_destroy (message);
message = NULL;
}
if (state->verbose) {
if (state->output_is_a_tty)
printf("\r\033[K");
printf ("%i/%i: %s",
state->processed_files,
state->total_files,
next);
putchar((state->output_is_a_tty) ? '\r' : '\n');
fflush (stdout);
}
status = notmuch_database_add_message (notmuch, next, &message);
switch (status) {
/* success */
case NOTMUCH_STATUS_SUCCESS:
state->added_messages++;
tag_inbox_and_unread (message);
break;
/* Non-fatal issues (go on to next file) */
case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
/* Stay silent on this one. */
break;
case NOTMUCH_STATUS_FILE_NOT_EMAIL:
fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
next);
break;
/* Fatal issues. Don't process anymore. */
case NOTMUCH_STATUS_READONLY_DATABASE:
case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
case NOTMUCH_STATUS_OUT_OF_MEMORY:
fprintf (stderr, "Error: %s. Halting processing.\n",
notmuch_status_to_string (status));
ret = status;
goto DONE;
default:
case NOTMUCH_STATUS_FILE_ERROR:
case NOTMUCH_STATUS_NULL_POINTER:
case NOTMUCH_STATUS_TAG_TOO_LONG:
case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
case NOTMUCH_STATUS_LAST_STATUS:
INTERNAL_ERROR ("add_message returned unexpected value: %d", status);
goto DONE;
}
if (message) {
notmuch_message_destroy (message);
message = NULL;
}
if (do_add_files_print_progress) {
do_add_files_print_progress = 0;
add_files_print_progress (state);
}
}
} else if (S_ISDIR (st->st_mode)) {
status = add_files_recursive (notmuch, next, st, state);
if (status && ret == NOTMUCH_STATUS_SUCCESS)
ret = status;
if (do_add_files_print_progress) {
do_add_files_print_progress = 0;
add_files_print_progress (state);
}
talloc_free (next);
next = NULL;
}
status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
if (status && ret == NOTMUCH_STATUS_SUCCESS)
ret = status;
/* Now that we've walked the whole filesystem list, anything left
* over in the database lists has been deleted. */
while (notmuch_filenames_has_more (db_files))
{
char *absolute = talloc_asprintf (state->removed_files,
"%s/%s", path,
notmuch_filenames_get (db_files));
_filename_list_add (state->removed_files, absolute);
notmuch_filenames_advance (db_files);
}
while (notmuch_filenames_has_more (db_subdirs))
{
char *absolute = talloc_asprintf (state->removed_directories,
"%s/%s", path,
notmuch_filenames_get (db_subdirs));
_filename_list_add (state->removed_directories, absolute);
notmuch_filenames_advance (db_subdirs);
}
if (! interrupted) {
status = notmuch_directory_set_mtime (directory, fs_mtime);
if (status && ret == NOTMUCH_STATUS_SUCCESS)
ret = status;
}
DONE:
if (next)
@ -299,8 +474,14 @@ add_files_recursive (notmuch_database_t *notmuch,
free (entry);
if (dir)
closedir (dir);
if (namelist)
free (namelist);
if (fs_entries)
free (fs_entries);
if (db_subdirs)
notmuch_filenames_destroy (db_subdirs);
if (db_files)
notmuch_filenames_destroy (db_files);
if (directory)
notmuch_directory_destroy (directory);
return ret;
}
@ -308,27 +489,16 @@ add_files_recursive (notmuch_database_t *notmuch,
/* This is the top-level entry point for add_files. It does a couple
* of error checks, sets up the progress-printing timer and then calls
* into the recursive function. */
notmuch_status_t
static notmuch_status_t
add_files (notmuch_database_t *notmuch,
const char *path,
add_files_state_t *state)
{
struct stat st;
notmuch_status_t status;
struct sigaction action;
struct itimerval timerval;
notmuch_bool_t timer_is_active = FALSE;
if (stat (path, &st)) {
fprintf (stderr, "Error reading directory %s: %s\n",
path, strerror (errno));
return NOTMUCH_STATUS_FILE_ERROR;
}
if (! S_ISDIR (st.st_mode)) {
fprintf (stderr, "Error: %s is not a directory.\n", path);
return NOTMUCH_STATUS_FILE_ERROR;
}
struct stat st;
if (state->output_is_a_tty && ! debugger_is_active () && ! state->verbose) {
/* Setup our handler for SIGALRM */
@ -348,7 +518,18 @@ add_files (notmuch_database_t *notmuch,
timer_is_active = TRUE;
}
status = add_files_recursive (notmuch, path, &st, state);
if (stat (path, &st)) {
fprintf (stderr, "Error reading directory %s: %s\n",
path, strerror (errno));
return NOTMUCH_STATUS_FILE_ERROR;
}
if (! S_ISDIR (st.st_mode)) {
fprintf (stderr, "Error: %s is not a directory.\n", path);
return NOTMUCH_STATUS_FILE_ERROR;
}
status = add_files_recursive (notmuch, path, state);
if (timer_is_active) {
/* Now stop the timer. */
@ -378,21 +559,21 @@ count_files (const char *path, int *count)
struct dirent *entry = NULL;
char *next;
struct stat st;
struct dirent **namelist = NULL;
int n_entries = scandir (path, &namelist, 0, ino_cmp);
struct dirent **fs_entries = NULL;
int num_fs_entries = scandir (path, &fs_entries, 0, dirent_sort_inode);
int i = 0;
if (n_entries == -1) {
if (num_fs_entries == -1) {
fprintf (stderr, "Warning: failed to open directory %s: %s\n",
path, strerror (errno));
goto DONE;
}
while (!interrupted) {
if (i == n_entries)
if (i == num_fs_entries)
break;
entry= namelist[i++];
entry = fs_entries[i++];
/* Ignore special directories to avoid infinite recursion.
* Also ignore the .notmuch directory.
@ -431,8 +612,77 @@ count_files (const char *path, int *count)
DONE:
if (entry)
free (entry);
if (namelist)
free (namelist);
if (fs_entries)
free (fs_entries);
}
static void
upgrade_print_progress (void *closure,
double progress)
{
add_files_state_t *state = closure;
printf ("Upgrading database: %.2f%% complete", progress * 100.0);
if (progress > 0) {
struct timeval tv_now;
double elapsed, time_remaining;
gettimeofday (&tv_now, NULL);
elapsed = notmuch_time_elapsed (state->tv_start, tv_now);
time_remaining = (elapsed / progress) * (1.0 - progress);
printf (" (");
notmuch_time_print_formatted_seconds (time_remaining);
printf (" remaining)");
}
printf (". \r");
fflush (stdout);
}
/* Recursively remove all filenames from the database referring to
* 'path' (or to any of its children). */
static void
_remove_directory (void *ctx,
notmuch_database_t *notmuch,
const char *path,
int *renamed_files,
int *removed_files)
{
notmuch_directory_t *directory;
notmuch_filenames_t *files, *subdirs;
notmuch_status_t status;
char *absolute;
directory = notmuch_database_get_directory (notmuch, path);
for (files = notmuch_directory_get_child_files (directory);
notmuch_filenames_has_more (files);
notmuch_filenames_advance (files))
{
absolute = talloc_asprintf (ctx, "%s/%s", path,
notmuch_filenames_get (files));
status = notmuch_database_remove_message (notmuch, absolute);
if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
*renamed_files = *renamed_files + 1;
else
*removed_files = *removed_files + 1;
talloc_free (absolute);
}
for (subdirs = notmuch_directory_get_child_directories (directory);
notmuch_filenames_has_more (subdirs);
notmuch_filenames_advance (subdirs))
{
absolute = talloc_asprintf (ctx, "%s/%s", path,
notmuch_filenames_get (subdirs));
_remove_directory (ctx, notmuch, absolute, renamed_files, removed_files);
talloc_free (absolute);
}
notmuch_directory_destroy (directory);
}
int
@ -448,6 +698,9 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
const char *db_path;
char *dot_notmuch_path;
struct sigaction action;
_filename_node_t *f;
int renamed_files, removed_files;
notmuch_status_t status;
int i;
add_files_state.verbose = 0;
@ -462,13 +715,6 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
}
}
/* Setup our handler for SIGINT */
memset (&action, 0, sizeof (struct sigaction));
action.sa_handler = handle_sigint;
sigemptyset (&action.sa_mask);
action.sa_flags = SA_RESTART;
sigaction (SIGINT, &action, NULL);
config = notmuch_config_open (ctx, NULL, NULL);
if (config == NULL)
return 1;
@ -485,33 +731,73 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
if (interrupted)
return 1;
printf ("Found %d total files. \n", count);
printf ("Found %d total files (that's not much mail).\n", count);
notmuch = notmuch_database_create (db_path);
add_files_state.ignore_read_only_directories = FALSE;
add_files_state.total_files = count;
} else {
notmuch = notmuch_database_open (db_path,
NOTMUCH_DATABASE_MODE_READ_WRITE);
add_files_state.ignore_read_only_directories = TRUE;
if (notmuch == NULL)
return 1;
if (notmuch_database_needs_upgrade (notmuch)) {
printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n");
gettimeofday (&add_files_state.tv_start, NULL);
notmuch_database_upgrade (notmuch, upgrade_print_progress,
&add_files_state);
printf ("Your notmuch database has now been upgraded to database format version %u.\n",
notmuch_database_get_version (notmuch));
}
add_files_state.total_files = 0;
}
if (notmuch == NULL)
return 1;
/* Setup our handler for SIGINT. We do this after having
* potentially done a database upgrade we this interrupt handler
* won't support. */
memset (&action, 0, sizeof (struct sigaction));
action.sa_handler = handle_sigint;
sigemptyset (&action.sa_mask);
action.sa_flags = SA_RESTART;
sigaction (SIGINT, &action, NULL);
talloc_free (dot_notmuch_path);
dot_notmuch_path = NULL;
add_files_state.saw_read_only_directory = FALSE;
add_files_state.processed_files = 0;
add_files_state.added_messages = 0;
gettimeofday (&add_files_state.tv_start, NULL);
add_files_state.removed_files = _filename_list_create (ctx);
add_files_state.removed_directories = _filename_list_create (ctx);
ret = add_files (notmuch, db_path, &add_files_state);
removed_files = 0;
renamed_files = 0;
for (f = add_files_state.removed_files->head; f; f = f->next) {
status = notmuch_database_remove_message (notmuch, f->filename);
if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
renamed_files++;
else
removed_files++;
}
for (f = add_files_state.removed_directories->head; f; f = f->next) {
_remove_directory (ctx, notmuch, f->filename,
&renamed_files, &removed_files);
}
talloc_free (add_files_state.removed_files);
talloc_free (add_files_state.removed_directories);
gettimeofday (&tv_now, NULL);
elapsed = notmuch_time_elapsed (add_files_state.tv_start,
tv_now);
if (add_files_state.processed_files) {
printf ("Processed %d %s in ", add_files_state.processed_files,
add_files_state.processed_files == 1 ?
@ -524,22 +810,30 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
printf (". \n");
}
}
if (add_files_state.added_messages) {
printf ("Added %d new %s to the database (not much, really).\n",
printf ("Added %d new %s to the database.",
add_files_state.added_messages,
add_files_state.added_messages == 1 ?
"message" : "messages");
} else {
printf ("No new mail---and that's not much.\n");
printf ("No new mail.");
}
if (elapsed > 1 && ! add_files_state.saw_read_only_directory) {
printf ("\nTip: If you have any sub-directories that are archives (that is,\n"
"they will never receive new mail), marking these directories as\n"
"read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
"much more efficient (it won't even look in those directories).\n");
if (removed_files) {
printf (" Removed %d %s.",
removed_files,
removed_files == 1 ? "message" : "messages");
}
if (renamed_files) {
printf (" Detected %d file %s.",
renamed_files,
renamed_files == 1 ? "rename" : "renames");
}
printf ("\n");
if (ret) {
printf ("\nNote: At least one error was encountered: %s\n",
notmuch_status_to_string (ret));

View file

@ -109,14 +109,6 @@ whenever new mail is delivered and you wish to incorporate it into the
database. These subsequent runs will be much quicker than the initial
run.
Note:
.B notmuch new
runs (other than the first run) will skip any read-only directories,
so you can use that to mark directories that will not receive any new
mail (and make
.B notmuch new
even faster).
Invoking
.B notmuch
with no command argument will run

View file

@ -145,11 +145,6 @@ command_t commands[] = {
"\t\t\tVerbose operation. Shows paths of message files as\n"
"\t\t\tthey are being indexed.\n"
"\n"
"\t\tNote: \"notmuch new\" runs (other than the first run) will\n"
"\t\tskip any read-only directories, so you can use that to mark\n"
"\t\tdirectories that will not receive any new mail (and make\n"
"\t\t\"notmuch new\" even faster).\n"
"\n"
"\t\tInvoking notmuch with no command argument will run new if\n"
"\t\tthe setup command has previously been completed, but new has\n"
"\t\tnot previously been run." },