[PATCH/RFC 4/7] Various new methods invoked by the update-process

Hubert Pineault hpineault at riseup.net
Sun Mar 3 08:57:00 GMT 2019


The main update process is not in this commit and all alteration of
existing methods will be subtmitted in distinct commits. The methods
found in this commit are of minor importance. Here is the list and
explanation.

folder.IMAP.IMAPFolder.getimapname():
  Simply returns 'self.imap', which is a proprety containing the
  original imap response before decoding in utf8.

repository.MailDir.moveroot(newroot, update_mbnames):
  Move the local maildir root folder to another location in the
  filesystem and set self.root accordingly. The method is used to move
  a maildir root folder to a backup of temporary folder.

repository.MailDir.getmessagefilename(uid):
  return filename...

repository.Base.BaseRepository.getmetadata():
  Add the new property self.metadatadir. Return a tuple of
  self.metadatadir, self.mapdir and self.uiddir. Initialize properties
  if it's needed. This method is implemented in order to be called by
  account.movemetadata).  Since mapdir and uiddir initilization are
  done in this method, BaseRepository.__init__() will be modified in
  another commit.

repository.LocalStatus.LocalStatusRepository.getmetadata():
  Same idea as BaseRepository.getmetadata(), but we need a specific
  method for status repos.

ui.UIBase: there's 10 new methods for printing output.

account.movemetadatadir(newmetadatadir, update_mbnames):
  Move the metadata root folder to another location in the filesystem
  and call account.getmetadata() to set account proprieties
  acconrdingly . The method is used to move a metadata to a backup of
  temporary folder.  Note that this method implies some code
  alteration in accout.init() and account.getmetadata()

  update_mbnames: I still need to figure out how mbnames work. Not
  Sure if this will be usefull. Some help please?

account.get_folderlist():
  Initialize remote and local repos.

  To make the link between local folders and remote folders, old and
  new config files need to have their own remoterepos. Although, it
  shouldn't have to send two request to imap server. I haven't found a
  way to skip the imap list cmd for the new config file while keeping
  it from pointing to the old config file remote repo. One way to
  could be to write a whole new logic of initialysing repositories.
  This methods would need to get imap result and pass it to the new
  remoterepos.

Signed-off-by: Hubert Pineault <hpineault at riseup.net>
---
 offlineimap/accounts.py               | 123 +++++++++++++++++++++++++++++++---
 offlineimap/folder/IMAP.py            |   3 +
 offlineimap/folder/Maildir.py         |   4 +-
 offlineimap/repository/Base.py        |  18 +++++
 offlineimap/repository/LocalStatus.py |  24 +++++++
 offlineimap/repository/Maildir.py     |  24 +++++++
 offlineimap/ui/UIBase.py              |  85 +++++++++++++++++++++++
 7 files changed, 272 insertions(+), 9 deletions(-)

diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py
index 4aca7c4..0ae8b0d 100644
--- a/offlineimap/accounts.py
+++ b/offlineimap/accounts.py
@@ -257,7 +257,8 @@ class SyncableAccount(Account):
                 pass    # Failed to delete for some reason.
 
     def syncrunner(self):
-        """The target for both single and multi-threaded modes."""
+        """The target for three mode:
+        single mode, multi-threaded mode and update-conf mode."""
 
         self.ui.registerthread(self)
         try:
@@ -279,8 +280,13 @@ class SyncableAccount(Account):
         while looping:
             self.ui.acct(self)
             try:
+                # if not self.config.getboolean('general', 'is-new-config-source'):
                 self.__lock()
-                self.__sync()
+                if self.config.getboolean('general', 'update-conf'):
+                    self.get_folderlist()
+                else:
+                    self.__sync()
+
             except (KeyboardInterrupt, SystemExit):
                 raise
             except OfflineImapError as e:
@@ -292,17 +298,26 @@ class SyncableAccount(Account):
                         raise
                 self.ui.error(e, exc_info()[2])
             except Exception as e:
-                self.ui.error(e, exc_info()[2], msg=
-                    "While attempting to sync account '%s'"% self)
+                if self.config.newconfigsource:
+                    self.ui.error(e, exc_info()[2], msg=
+                        "While attempting to update account '%s'"% self)
+                else:
+                    self.ui.error(e, exc_info()[2], msg=
+                        "While attempting to sync account '%s'"% self)
             else:
                 # After success sync, reset the looping counter to 3.
                 if self.refreshperiod:
                     looping = 3
             finally:
                 self.ui.acctdone(self)
