UIDMaps: prevent from leaving a truncated map file
If the map file is not properly written (e.g. due to unexpected kill) offlineimap might wrongly consider some UIDs to have been deleted from the local side which could lead to data loss. Use a temporary map file rather than writing to the map file directly. Github-ref: https://github.com/OfflineIMAP/offlineimap/issues/380 Signed-off-by: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
This commit is contained in:
parent
0a635bd236
commit
2ac68d82aa
@ -16,6 +16,8 @@
|
|||||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
import shutil
|
||||||
|
from os import fsync, unlink
|
||||||
from sys import exc_info
|
from sys import exc_info
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
import six
|
import six
|
||||||
@ -31,6 +33,7 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
be an IMAPFolder.
|
be an IMAPFolder.
|
||||||
|
|
||||||
Instance variables (self.):
|
Instance variables (self.):
|
||||||
|
dofsync: boolean for fsync calls.
|
||||||
r2l: dict mapping message uids: self.r2l[remoteuid]=localuid
|
r2l: dict mapping message uids: self.r2l[remoteuid]=localuid
|
||||||
l2r: dict mapping message uids: self.r2l[localuid]=remoteuid
|
l2r: dict mapping message uids: self.r2l[localuid]=remoteuid
|
||||||
#TODO: what is the difference, how are they used?
|
#TODO: what is the difference, how are they used?
|
||||||
@ -39,18 +42,33 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
IMAPFolder.__init__(self, *args, **kwargs)
|
IMAPFolder.__init__(self, *args, **kwargs)
|
||||||
|
self.dofsync = self.config.getdefaultboolean("general", "fsync", True)
|
||||||
self.maplock = Lock()
|
self.maplock = Lock()
|
||||||
(self.diskr2l, self.diskl2r) = self._loadmaps()
|
self.diskr2l, self.diskl2r = self._loadmaps()
|
||||||
|
self.r2l, self.l2r = None, None
|
||||||
|
# Representing the local IMAP Folder using local UIDs.
|
||||||
|
# XXX: This should be removed since we inherit from IMAPFolder.
|
||||||
|
# See commit 3ce514e92ba7 to know more.
|
||||||
self._mb = IMAPFolder(*args, **kwargs)
|
self._mb = IMAPFolder(*args, **kwargs)
|
||||||
"""Representing the local IMAP Folder using local UIDs"""
|
|
||||||
|
|
||||||
def _getmapfilename(self):
|
def _getmapfilename(self):
|
||||||
return os.path.join(self.repository.getmapdir(),
|
return os.path.join(self.repository.getmapdir(),
|
||||||
self.getfolderbasename())
|
self.getfolderbasename())
|
||||||
|
|
||||||
def _loadmaps(self):
|
def _loadmaps(self):
|
||||||
with self.maplock:
|
mapfilename = self._getmapfilename()
|
||||||
mapfilename = self._getmapfilename()
|
mapfilenametmp = "%s.tmp"% mapfilename
|
||||||
|
mapfilenamelock = "%s.lock"% mapfilename
|
||||||
|
with self.maplock and open(mapfilenamelock, 'w') as mapfilelock:
|
||||||
|
try:
|
||||||
|
fnctl.lockf(mapfilelock, fnctl.LOCK_EX) # Blocks until acquired.
|
||||||
|
except NameError:
|
||||||
|
pass # Windows...
|
||||||
|
if os.path.exists(mapfilenametmp):
|
||||||
|
self.ui.warn("a previous run might have leave the UIDMaps file"
|
||||||
|
" in incorrect state; some sync operations might be done"
|
||||||
|
" again and some emails might become duplicated.")
|
||||||
|
unlink(mapfilenametmp)
|
||||||
if not os.path.exists(mapfilename):
|
if not os.path.exists(mapfilename):
|
||||||
return ({}, {})
|
return ({}, {})
|
||||||
file = open(mapfilename, 'rt')
|
file = open(mapfilename, 'rt')
|
||||||
@ -77,8 +95,10 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
|
|
||||||
def _savemaps(self):
|
def _savemaps(self):
|
||||||
mapfilename = self._getmapfilename()
|
mapfilename = self._getmapfilename()
|
||||||
|
# Do not use the map file directly to prevent from leaving it truncated.
|
||||||
|
mapfilenametmp = "%s.tmp"% mapfilename
|
||||||
mapfilenamelock = "%s.lock"% mapfilename
|
mapfilenamelock = "%s.lock"% mapfilename
|
||||||
with open(mapfilenamelock, 'w') as mapfilelock:
|
with self.maplock and open(mapfilenamelock, 'w') as mapfilelock:
|
||||||
# The "account" lock already prevents from multiple access by
|
# The "account" lock already prevents from multiple access by
|
||||||
# different processes. However, we still need to protect for
|
# different processes. However, we still need to protect for
|
||||||
# multiple access from different threads.
|
# multiple access from different threads.
|
||||||
@ -86,10 +106,13 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
fnctl.lockf(mapfilelock, fnctl.LOCK_EX) # Blocks until acquired.
|
fnctl.lockf(mapfilelock, fnctl.LOCK_EX) # Blocks until acquired.
|
||||||
except NameError:
|
except NameError:
|
||||||
pass # Windows...
|
pass # Windows...
|
||||||
with open(mapfilename, 'wt') as mapfilefd:
|
with open(mapfilenametmp, 'wt') as mapfilefd:
|
||||||
for (key, value) in self.diskl2r.items():
|
for (key, value) in self.diskl2r.items():
|
||||||
mapfilefd.write("%d:%d\n"% (key, value))
|
mapfilefd.write("%d:%d\n"% (key, value))
|
||||||
|
if self.dofsync is True:
|
||||||
|
fsync(mapfilefd)
|
||||||
# The lock is released when the file descriptor ends.
|
# The lock is released when the file descriptor ends.
|
||||||
|
shutil.move(mapfilenametmp, mapfilename)
|
||||||
|
|
||||||
def _uidlist(self, mapping, items):
|
def _uidlist(self, mapping, items):
|
||||||
try:
|
try:
|
||||||
@ -140,6 +163,7 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
# Interface from BaseFolder
|
# Interface from BaseFolder
|
||||||
def uidexists(self, ruid):
|
def uidexists(self, ruid):
|
||||||
"""Checks if the (remote) UID exists in this Folder"""
|
"""Checks if the (remote) UID exists in this Folder"""
|
||||||
|
|
||||||
# This implementation overrides the one in BaseFolder, as it is
|
# This implementation overrides the one in BaseFolder, as it is
|
||||||
# much more efficient for the mapped case.
|
# much more efficient for the mapped case.
|
||||||
return ruid in self.r2l
|
return ruid in self.r2l
|
||||||
@ -147,7 +171,9 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
# Interface from BaseFolder
|
# Interface from BaseFolder
|
||||||
def getmessageuidlist(self):
|
def getmessageuidlist(self):
|
||||||
"""Gets a list of (remote) UIDs.
|
"""Gets a list of (remote) UIDs.
|
||||||
|
|
||||||
You may have to call cachemessagelist() before calling this function!"""
|
You may have to call cachemessagelist() before calling this function!"""
|
||||||
|
|
||||||
# This implementation overrides the one in BaseFolder, as it is
|
# This implementation overrides the one in BaseFolder, as it is
|
||||||
# much more efficient for the mapped case.
|
# much more efficient for the mapped case.
|
||||||
return self.r2l.keys()
|
return self.r2l.keys()
|
||||||
@ -155,16 +181,19 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
# Interface from BaseFolder
|
# Interface from BaseFolder
|
||||||
def getmessagecount(self):
|
def getmessagecount(self):
|
||||||
"""Gets the number of messages in this folder.
|
"""Gets the number of messages in this folder.
|
||||||
|
|
||||||
You may have to call cachemessagelist() before calling this function!"""
|
You may have to call cachemessagelist() before calling this function!"""
|
||||||
|
|
||||||
# This implementation overrides the one in BaseFolder, as it is
|
# This implementation overrides the one in BaseFolder, as it is
|
||||||
# much more efficient for the mapped case.
|
# much more efficient for the mapped case.
|
||||||
return len(self.r2l)
|
return len(self.r2l)
|
||||||
|
|
||||||
# Interface from BaseFolder
|
# Interface from BaseFolder
|
||||||
def getmessagelist(self):
|
def getmessagelist(self):
|
||||||
"""Gets the current message list. This function's implementation
|
"""Gets the current message list.
|
||||||
is quite expensive for the mapped UID case. You must call
|
|
||||||
cachemessagelist() before calling this function!"""
|
This function's implementation is quite expensive for the mapped UID
|
||||||
|
case. You must call cachemessagelist() before calling this function!"""
|
||||||
|
|
||||||
retval = {}
|
retval = {}
|
||||||
localhash = self._mb.getmessagelist()
|
localhash = self._mb.getmessagelist()
|
||||||
@ -208,6 +237,7 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
check against dryrun settings, so you need to ensure that
|
check against dryrun settings, so you need to ensure that
|
||||||
savemessage is never called in a dryrun mode.
|
savemessage is never called in a dryrun mode.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.ui.savemessage('imap', uid, flags, self)
|
self.ui.savemessage('imap', uid, flags, self)
|
||||||
# Mapped UID instances require the source to already have a
|
# Mapped UID instances require the source to already have a
|
||||||
# positive UID, so simply return here.
|
# positive UID, so simply return here.
|
||||||
@ -263,6 +293,7 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
:param new_uid: The old remote UID will be changed to a new
|
:param new_uid: The old remote UID will be changed to a new
|
||||||
UID. The UIDMaps case handles this efficiently by simply
|
UID. The UIDMaps case handles this efficiently by simply
|
||||||
changing the mappings file."""
|
changing the mappings file."""
|
||||||
|
|
||||||
if ruid not in self.r2l:
|
if ruid not in self.r2l:
|
||||||
raise OfflineImapError("Cannot change unknown Maildir UID %s"%
|
raise OfflineImapError("Cannot change unknown Maildir UID %s"%
|
||||||
ruid, OfflineImapError.ERROR.MESSAGE)
|
ruid, OfflineImapError.ERROR.MESSAGE)
|
||||||
|
Loading…
Reference in New Issue
Block a user