Implement locking of whole storage

This commit is contained in:
Unrud 2016-05-21 00:38:42 +02:00
parent 8e09c0b315
commit 2c45b1998c
2 changed files with 138 additions and 24 deletions

View File

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

View File

@ -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 readerswriter lock
cls._lock.acquire()
lock = Lock(cls._lock.release)
return lock