[PATCH v2] learn --mbnames-prune CLI option

Nicolas Sebrecht nicolas.s-dev at laposte.net
Sun Jun 26 16:39:46 UTC 2016


This is usefull to remove dangling entries for removed accounts or if mbnames is
not enabled anymore.

Signed-off-by: Nicolas Sebrecht <nicolas.s-dev at laposte.net>
---

Forgot to update the man page...

-- interdiff --
diff --git a/docs/offlineimap.txt b/docs/offlineimap.txt
index 989bbae..fd2dd9d 100644
--- a/docs/offlineimap.txt
+++ b/docs/offlineimap.txt
@@ -177,6 +177,22 @@ before running this fix as well as verify the results using the `--dry-run'
 flag first.
 
 
+--mbnames-prune::
+  Remove dangling entries for removed accounts or if mbnames is not enabled/used
+  anymore.
++
+Internally, offlineimap build intermediate mbnames files. They are added
+automatically when mbnames is enabled. However, disabling accounts so they are
+not synced anymore does not necessarily means they should be removed from the file
+built by mbnames. It is required to start offlineimap with this CLI option each
+time accounts are removed. When run, any account not in the 'accounts'
+configuration option are removed in the mbnames file.
++
+It is possible to manually remove intermediate files in '<metadata>/mbnames/'.
++
+Notice this option honors --dry-run.
+
+
 Synchronization Performance
 ---------------------------
 
-- /interdiff --

The following changes since commit e8509a04e6ccecf0eb416f237034767b0aad300f:

  remove dead code (2016-06-26 17:09:03 +0200)

are available in the git repository at:

  https://github.com/nicolas33/offlineimap.git ns/mbnames-prune-2

for you to fetch changes up to 9f5c9680eed12288287fe5fa32b18dfe92e22f27:

  learn --mbnames-prune CLI option (2016-06-26 18:36:20 +0200)

----------------------------------------------------------------

 docs/offlineimap.txt        |  16 ++++++
 offlineimap/CustomConfig.py |   4 +-
 offlineimap/init.py         |  12 +++-
 offlineimap/mbnames.py      | 131 +++++++++++++++++++++++++++++++-------------
 4 files changed, 121 insertions(+), 42 deletions(-)

diff --git a/docs/offlineimap.txt b/docs/offlineimap.txt
index 989bbae..fd2dd9d 100644
--- a/docs/offlineimap.txt
+++ b/docs/offlineimap.txt
@@ -177,6 +177,22 @@ before running this fix as well as verify the results using the `--dry-run'
 flag first.
 
 
+--mbnames-prune::
+  Remove dangling entries for removed accounts or if mbnames is not enabled/used
+  anymore.
++
+Internally, offlineimap build intermediate mbnames files. They are added
+automatically when mbnames is enabled. However, disabling accounts so they are
+not synced anymore does not necessarily means they should be removed from the file
+built by mbnames. It is required to start offlineimap with this CLI option each
+time accounts are removed. When run, any account not in the 'accounts'
+configuration option are removed in the mbnames file.
++
+It is possible to manually remove intermediate files in '<metadata>/mbnames/'.
++
+Notice this option honors --dry-run.
+
+
 Synchronization Performance
 ---------------------------
 
diff --git a/offlineimap/CustomConfig.py b/offlineimap/CustomConfig.py
index 445a04f..8fc62c6 100644
--- a/offlineimap/CustomConfig.py
+++ b/offlineimap/CustomConfig.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2003-2015 John Goerzen & contributors
+# Copyright (C) 2003-2016 John Goerzen & contributors
 #
 #    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
@@ -22,7 +22,7 @@ import six
 
 try:
     from ConfigParser import SafeConfigParser, Error
-except ImportError: #python3
+except ImportError: # Python3.
     from configparser import SafeConfigParser, Error
 from offlineimap.localeval import LocalEval
 
diff --git a/offlineimap/init.py b/offlineimap/init.py
index 37df977..c0f69f3 100644
--- a/offlineimap/init.py
+++ b/offlineimap/init.py
@@ -149,6 +149,10 @@ class OfflineImap(object):
                   action="store_true", dest="migrate_fmd5", default=False,
                   help="migrate FMD5 hashes from versions prior to 6.3.5")
 
