Implement locking of whole storage
This commit is contained in:
parent
8e09c0b315
commit
2c45b1998c
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user