X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=cnotmuch%2Fdatabase.py;h=44fd31548f7583801827ac9470211332c83f2848;hp=e540bb3ce46dcb2670dcad31b5c11373145ee089;hb=7390c869c71cd7d65a99b3dacb17ca659aa09c8b;hpb=e80daac2931d99da10bf2d1d106fb35455455470 diff --git a/cnotmuch/database.py b/cnotmuch/database.py index e540bb3c..44fd3154 100644 --- a/cnotmuch/database.py +++ b/cnotmuch/database.py @@ -1,5 +1,5 @@ import os -from ctypes import c_int, c_char_p, c_void_p, c_uint, byref +from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum from cnotmuch.thread import Threads from cnotmuch.message import Messages @@ -20,7 +20,11 @@ class Database(object): MODE = Enum(['READ_ONLY','READ_WRITE']) """Constants: Mode in which to open the database""" - """notmuch_database_get_path (notmuch_database_t *database)""" + """notmuch_database_get_directory""" + _get_directory = nmlib.notmuch_database_get_directory + _get_directory.restype = c_void_p + + """notmuch_database_get_path""" _get_path = nmlib.notmuch_database_get_path _get_path.restype = c_char_p @@ -28,24 +32,28 @@ class Database(object): _get_version = nmlib.notmuch_database_get_version _get_version.restype = c_uint - """notmuch_database_open (const char *path, notmuch_database_mode_t mode)""" + """notmuch_database_open""" _open = nmlib.notmuch_database_open _open.restype = c_void_p - """ notmuch_database_find_message """ + """notmuch_database_upgrade""" + _upgrade = nmlib.notmuch_database_upgrade + _upgrade.argtypes = [c_void_p, c_void_p, c_void_p] + + """ notmuch_database_find_message""" _find_message = nmlib.notmuch_database_find_message _find_message.restype = c_void_p - """notmuch_database_get_all_tags (notmuch_database_t *database)""" + """notmuch_database_get_all_tags""" _get_all_tags = nmlib.notmuch_database_get_all_tags _get_all_tags.restype = c_void_p - """ notmuch_database_create(const char *path):""" + """notmuch_database_create""" _create = nmlib.notmuch_database_create _create.restype = c_void_p def __init__(self, path=None, create=False, mode= 0): - """If *path* is *None*, we will try to read a users notmuch + """If *path* is `None`, we will try to read a users notmuch configuration and use his configured database. The location of the configuration file can be specified through the environment variable *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`. @@ -89,8 +97,8 @@ class Database(object): This function is used by __init__() and usually does not need to be called directly. It wraps the underlying *notmuch_database_create* function and creates a new notmuch - database at *path*. It will always return a database in - :attr:`MODE`.READ_WRITE mode as creating an empty database for + database at *path*. It will always return a database in :attr:`MODE` + .READ_WRITE mode as creating an empty database for reading only does not make a great deal of sense. :param path: A directory in which we should create the database. @@ -134,7 +142,7 @@ class Database(object): def get_path(self): """Returns the file path of an open database - Wraps notmuch_database_get_path""" + Wraps *notmuch_database_get_path*.""" # Raise a NotmuchError if not initialized self._verify_initialized_db() @@ -155,10 +163,10 @@ class Database(object): def needs_upgrade(self): """Does this database need to be upgraded before writing to it? - If this function returns True then no functions that modify the - database (:meth:`add_message`, :meth:`add_tag`, - :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade` - is called successfully first. + If this function returns `True` then no functions that modify the + database (:meth:`add_message`, + :meth:`Message.add_tag`, :meth:`Directory.set_mtime`, + etc.) will work unless :meth:`upgrade` is called successfully first. :returns: `True` or `False` :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if @@ -167,7 +175,71 @@ class Database(object): # Raise a NotmuchError if not initialized self._verify_initialized_db() - return notmuch_database_needs_upgrade(self.db) + return notmuch_database_needs_upgrade(self._db) + + def upgrade(self): + """Upgrades 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. + + NOT IMPLEMENTED: 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. + + :TODO: catch exceptions, document return values and etc... + """ + # Raise a NotmuchError if not initialized + self._verify_initialized_db() + + status = Database._upgrade (self._db, None, None) + #TODO: catch exceptions, document return values and etc + return status + + def get_directory(self, path): + """Returns a :class:`Directory` of path, + (creating it if it does not exist(?)) + + .. warning:: This call needs a writeable database in + Database.MODE.READ_WRITE mode. The underlying library will exit the + program if this method is used on a read-only database! + + :param path: A str containing the path relative to the path of database + (see :meth:`get_path`), or else should be an absolute path + with initial components that match the path of 'database'. + :returns: :class:`Directory` or raises an exception. + :exception: :exc:`NotmuchError` + + STATUS.NOT_INITIALIZED + If the database was not intitialized. + + STATUS.FILE_ERROR + If path is not relative database or absolute with initial + components same as database. + + """ + # Raise a NotmuchError if not initialized + self._verify_initialized_db() + + # sanity checking if path is valid, and make path absolute + if path[0] == os.sep: + # we got an absolute path + if not path.startswith(self.get_path()): + # but its initial components are not equal to the db path + raise NotmuchError(STATUS.FILE_ERROR, + message="Database().get_directory() called with a wrong absolute path.") + abs_dirpath = path + else: + #we got a relative path, make it absolute + abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path)) + + dir_p = Database._get_directory(self._db, path); + + # return the Directory, init it with the absolute path + return Directory(abs_dirpath, dir_p, self) def add_message(self, filename): """Adds a new message to the database @@ -236,7 +308,7 @@ class Database(object): is removed for a particular message, the database content for that message will be entirely removed. - :returns: A STATUS.* value with the following meaning: + :returns: A STATUS value with the following meaning: STATUS.SUCCESS The last filename was removed and the message was removed @@ -521,3 +593,230 @@ class Query(object): """Close and free the Query""" if self._query is not None: nmlib.notmuch_query_destroy (self._query) + + +#------------------------------------------------------------------------------ +class Directory(object): + """Represents a directory entry in the notmuch directory + + Modifying attributes of this object will modify the + database, not the real directory attributes. + + The Directory object is usually derived from another object + e.g. via :meth:`Database.get_directory`, and will automatically be + become invalid whenever that parent is deleted. You should + therefore initialized this object handing it a reference to the + parent, preventing the parent from automatically being garbage + collected. + """ + + """notmuch_directory_get_mtime""" + _get_mtime = nmlib.notmuch_directory_get_mtime + _get_mtime.restype = c_long + + """notmuch_directory_set_mtime""" + _set_mtime = nmlib.notmuch_directory_set_mtime + _set_mtime.argtypes = [c_char_p, c_long] + + """notmuch_directory_get_child_files""" + _get_child_files = nmlib.notmuch_directory_get_child_files + _get_child_files.restype = c_void_p + + """notmuch_directory_get_child_directories""" + _get_child_directories = nmlib.notmuch_directory_get_child_directories + _get_child_directories.restype = c_void_p + + def _verify_dir_initialized(self): + """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None""" + if self._dir_p is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + def __init__(self, path, dir_p, parent): + """ + :param path: The absolute path of the directory object. + :param dir_p: The pointer to an internal notmuch_directory_t object. + :param parent: The object this Directory is derived from + (usually a :class:`Database`). We do not directly use + this, but store a reference to it as long as + this Directory object lives. This keeps the + parent object alive. + """ + self._path = path + self._dir_p = dir_p + self._parent = parent + + + def set_mtime (self, mtime): + """Sets the mtime value of this directory in the database + + 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: + + * Read the mtime of a directory from the filesystem + + * Call :meth:`Database.add_message` for all mail files in + the directory + + * 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 :meth:`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:: :meth:`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. + + :param mtime: A (time_t) timestamp + :returns: Nothing on success, raising an exception on failure. + :exception: :exc:`NotmuchError`: + + STATUS.XAPIAN_EXCEPTION + A Xapian exception occurred, mtime not stored. + STATUS.READ_ONLY_DATABASE + Database was opened in read-only mode so directory + mtime cannot be modified. + STATUS.NOT_INITIALIZED + The directory has not been initialized + """ + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None + self._verify_dir_initialized() + + #TODO: make sure, we convert the mtime parameter to a 'c_long' + status = Directory._set_mtime(self._dir_p, mtime) + + #return on success + if status == STATUS.SUCCESS: + return + #fail with Exception otherwise + raise NotmuchError(status) + + def get_mtime (self): + """Gets the mtime value of this directory in the database + + Retrieves a previously stored mtime for this directory. + + :param mtime: A (time_t) timestamp + :returns: Nothing on success, raising an exception on failure. + :exception: :exc:`NotmuchError`: + + STATUS.NOT_INITIALIZED + The directory has not been initialized + """ + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None + self._verify_dir_initialized() + + return Directory._get_mtime (self._dir_p) + + # Make mtime attribute a property of Directory() + mtime = property(get_mtime, set_mtime, doc="""Property that allows getting + and setting of the Directory *mtime* (read-write) + + See :meth:`get_mtime` and :meth:`set_mtime` for usage and + possible exceptions.""") + + def get_child_files(self): + """Gets a Filenames 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. + """ + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None + self._verify_dir_initialized() + + files_p = Directory._get_child_files(self._dir_p) + return Filenames(files_p, self) + + def get_child_directories(self): + """Gets a :class:`Filenames` 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. + """ + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None + self._verify_dir_initialized() + + files_p = Directory._get_child_directories(self._dir_p) + return Filenames(files_p, self) + + @property + def path(self): + """Returns the absolute path of this Directory (read-only)""" + return self._path + + def __repr__(self): + """Object representation""" + return "" % self._path + + def __del__(self): + """Close and free the Directory""" + if self._dir_p is not None: + nmlib.notmuch_directory_destroy(self._dir_p) + +#------------------------------------------------------------------------------ +class Filenames(object): + """An iterator over File- or Directory names that are stored in the database + """ + + #notmuch_filenames_get + _get = nmlib.notmuch_filenames_get + _get.restype = c_char_p + + def __init__(self, files_p, parent): + """ + :param files_p: The pointer to an internal notmuch_filenames_t object. + :param parent: The object this Directory is derived from + (usually a Directory()). We do not directly use + this, but store a reference to it as long as + this Directory object lives. This keeps the + parent object alive. + """ + self._files_p = files_p + self._parent = parent + + def __iter__(self): + """ Make Filenames an iterator """ + return self + + def next(self): + if self._files_p is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + if not nmlib.notmuch_filenames_valid(self._files_p): + self._files_p = None + raise StopIteration + + file = Filenames._get (self._files_p) + nmlib.notmuch_filenames_move_to_next(self._files_p) + return file + + def __len__(self): + """len(:class:`Filenames`) returns the number of contained files + + .. note:: As this iterates over the files, we will not be able to + iterate over them again! So this will fail:: + + #THIS FAILS + files = Database().get_directory('').get_child_files() + if len(files) > 0: #this 'exhausts' msgs + # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!! + for file in files: print file + """ + if self._files_p is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + i=0 + while nmlib.notmuch_filenames_valid(self._files_p): + nmlib.notmuch_filenames_move_to_next(self._files_p) + i += 1 + self._files_p = None + return i + + def __del__(self): + """Close and free Filenames""" + if self._files_p is not None: + nmlib.notmuch_filenames_destroy(self._files_p)