-                self._unlock()
-                if looping and self._sleeper() >= 2:
+                # If in update-conf mode, keep original account locked
+                # and don't run sleeper
+                if self.config.getboolean('general', 'update-conf'):
                     looping = 0
+                else:
+                    self._unlock()
+                    if looping and self._sleeper() >= 2:
+                        looping = 0
 
     def get_local_folder(self, remotefolder):
         """Return the corresponding local folder for a given remotefolder."""
@@ -312,8 +327,8 @@ class SyncableAccount(Account):
             replace(self.remoterepos.getsep(), self.localrepos.getsep()))
 
 
-    # The syncrunner will loop on this method. This means it is called more than
-    # once during the run.
+    # The syncrunner will loop on this method (unless in update-conf mode).
+    # This means it is called more than once during the run.
     def __sync(self):
         """Synchronize the account once, then return.
 
@@ -452,6 +467,98 @@ class SyncableAccount(Account):
         except Exception as e:
             self.ui.error(e, exc_info()[2], msg="Calling hook")
 
+    ########
+    ### Config update methodes:
+    #
+    def movemetadatadir(self, newmetadatadir, update_mbnames=False):
+        '''Move metadatadir to a new path.
+
+        This function is called by the config updating process.
+
+        TODO: update mbnames according to new root'''
+
+        self.ui.movemetadatadir(self.name, newmetadatadir)
+        if update_mbnames:
+            self.ui.warn("Updating mbname for moving metadatadir NOT YET IMPLEMENTED")
+            # raise
+            #XXX TODO
+        if self.dryrun:
+            return
+
+        try:
+            from shutil import move
+            # Get list of metadata objects to move to backup folder
+            movables = []
+            movables.append(self.getaccountmeta())
+            if self._lockfilepath:
+                movables.append(self._lockfilepath)
+            movables.append(self.localrepos.metadatadir)
+            movables.append(self.remoterepos.metadatadir)
+
+            # Set path for new meta
+            self.config.set('general', 'metadata', newmetadatadir)
+            self.metadatadir = self.config.getmetadatadir()
+
+            # Move metadata
+            for m in movables:
+                basename = os.path.basename(m)
+                move(m, os.path.join(newmetadatadir, basename))
+
+            self._lockfilepath = os.path.join(
+                newmetadatadir,
+                os.path.basename(self._lockfilepath))
+            self.metadatadir = newmetadatadir
+            self.localrepos.metadatadir = None
+            self.localrepos.getmetadata()
+            self.remoterepos.metadatadir = None
+            self.remoterepos.getmetadata()
+            self.statusrepos.metadatadir = None
+            self.statusrepos.getmetadata()
+
+        except OSError or OfflineImapError as e:
+            self.ui.error(e, exc_info()[2],
+                          "Moving metadatadir to '%s'"%
+                          (newmetadatadir))
+            raise
+
+    # The syncrunner will loop on this method. This means it is called more than
+    # once during the run.
+    def get_folderlist(self):
+        """Get the account folderlist once, then return.
+
+        Assumes that `self.remoterepos`, `self.localrepos`, and
+        `self.statusrepos` has already been populated, so it should only
+        be called from the :meth:`syncrunner` function.
+
+        Based on __sync()"""
+
+        hook = self.getconf('presynchook', '')  # Is it important?
+        self.callhook(hook)
+
+        if self.utf_8_support and self.remoterepos.getdecodefoldernames():
+            raise OfflineImapError("Configuration mismatch in account " +
+                        "'%s'. "% self.getname() +
+                        "\nAccount setting 'utf8foldernames' and repository " +
+                        "setting 'decodefoldernames'\nmay not be used at the " +
+                        "same time. This account has not been updated.\n" +
+                        "Please check the configuration and documentation.",
+                    OfflineImapError.ERROR.REPO)
+
+        try:
+            self.remoterepos.getfolders()
+            self.remoterepos.dropconnections()
+            self.localrepos.getfolders()
+
+        #XXX: This section should be checked and
+        # rewritten (copy-pasted from callhook to handle try)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception as e:
+            self.ui.error(e, exc_info()[2], msg="Calling hook")
+
+        hook = self.getconf('postupdateconfhook', '')  # Is this right?
+        self.callhook(hook)
+
 
 #XXX: This function should likely be refactored. This should not be passed the
 # account instance.
diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py
index ead4396..3a716e4 100644
--- a/offlineimap/folder/IMAP.py
+++ b/offlineimap/folder/IMAP.py
@@ -96,6 +96,9 @@ class IMAPFolder(BaseFolder):
             name = imaputil.utf8_IMAP(name)
         return name
 
+    def getimapname(self):
+        return self.imap_name
+
     # Interface from BaseFolder
     def suggeststhreads(self):
         singlethreadperfolder_default = False
diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py
index f061bcd..795f4d3 100644
--- a/offlineimap/folder/Maildir.py
+++ b/offlineimap/folder/Maildir.py
@@ -295,7 +295,6 @@ class MaildirFolder(BaseFolder):
             uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
         return uniq_name.replace(os.path.sep, self.sep_subst)
 
-
     def save_to_tmp_file(self, filename, content):
         """Saves given content to the named temporary file in the
         'tmp' subdirectory of $CWD.
