|
@@ -29,6 +29,8 @@ import json
|
|
|
import os
|
|
import os
|
|
|
import posixpath
|
|
import posixpath
|
|
|
import shutil
|
|
import shutil
|
|
|
|
|
+import stat
|
|
|
|
|
+import threading
|
|
|
import time
|
|
import time
|
|
|
from contextlib import contextmanager
|
|
from contextlib import contextmanager
|
|
|
from hashlib import md5
|
|
from hashlib import md5
|
|
@@ -37,6 +39,35 @@ from uuid import uuid4
|
|
|
|
|
|
|
|
import vobject
|
|
import vobject
|
|
|
|
|
|
|
|
|
|
+if os.name == "nt":
|
|
|
|
|
+ import ctypes
|
|
|
|
|
+ import ctypes.wintypes
|
|
|
|
|
+ import msvcrt
|
|
|
|
|
+
|
|
|
|
|
+ LOCKFILE_EXCLUSIVE_LOCK = 2
|
|
|
|
|
+ if ctypes.sizeof(ctypes.c_void_p) == 4:
|
|
|
|
|
+ ULONG_PTR = ctypes.c_uint32
|
|
|
|
|
+ else:
|
|
|
|
|
+ ULONG_PTR = ctypes.c_uint64
|
|
|
|
|
+
|
|
|
|
|
+ class Overlapped(ctypes.Structure):
|
|
|
|
|
+ _fields_ = [("internal", ULONG_PTR),
|
|
|
|
|
+ ("internal_high", ULONG_PTR),
|
|
|
|
|
+ ("offset", ctypes.wintypes.DWORD),
|
|
|
|
|
+ ("offset_high", ctypes.wintypes.DWORD),
|
|
|
|
|
+ ("h_event", ctypes.wintypes.HANDLE)]
|
|
|
|
|
+
|
|
|
|
|
+ lock_file_ex = ctypes.windll.kernel32.LockFileEx
|
|
|
|
|
+ lock_file_ex.argtypes = [ctypes.wintypes.HANDLE,
|
|
|
|
|
+ ctypes.wintypes.DWORD,
|
|
|
|
|
+ ctypes.wintypes.DWORD,
|
|
|
|
|
+ ctypes.wintypes.DWORD,
|
|
|
|
|
+ ctypes.wintypes.DWORD,
|
|
|
|
|
+ ctypes.POINTER(Overlapped)]
|
|
|
|
|
+ lock_file_ex.restype = ctypes.wintypes.BOOL
|
|
|
|
|
+elif os.name == "posix":
|
|
|
|
|
+ import fcntl
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def load(configuration, logger):
|
|
def load(configuration, logger):
|
|
|
"""Load the storage manager chosen in configuration."""
|
|
"""Load the storage manager chosen in configuration."""
|
|
@@ -245,6 +276,18 @@ class BaseCollection:
|
|
|
"""Get the unicode string representing the whole collection."""
|
|
"""Get the unicode string representing the whole collection."""
|
|
|
raise NotImplementedError
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def acquire_lock(cls, mode):
|
|
|
|
|
+ """Lock the whole storage.
|
|
|
|
|
+
|
|
|
|
|
+ ``mode`` must either be "r" for shared access or "w" for exclusive
|
|
|
|
|
+ access.
|
|
|
|
|
+
|
|
|
|
|
+ Returns an object which has a method ``release``.
|
|
|
|
|
+
|
|
|
|
|
+ """
|
|
|
|
|
+ raise NotImplementedError
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class Collection(BaseCollection):
|
|
class Collection(BaseCollection):
|
|
|
"""Collection stored in several files per calendar."""
|
|
"""Collection stored in several files per calendar."""
|
|
@@ -474,3 +517,61 @@ class Collection(BaseCollection):
|
|
|
elif self.get_meta("tag") == "VADDRESSBOOK":
|
|
elif self.get_meta("tag") == "VADDRESSBOOK":
|
|
|
return "".join([item.serialize() for item in items])
|
|
return "".join([item.serialize() for item in items])
|
|
|
return ""
|
|
return ""
|
|
|
|
|
+
|
|
|
|
|
+ _lock = threading.Lock()
|
|
|
|
|
+
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def acquire_lock(cls, mode):
|
|
|
|
|
+ class Lock:
|
|
|
|
|
+ def __init__(self, release_method):
|
|
|
|
|
+ self._release_method = release_method
|
|
|
|
|
+
|
|
|
|
|
+ def release(self):
|
|
|
|
|
+ self._release_method()
|
|
|
|
|
+
|
|
|
|
|
+ if mode not in ("r", "w"):
|
|
|
|
|
+ raise ValueError("Invalid lock mode: %s" % mode)
|
|
|
|
|
+ folder = os.path.expanduser(
|
|
|
|
|
+ cls.configuration.get("storage", "filesystem_folder"))
|
|
|
|
|
+ if not os.path.exists(folder):
|
|
|
|
|
+ os.makedirs(folder, exist_ok=True)
|
|
|
|
|
+ lock_path = os.path.join(folder, "Radicale.lock")
|
|
|
|
|
+ lock_file = open(lock_path, "w+")
|
|
|
|
|
+ # set access rights to a necessary minimum to prevent locking by
|
|
|
|
|
+ # arbitrary users
|
|
|
|
|
+ try:
|
|
|
|
|
+ os.chmod(lock_path, stat.S_IWUSR | stat.S_IRUSR)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ cls.logger.debug("Failed to set permissions on lock file")
|
|
|
|
|
+ locked = False
|
|
|
|
|
+ if os.name == "nt":
|
|
|
|
|
+ handle = msvcrt.get_osfhandle(lock_file.fileno())
|
|
|
|
|
+ flags = LOCKFILE_EXCLUSIVE_LOCK if mode == "w" else 0
|
|
|
|
|
+ overlapped = Overlapped()
|
|
|
|
|
+ if lock_file_ex(handle, flags, 0, 1, 0, overlapped):
|
|
|
|
|
+ locked = True
|
|
|
|
|
+ elif os.name == "posix":
|
|
|
|
|
+ operation = fcntl.LOCK_EX if mode == "w" else fcntl.LOCK_SH
|
|
|
|
|
+ # According to documentation flock() is emulated with fcntl() on
|
|
|
|
|
+ # some platforms. fcntl() locks are not associated with an open
|
|
|
|
|
+ # file descriptor. The same file can be locked multiple times
|
|
|
|
|
+ # within the same process and if any fd of the file is closed,
|
|
|
|
|
+ # all locks are released.
|
|
|
|
|
+ # flock() does not work on NFS shares.
|
|
|
|
|
+ try:
|
|
|
|
|
+ fcntl.flock(lock_file.fileno(), operation)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ pass
|
|
|
|
|
+ else:
|
|
|
|
|
+ locked = True
|
|
|
|
|
+ if locked:
|
|
|
|
|
+ lock = Lock(lock_file.close)
|
|
|
|
|
+ else:
|
|
|
|
|
+ cls.logger.debug("Locking not supported")
|
|
|
|
|
+ lock_file.close()
|
|
|
|
|
+ # Fallback to primitive lock which only works within one process
|
|
|
|
|
+ # and doesn't distinguish between shared and exclusive access.
|
|
|
|
|
+ # TODO: use readers–writer lock
|
|
|
|
|
+ cls._lock.acquire()
|
|
|
|
|
+ lock = Lock(cls._lock.release)
|
|
|
|
|
+ return lock
|