From 2c45b1998c5c5a695770ae1a82439a0faa3bd1d5 Mon Sep 17 00:00:00 2001 From: Unrud Date: Sat, 21 May 2016 00:38:42 +0200 Subject: [PATCH] Implement locking of whole storage --- radicale/__init__.py | 61 ++++++++++++++++---------- radicale/storage.py | 101 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 24 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index 35ab75d..9894d9f 100644 --- a/radicale/__init__.py +++ b/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 + 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) - # 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 + 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 - 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 + # 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 diff --git a/radicale/storage.py b/radicale/storage.py index 982026f..7e4c2d8 100644 --- a/radicale/storage.py +++ b/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