@@ -416,6 +415,9 @@ class MaildirFolder(BaseFolder):
         self.ui.debug('maildir', 'savemessage: returning uid %d' % uid)
         return uid
 
+    def getmessagefilename(self, uid):
+        return self.messagelist[uid]['filename']
+
     # Interface from BaseFolder
     def getmessageflags(self, uid):
         return self.messagelist[uid]['flags']
diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py
index 2ad7708..60f926c 100644
--- a/offlineimap/repository/Base.py
+++ b/offlineimap/repository/Base.py
@@ -63,6 +63,24 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
             self.foldersort = self.localeval.eval(
                 self.getconf('foldersort'), {'re': re})
 
+    def getmetadata(self):
+        if self.metadatadir and self.mapdir and self.uiddir:
+            return self.metadatadir, self.mapdir, self.uiddir
+        else:
+            self.metadatadir = os.path.join(self.config.getmetadatadir(),
+                                            'Repository-' + self.name)
+            if not os.path.exists(self.metadatadir):
+                os.mkdir(self.metadatadir, 0o700)
+            self.mapdir = os.path.join(self.metadatadir, 'UIDMapping')
+            if not os.path.exists(self.mapdir):
+                os.mkdir(self.mapdir, 0o700)
+            # FIXME: self.uiddir variable name is lying about itself.
+            # (Still true with this new method???)
+            self.uiddir = os.path.join(self.metadatadir, 'FolderValidity')
+            if not os.path.exists(self.uiddir):
+                os.mkdir(self.uiddir, 0o700)
+            return self.metadatadir, self.mapdir, self.uiddir
+
     def restore_atime(self):
         """Sets folders' atime back to their values after a sync
 
diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py
index a651fd2..4ad2315 100644
--- a/offlineimap/repository/LocalStatus.py
+++ b/offlineimap/repository/LocalStatus.py
@@ -53,6 +53,30 @@ class LocalStatusRepository(BaseRepository):
         # self._folders is a dict of name:LocalStatusFolders().
         self._folders = {}
 
+    def getmetadata(self):
+        # class and root for all backends.
+        self.backends = {}
+        self.backends['sqlite'] = {
+            'class': LocalStatusSQLiteFolder,
+            'root': os.path.join(self.account.getaccountmeta(), 'LocalStatus-sqlite')
+        }
+        self.backends['plain'] = {
+            'class': LocalStatusFolder,
+            'root': os.path.join(self.account.getaccountmeta(), 'LocalStatus')
+        }
+
+        if self.account.getconf('status_backend', None) is not None:
+            raise OfflineImapError(
+                "the 'status_backend' configuration option is not supported"
+                " anymore; please, remove this configuration option.",
+                OfflineImapError.ERROR.REPO
+            )
+        # Set class and root for sqlite.
+        self.setup_backend('sqlite')
+
+        if not os.path.exists(self.root):
+            os.mkdir(self.root, 0o700)
+
     def _instanciatefolder(self, foldername):
         return self.LocalStatusFolderClass(foldername, self) # Instanciate.
 
diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py
index 0db728c..5c2685d 100644
--- a/offlineimap/repository/Maildir.py
+++ b/offlineimap/repository/Maildir.py
@@ -130,6 +130,30 @@ class MaildirRepository(BaseRepository):
                 else:
                     raise
 
+    def moveroot(self, newroot, update_mbnames = False):
+        '''Move local repository root folder to a new path.
+
+        This function is called by the config updating process.
+
+        TODO: update mbnames according to new root'''
+
+        self.ui.moveroot(self.name, newroot)
+        if self.account.dryrun:
+            return
+        try:
+            os.renames(self.root, newroot)
+            self.root = newroot
+        except OSError or OfflineImapError as e:
+            self.ui.error(e, exc_info()[2],
+                          "Moving root from '%s' to '%s'"%
+                          (self.root, newroot))
+            raise
+
+        if update_mbnames:
+            self.ui.warn("Updating mbname for moving root folder NOT YET IMPLEMENTED")
+            # raise 
+            #XXX TODO
+
     def deletefolder(self, foldername):
         self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s"% foldername)
 
diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py
index 731d6f1..f46eb35 100644
--- a/offlineimap/ui/UIBase.py
+++ b/offlineimap/ui/UIBase.py
@@ -344,6 +344,91 @@ class UIBase(object):
             self.debug('', "Copying folder structure from %s to %s" %\
                            (src_repo, dst_repo))
 
+    ############################## Config updating
+
+    def updateconf(self, oldconf, newconf):
+        """Output that we start the updating process."""
+
+        self.acct_startimes['updateconf'] = time.time()
+        self.logger.info("*** Updating config file %s from source %s"%
+            (oldconf, newconf))
+
+    def updateconfdone(self):
+        """Output that we finished the updating process."""
+
+        sec = time.time() - self.acct_startimes['updateconf']
+        del self.acct_startimes['updateconf']
+        self.logger.info("*** Finished updating config. Total time elapsed: %d:%02d"%
+            (sec // 60, sec % 60))
+
+    def updateconfacct(self, account):
+        """Output that we start updating folder structure for account."""
+
+        self.acct_startimes['updateconfacct'] = time.time()
+        self.logger.info("*** Updating folder structure for account '%s'"%
+            account)
+
+    def updateconfacctdone(self, account):
+        """Output that we finished updating folder structure for account."""
+
+        sec = time.time() - self.acct_startimes['updateconfacct']
+        del self.acct_startimes['updateconfacct']
+        self.logger.info("*** Finished account '%s' in %d:%02d"%
+            (account, sec // 60, sec % 60))
+
+    def getfoldercontent(self, oldfolder, newfolder, move=False):
+        """Log 'Moving folder content...'."""
+
+        self.acct_startimes['getfoldercontent'] = time.time()
+        prefix = "Moving " if move else "Copying "
+        prefix = "[DRYRUN] " + prefix if self.dryrun else prefix
+        self.info(('{0}folder content from "{1}" to "{2}"'.format(
+            prefix, oldfolder, newfolder)))
+
+    def nbmessagestosend(self, folder_basename, nbofmessages, movecontent):
+        """ Output the number of messages (files) this operation will move."""
+
+        indent = '{:>4}'.format('')
+        prefix = "[DRYRUN] " if self.dryrun else ""
+        action = 'Moving ' if movecontent else 'Copying '
+        if nbofmessages == 0:
+            self.info("{0}Folder empty.".format(indent))
+        else:
+            self.info(("{0}{1}{2}{3} messages to folder '{4}'".format(
+                indent, prefix, action, nbofmessages, folder_basename)))
+
+    def sendingmessage(self, uid, num, num_to_send, src, destfolder, movecontent):
+        """Output a log line stating which message we copy."""
+
+        indent = '{:>4}'.format('')
+        prefix = "[DRYRUN] " if self.dryrun else ""
+        action = 'Moving ' if movecontent else 'Copying '
+        self.logger.debug("%s%s%s message UID %s (%d/%d) %s:%s -> %s:%s"% (
+                indent, prefix, action, uid, num, num_to_send, src.repository, src,
+                destfolder.repository, destfolder))
+
+    def getfoldercontentdone(self):
+        """Output that we finished operation on folder content for old folder."""
+
+        indent = '{:>4}'.format('')
+        sec = time.time() - self.acct_startimes['getfoldercontent']
+        del self.acct_startimes['getfoldercontent']
+        self.logger.info("%s Operation completed in %d:%02d"%
+                         (indent, sec // 60, sec % 60))
+
+    def moveroot(self, account, newroot):
+        """Output that we are moving the root folder for the account."""
+
+        prefix = "[DRYRUN] " if self.dryrun else ""
+        self.logger.info("*** {0}Moving root folder for account '{1}' to {2}".format(
+            prefix, account, newroot))
+
+    def movemetadatadir(self, account, newmetadatadir):
+        """Output that we are moving the metadatadir for the account."""
+
+        self.logger.info("*** Moving metadata directory for account '%s' to %s"%
+                         (account, newmetadatadir))
+
     ############################## Folder syncing
     def makefolder(self, repo, foldername):
         """Called when a folder is created."""
-- 
2.11.0




More information about the OfflineIMAP-project mailing list