+        parser.add_option("--mbnames-prune",
+                  action="store_true", dest="mbnames_prune", default=False,
+                  help="remove mbnames entries for accounts not in accounts")
+
         parser.add_option("-V",
                   action="store_true", dest="version",
                   default=False,
@@ -267,8 +271,14 @@ class OfflineImap(object):
                 if dtype.lower() == u'imap':
                     imaplib.Debug = 5
 
+        if options.mbnames_prune:
+            mbnames.init(config, self.ui, options.dryrun)
+            mbnames.prune(config.get("general", "accounts"))
+            mbnames.write()
+            sys.exit(0)
+
         if options.runonce:
-            # Must kill the possible default option
+            # Must kill the possible default option.
             if config.has_option('DEFAULT', 'autorefresh'):
                 config.remove_option('DEFAULT', 'autorefresh')
             # FIXME: spaghetti code alert!
diff --git a/offlineimap/mbnames.py b/offlineimap/mbnames.py
index 1dfcd3a..267bb1e 100644
--- a/offlineimap/mbnames.py
+++ b/offlineimap/mbnames.py
@@ -19,22 +19,28 @@
 import re   # For folderfilter.
 import json
 from threading import Lock
-from os import listdir, makedirs, path
+from os import listdir, makedirs, path, unlink
 from sys import exc_info
 try:
     import UserDict
-except ImportError:
-    # Py3
+except ImportError: # Py3.
     from collections import UserDict
+try:
+    from ConfigParser import NoSectionError
+except ImportError: # Py3.
+    from configparser import NoSectionError
 
 
 _mbLock = Lock()
 _mbnames = None
 
 
+def _is_enabled(conf):
+    return False
+
 def add(accountname, folder_root, foldername):
     global _mbnames
-    if _mbnames is None:
+    if _mbnames.is_enabled() is not True:
         return
 
     with _mbLock:
@@ -42,15 +48,21 @@ def add(accountname, folder_root, foldername):
 
 def init(conf, ui, dry_run):
     global _mbnames
-    enabled = conf.getdefaultboolean("mbnames", "enabled", False)
-    if enabled is True and _mbnames is None:
+    if _mbnames is None:
         _mbnames = _Mbnames(conf, ui, dry_run)
 
+def prune(accounts):
+    global _mbnames
+    if _mbnames.is_enabled() is True:
+        _mbnames.prune(accounts)
+    else:
+        _mbnames.pruneAll(accounts)
+
 def write():
     """Write the mbnames file."""
 
     global _mbnames
-    if _mbnames is None:
+    if _mbnames.is_enabled() is not True:
         return
 
     if _mbnames.get_incremental() is not True:
@@ -60,7 +72,7 @@ def writeIntermediateFile(accountname):
     """Write intermediate mbnames file."""
 
     global _mbnames
-    if _mbnames is None:
+    if _mbnames.is_enabled() is not True:
         return
 
     _mbnames.writeIntermediateFile(accountname)
@@ -101,17 +113,18 @@ class _IntermediateMbnames(object):
                 })
 
         if not self._dryrun:
-            with open(self._path, "wt") as intermediateFile:
-                json.dump(itemlist, intermediateFile)
+            with open(self._path, "wt") as intermediateFD:
+                json.dump(itemlist, intermediateFD)
 
 
 class _Mbnames(object):
     def __init__(self, config, ui, dry_run):
 
         self._config = config
-        self._dryrun = dry_run
         self.ui = ui
+        self._dryrun = dry_run
 
+        self._enabled = None
         # Keys: accountname, values: _IntermediateMbnames instance
         self._intermediates = {}
         self._incremental = None
@@ -119,14 +132,13 @@ class _Mbnames(object):
         self._path = None
         self._folderfilter = lambda accountname, foldername: True
         self._func_sortkey = lambda d: (d['accountname'], d['foldername'])
-        self._peritem = self._config.get("mbnames", "peritem", raw=1)
-
         localeval = config.getlocaleval()
-        self._header = localeval.eval(config.get("mbnames", "header"))
-        self._sep = localeval.eval(config.get("mbnames", "sep"))
-        self._footer = localeval.eval(config.get("mbnames", "footer"))
-
         mbnamesdir = path.join(config.getmetadatadir(), "mbnames")
