فهرست منبع

Implement locking of whole storage

Unrud 9 سال پیش
والد
کامیت
2c45b1998c
2فایلهای تغییر یافته به همراه140 افزوده شده و 26 حذف شده
  1. 39 26
      radicale/__init__.py
  2. 101 0
      radicale/storage.py

+ 39 - 26
radicale/__init__.py

@@ -273,32 +273,45 @@ class Application:
         is_authenticated = self.is_authenticated(user, password)
         is_valid_user = is_authenticated or not user
 
-        if is_valid_user:
-            items = self.Collection.discover(
-                path, environ.get("HTTP_DEPTH", "0"))
-            read_allowed_items, write_allowed_items = (
-                self.collect_allowed_items(items, user))
-        else:
-            read_allowed_items, write_allowed_items = None, None
-
-        # Get content
-        content_length = int(environ.get("CONTENT_LENGTH") or 0)
-        if content_length:
-            content = self.decode(
-                environ["wsgi.input"].read(content_length), environ)
-            self.logger.debug("Request content:\n%s" % content)
-        else:
-            content = None
-
-        if is_valid_user and (
-                (read_allowed_items or write_allowed_items) or
-                (is_authenticated and function == self.do_PROPFIND) or
-                function == self.do_OPTIONS):
-            status, headers, answer = function(
-                environ, read_allowed_items, write_allowed_items, content,
-                user)
-        else:
-            status, headers, answer = NOT_ALLOWED
+        lock = None
+        try:
+            if is_valid_user:
+                if function in (self.do_GET, self.do_HEAD,
+                                self.do_OPTIONS, self.do_PROPFIND,
+                                self.do_REPORT):
+                    lock_mode = "r"
+                else:
+                    lock_mode = "w"
+                lock = self.Collection.acquire_lock(lock_mode)
+
+                items = self.Collection.discover(
+                    path, environ.get("HTTP_DEPTH", "0"))
+                read_allowed_items, write_allowed_items = (
+                    self.collect_allowed_items(items, user))
+            else:
+                read_allowed_items, write_allowed_items = None, None
+
+            # Get content
+            content_length = int(environ.get("CONTENT_LENGTH") or 0)
+            if content_length:
+                content = self.decode(
+                    environ["wsgi.input"].read(content_length), environ)
+                self.logger.debug("Request content:\n%s" % content)
+            else:
+                content = None
+
+            if is_valid_user and (
+                    (read_allowed_items or write_allowed_items) or
+                    (is_authenticated and function == self.do_PROPFIND) or
+                    function == self.do_OPTIONS):
+                status, headers, answer = function(
+                    environ, read_allowed_items, write_allowed_items, content,
+                    user)
+            else:
+                status, headers, answer = NOT_ALLOWED
+        finally:
+            if lock:
+                lock.release()
 
         if (status, headers, answer) == NOT_ALLOWED and not is_authenticated:
             # Unknown or unauthorized user

+ 101 - 0
radicale/storage.py

@@ -29,6 +29,8 @@ import json
 import os
 import posixpath
 import shutil
+import stat
+import threading
 import time
 from contextlib import contextmanager
 from hashlib import md5
@@ -37,6 +39,35 @@ from uuid import uuid4
 
 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):
     """Load the storage manager chosen in configuration."""
@@ -245,6 +276,18 @@ class BaseCollection:
         """Get the unicode string representing the whole collection."""
         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):
     """Collection stored in several files per calendar."""
@@ -474,3 +517,61 @@ class Collection(BaseCollection):
         elif self.get_meta("tag") == "VADDRESSBOOK":
             return "".join([item.serialize() for item in items])
         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