+        self._peritem = None
+        self._header = None
+        self._sep = None
+        self._footer = None
+
         try:
             if not self._dryrun:
                 makedirs(mbnamesdir)
@@ -134,17 +146,40 @@ class _Mbnames(object):
             pass
         self._mbnamesdir = mbnamesdir
 
-        xforms = [path.expanduser, path.expandvars]
-        self._path = config.apply_xforms(
-            config.get("mbnames", "filename"), xforms)
+        try:
+            self._enabled = self._config.getdefaultboolean(
+                "mbnames", "enabled", False)
+            self._peritem = self._config.get("mbnames", "peritem", raw=1)
+            self._header = localeval.eval(config.get("mbnames", "header"))
+            self._sep = localeval.eval(config.get("mbnames", "sep"))
+            self._footer = localeval.eval(config.get("mbnames", "footer"))
+
+            xforms = [path.expanduser, path.expandvars]
+            self._path = config.apply_xforms(
+                config.get("mbnames", "filename"), xforms)
+
+            if self._config.has_option("mbnames", "sort_keyfunc"):
+                self._func_sortkey = localeval.eval(
+                    self._config.get("mbnames", "sort_keyfunc"), {'re': re})
+
+            if self._config.has_option("mbnames", "folderfilter"):
+                self._folderfilter = localeval.eval(
+                    self._config.get("mbnames", "folderfilter"), {'re': re})
+        except NoSectionError:
+            pass
 
-        if self._config.has_option("mbnames", "sort_keyfunc"):
-            self._func_sortkey = localeval.eval(
-                self._config.get("mbnames", "sort_keyfunc"), {'re': re})
+    def _iterIntermediateFiles(self):
+        for foo in listdir(self._mbnamesdir):
+            foo = path.join(self._mbnamesdir, foo)
+            if path.isfile(foo) and foo[-5:] == '.json':
+                yield foo
 
-        if self._config.has_option("mbnames", "folderfilter"):
-            self._folderfilter = localeval.eval(
-                self._config.get("mbnames", "folderfilter"), {'re': re})
+    def _removeIntermediateFile(self, path):
+        if self._dryrun:
+            self.ui.info("would remove %s"% path)
+        else:
+            unlink(path)
+            self.ui.info("removed %s"% path)
 
     def addAccountFolder(self, accountname, folder_root, foldername):
         """Add foldername entry for an account."""
@@ -167,23 +202,41 @@ class _Mbnames(object):
 
         return self._incremental
 
+    def is_enabled(self):
+        return self._enabled
+
+    def prune(self, accounts):
+        removals = False
+        for intermediateFile in self._iterIntermediateFiles():
+            filename = path.basename(intermediateFile)
+            accountname = filename[:-5]
+            if accountname not in accounts:
+                removals = True
+                self._removeIntermediateFile(intermediateFile)
+
+        if removals is False:
+            self.ui.info("no cache file to remove")
+
+    def pruneAll(self, accounts):
+        for intermediateFile in self._iterIntermediateFiles():
+            self._removeIntermediateFile(intermediateFile)
+
     def write(self):
         itemlist = []
 
         try:
-            for foo in listdir(self._mbnamesdir):
-                foo = path.join(self._mbnamesdir, foo)
-                if path.isfile(foo) and foo[-5:] == '.json':
-                    try:
-                        with open(foo, 'rt') as intermediateFile:
-                            for item in json.load(intermediateFile):
-                                itemlist.append(item)
-                    except Exception as e:
-                        self.ui.error(
-                            e,
-                            exc_info()[2],
-                            "intermediate mbnames file %s not properly read"% foo
-                        )
+            for intermediateFile in self._iterIntermediateFiles():
+                try:
+                    with open(intermediateFile, 'rt') as intermediateFD:
+                        for item in json.load(intermediateFD):
+                            itemlist.append(item)
+                except Exception as e:
+                    self.ui.error(
+                        e,
+                        exc_info()[2],
+                        ("intermediate mbnames file %s not properly read"%
+                            intermediateFile)
+                    )
         except OSError:
             pass
 
-- 
2.7.4



More information about the OfflineIMAP-project mailing list