| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820 |
- # This file is part of Radicale Server - Calendar Server
- # Copyright © 2014 Jean-Marc Martins
- # Copyright © 2012-2016 Guillaume Ayoub
- #
- # This library is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This library is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
- """
- Storage backends.
- This module loads the storage backend, according to the storage configuration.
- Default storage uses one folder per collection and one file per collection
- entry.
- """
- import errno
- import json
- import os
- import posixpath
- import shlex
- import stat
- import subprocess
- import threading
- import time
- from contextlib import contextmanager
- from hashlib import md5
- from importlib import import_module
- from itertools import groupby
- from random import getrandbits
- from tempfile import TemporaryDirectory, NamedTemporaryFile
- 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
- unlock_file_ex = ctypes.windll.kernel32.UnlockFileEx
- unlock_file_ex.argtypes = [
- ctypes.wintypes.HANDLE,
- ctypes.wintypes.DWORD,
- ctypes.wintypes.DWORD,
- ctypes.wintypes.DWORD,
- ctypes.POINTER(Overlapped)]
- unlock_file_ex.restype = ctypes.wintypes.BOOL
- elif os.name == "posix":
- import fcntl
- def load(configuration, logger):
- """Load the storage manager chosen in configuration."""
- storage_type = configuration.get("storage", "type")
- if storage_type == "multifilesystem":
- collection_class = Collection
- else:
- collection_class = import_module(storage_type).Collection
- class CollectionCopy(collection_class):
- """Collection copy, avoids overriding the original class attributes."""
- CollectionCopy.configuration = configuration
- CollectionCopy.logger = logger
- return CollectionCopy
- def get_etag(text):
- """Etag from collection or item."""
- etag = md5()
- etag.update(text.encode("utf-8"))
- return '"%s"' % etag.hexdigest()
- def get_uid(item):
- """UID value of an item if defined."""
- return hasattr(item, "uid") and item.uid.value
- def sanitize_path(path):
- """Make path absolute with leading slash to prevent access to other data.
- Preserve a potential trailing slash.
- """
- trailing_slash = "/" if path.endswith("/") else ""
- path = posixpath.normpath(path)
- new_path = "/"
- for part in path.split("/"):
- if not part or part in (".", ".."):
- continue
- new_path = posixpath.join(new_path, part)
- trailing_slash = "" if new_path.endswith("/") else trailing_slash
- return new_path + trailing_slash
- def is_safe_path_component(path):
- """Check if path is a single component of a path.
- Check that the path is safe to join too.
- """
- return path and "/" not in path and path not in (".", "..")
- def is_safe_filesystem_path_component(path):
- """Check if path is a single component of a filesystem path.
- Check that the path is safe to join too.
- """
- return (
- path and not os.path.splitdrive(path)[0] and
- not os.path.split(path)[0] and path not in (os.curdir, os.pardir) and
- not path.startswith(".") and not path.endswith("~"))
- def path_to_filesystem(root, *paths):
- """Convert path to a local filesystem path relative to base_folder.
- `root` must be a secure filesystem path, it will be prepend to the path.
- Conversion of `paths` is done in a secure manner, or raises ``ValueError``.
- """
- paths = [sanitize_path(path).strip("/") for path in paths]
- safe_path = root
- for path in paths:
- if not path:
- continue
- for part in path.split("/"):
- if not is_safe_filesystem_path_component(part):
- raise UnsafePathError(part)
- safe_path = os.path.join(safe_path, part)
- return safe_path
- class UnsafePathError(ValueError):
- def __init__(self, path):
- message = "Can't translate name safely to filesystem: %s" % path
- super().__init__(message)
- class ComponentExistsError(ValueError):
- def __init__(self, path):
- message = "Component already exists: %s" % path
- super().__init__(message)
- class ComponentNotFoundError(ValueError):
- def __init__(self, path):
- message = "Component doesn't exist: %s" % path
- super().__init__(message)
- class EtagMismatchError(ValueError):
- def __init__(self, etag1, etag2):
- message = "ETags don't match: %s != %s" % (etag1, etag2)
- super().__init__(message)
- class Item:
- def __init__(self, collection, item, href, last_modified=None):
- self.collection = collection
- self.item = item
- self.href = href
- self.last_modified = last_modified
- def __getattr__(self, attr):
- return getattr(self.item, attr)
- @property
- def etag(self):
- return get_etag(self.serialize())
- class BaseCollection:
- # Overriden on copy by the "load" function
- configuration = None
- logger = None
- def __init__(self, path, principal=False):
- """Initialize the collection.
- ``path`` must be the normalized relative path of the collection, using
- the slash as the folder delimiter, with no leading nor trailing slash.
- """
- raise NotImplementedError
- @classmethod
- def discover(cls, path, depth="0"):
- """Discover a list of collections under the given ``path``.
- If ``depth`` is "0", only the actual object under ``path`` is
- returned.
- If ``depth`` is anything but "0", it is considered as "1" and direct
- children are included in the result.
- The ``path`` is relative.
- The root collection "/" must always exist.
- """
- raise NotImplementedError
- @classmethod
- def move(cls, item, to_collection, to_href):
- """Move an object.
- ``item`` is the item to move.
- ``to_collection`` is the target collection.
- ``to_href`` is the target name in ``to_collection``. An item with the
- same name might already exist.
- """
- if item.collection.path == to_collection.path and item.href == to_href:
- return
- to_collection.upload(to_href, item.item)
- item.collection.delete(item.href)
- @property
- def etag(self):
- return get_etag(self.serialize())
- @classmethod
- def create_collection(cls, href, collection=None, props=None):
- """Create a collection.
- If the collection already exists and neither ``collection`` nor
- ``props`` are set, this method shouldn't do anything. Otherwise the
- existing collection must be replaced.
- ``collection`` is a list of vobject components.
- ``props`` are metadata values for the collection.
- ``props["tag"]`` is the type of collection (VCALENDAR or
- VADDRESSBOOK). If the key ``tag`` is missing, it is guessed from the
- collection.
- """
- raise NotImplementedError
- def list(self):
- """List collection items."""
- raise NotImplementedError
- def get(self, href):
- """Fetch a single item."""
- raise NotImplementedError
- def get_multi(self, hrefs):
- """Fetch multiple items. Duplicate hrefs must be ignored.
- Functionally similar to ``get``, but might bring performance benefits
- on some storages when used cleverly.
- """
- for href in set(hrefs):
- yield self.get(href)
- def pre_filtered_list(self, filters):
- """List collection items with optional pre filtering.
- This could largely improve performance of reports depending on
- the filters and this implementation.
- This returns all event by default
- """
- return [self.get(href) for href in self.list()]
- def has(self, href):
- """Check if an item exists by its href.
- Functionally similar to ``get``, but might bring performance benefits
- on some storages when used cleverly.
- """
- return self.get(href) is not None
- def upload(self, href, vobject_item):
- """Upload a new or replace an existing item."""
- raise NotImplementedError
- def delete(self, href=None):
- """Delete an item.
- When ``href`` is ``None``, delete the collection.
- """
- raise NotImplementedError
- def get_meta(self, key):
- """Get metadata value for collection."""
- raise NotImplementedError
- def set_meta(self, props):
- """Set metadata values for collection."""
- raise NotImplementedError
- @property
- def last_modified(self):
- """Get the HTTP-datetime of when the collection was modified."""
- raise NotImplementedError
- def serialize(self):
- """Get the unicode string representing the whole collection."""
- raise NotImplementedError
- @classmethod
- @contextmanager
- def acquire_lock(cls, mode, user=None):
- """Set a context manager to lock the whole storage.
- ``mode`` must either be "r" for shared access or "w" for exclusive
- access.
- ``user`` is the name of the logged in user or empty.
- """
- raise NotImplementedError
- class Collection(BaseCollection):
- """Collection stored in several files per calendar."""
- def __init__(self, path, principal=False, folder=None):
- if not folder:
- folder = self._get_collection_root_folder()
- # Path should already be sanitized
- self.path = sanitize_path(path).strip("/")
- self.encoding = self.configuration.get("encoding", "stock")
- self._filesystem_path = path_to_filesystem(folder, self.path)
- self._props_path = os.path.join(
- self._filesystem_path, ".Radicale.props")
- split_path = self.path.split("/")
- self.owner = split_path[0] if len(split_path) > 1 else None
- self.is_principal = principal
- @classmethod
- def _get_collection_root_folder(cls):
- filesystem_folder = os.path.expanduser(
- cls.configuration.get("storage", "filesystem_folder"))
- return os.path.join(filesystem_folder, "collection-root")
- @contextmanager
- def _atomic_write(self, path, mode="w", newline=None):
- directory = os.path.dirname(path)
- tmp = NamedTemporaryFile(
- mode=mode, dir=directory, encoding=self.encoding,
- delete=False, prefix=".Radicale.tmp-", newline=newline)
- try:
- yield tmp
- if self.configuration.getboolean("storage", "filesystem_fsync"):
- if os.name == "posix" and hasattr(fcntl, "F_FULLFSYNC"):
- fcntl.fcntl(tmp.fileno(), fcntl.F_FULLFSYNC)
- else:
- os.fsync(tmp.fileno())
- tmp.close()
- os.replace(tmp.name, path)
- except:
- tmp.close()
- os.remove(tmp.name)
- raise
- self._sync_directory(directory)
- @staticmethod
- def _find_available_file_name(exists_fn):
- # Prevent infinite loop
- for _ in range(10000):
- file_name = hex(getrandbits(32))[2:]
- if not exists_fn(file_name):
- return file_name
- raise FileExistsError(errno.EEXIST, "No usable file name found")
- @classmethod
- def _sync_directory(cls, path):
- """Sync directory to disk.
- This only works on POSIX and does nothing on other systems.
- """
- if not cls.configuration.getboolean("storage", "filesystem_fsync"):
- return
- if os.name == "posix":
- fd = os.open(path, 0)
- try:
- if hasattr(fcntl, "F_FULLFSYNC"):
- fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
- else:
- os.fsync(fd)
- finally:
- os.close(fd)
- @classmethod
- def _makedirs_synced(cls, filesystem_path):
- """Recursively create a directory and its parents in a sync'ed way.
- This method acts silently when the folder already exists.
- """
- if os.path.isdir(filesystem_path):
- return
- parent_filesystem_path = os.path.dirname(filesystem_path)
- # Prevent infinite loop
- if filesystem_path != parent_filesystem_path:
- # Create parent dirs recursively
- cls._makedirs_synced(parent_filesystem_path)
- # Possible race!
- os.makedirs(filesystem_path, exist_ok=True)
- cls._sync_directory(parent_filesystem_path)
- @classmethod
- def discover(cls, path, depth="0"):
- if path is None:
- # Wrong URL
- return
- # Path should already be sanitized
- sane_path = sanitize_path(path).strip("/")
- attributes = sane_path.split("/")
- if not attributes[0]:
- attributes.pop()
- folder = cls._get_collection_root_folder()
- # Create the root collection
- cls._makedirs_synced(folder)
- try:
- filesystem_path = path_to_filesystem(folder, sane_path)
- except ValueError:
- # Path is unsafe
- return
- # Check if the path exists and if it leads to a collection or an item
- if not os.path.isdir(filesystem_path):
- if attributes and os.path.isfile(filesystem_path):
- href = attributes.pop()
- else:
- return
- else:
- href = None
- path = "/".join(attributes)
- principal = len(attributes) == 1
- collection = cls(path, principal)
- if href:
- yield collection.get(href)
- return
- yield collection
- if depth == "0":
- return
- for item in collection.list():
- yield collection.get(item)
- for href in os.listdir(filesystem_path):
- if not is_safe_filesystem_path_component(href):
- if not href.startswith(".Radicale"):
- cls.logger.debug("Skipping collection: %s", href)
- continue
- child_filesystem_path = path_to_filesystem(filesystem_path, href)
- if os.path.isdir(child_filesystem_path):
- child_path = posixpath.join(path, href)
- child_principal = len(attributes) == 0
- yield cls(child_path, child_principal)
- @classmethod
- def create_collection(cls, href, collection=None, props=None):
- folder = cls._get_collection_root_folder()
- # Path should already be sanitized
- sane_path = sanitize_path(href).strip("/")
- attributes = sane_path.split("/")
- if not attributes[0]:
- attributes.pop()
- principal = len(attributes) == 1
- filesystem_path = path_to_filesystem(folder, sane_path)
- if not props:
- props = {}
- if not props.get("tag") and collection:
- props["tag"] = collection[0].name
- if not props:
- cls._makedirs_synced(filesystem_path)
- return cls(sane_path, principal=principal)
- parent_dir = os.path.dirname(filesystem_path)
- cls._makedirs_synced(parent_dir)
- # Create a temporary directory with an unsafe name
- with TemporaryDirectory(
- prefix=".Radicale.tmp-", dir=parent_dir) as tmp_dir:
- # The temporary directory itself can't be renamed
- tmp_filesystem_path = os.path.join(tmp_dir, "collection")
- os.makedirs(tmp_filesystem_path)
- self = cls("/", principal=principal, folder=tmp_filesystem_path)
- self.set_meta(props)
- if collection:
- if props.get("tag") == "VCALENDAR":
- collection, = collection
- items = []
- for content in ("vevent", "vtodo", "vjournal"):
- items.extend(
- getattr(collection, "%s_list" % content, []))
- items_by_uid = groupby(sorted(items, key=get_uid), get_uid)
- vobject_items = {}
- for uid, items in items_by_uid:
- new_collection = vobject.iCalendar()
- for item in items:
- new_collection.add(item)
- href = self._find_available_file_name(
- vobject_items.get)
- vobject_items[href] = new_collection
- self.upload_all_nonatomic(vobject_items)
- elif props.get("tag") == "VCARD":
- vobject_items = {}
- for card in collection:
- href = self._find_available_file_name(
- vobject_items.get)
- vobject_items[href] = card
- self.upload_all_nonatomic(vobject_items)
- # This operation is not atomic on the filesystem level but it's
- # very unlikely that one rename operations succeeds while the
- # other fails or that only one gets written to disk.
- if os.path.exists(filesystem_path):
- os.rename(filesystem_path, os.path.join(tmp_dir, "delete"))
- os.rename(tmp_filesystem_path, filesystem_path)
- cls._sync_directory(parent_dir)
- return cls(sane_path, principal=principal)
- def upload_all_nonatomic(self, vobject_items):
- """Upload a new set of items.
- This takes a mapping of href and vobject items and
- uploads them nonatomic and without existence checks.
- """
- fs = []
- for href, item in vobject_items.items():
- path = path_to_filesystem(self._filesystem_path, href)
- fs.append(open(path, "w", encoding=self.encoding, newline=""))
- fs[-1].write(item.serialize())
- fsync_fn = lambda fd: None
- if self.configuration.getboolean("storage", "filesystem_fsync"):
- if os.name == "posix" and hasattr(fcntl, "F_FULLFSYNC"):
- fsync_fn = lambda fd: fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
- else:
- fsync_fn = os.fsync
- # sync everything at once because it's slightly faster.
- for f in fs:
- fsync_fn(f.fileno())
- f.close()
- self._sync_directory(self._filesystem_path)
- @classmethod
- def move(cls, item, to_collection, to_href):
- os.replace(
- path_to_filesystem(item.collection._filesystem_path, item.href),
- path_to_filesystem(to_collection._filesystem_path, to_href))
- cls._sync_directory(to_collection._filesystem_path)
- if item.collection._filesystem_path != to_collection._filesystem_path:
- cls._sync_directory(item.collection._filesystem_path)
- def list(self):
- try:
- hrefs = os.listdir(self._filesystem_path)
- except IOError:
- return
- for href in hrefs:
- if not is_safe_filesystem_path_component(href):
- if not href.startswith(".Radicale"):
- self.logger.debug("Skipping component: %s", href)
- continue
- path = os.path.join(self._filesystem_path, href)
- if os.path.isfile(path):
- yield href
- def get(self, href):
- if not href:
- return None
- href = href.strip("{}").replace("/", "_")
- if not is_safe_filesystem_path_component(href):
- self.logger.debug(
- "Can't translate name safely to filesystem: %s", href)
- return None
- path = path_to_filesystem(self._filesystem_path, href)
- if not os.path.isfile(path):
- return None
- with open(path, encoding=self.encoding, newline="") as fd:
- text = fd.read()
- last_modified = time.strftime(
- "%a, %d %b %Y %H:%M:%S GMT",
- time.gmtime(os.path.getmtime(path)))
- return Item(self, vobject.readOne(text), href, last_modified)
- def has(self, href):
- return self.get(href) is not None
- def upload(self, href, vobject_item):
- if not is_safe_filesystem_path_component(href):
- raise UnsafePathError(href)
- path = path_to_filesystem(self._filesystem_path, href)
- item = Item(self, vobject_item, href)
- with self._atomic_write(path, newline="") as fd:
- fd.write(item.serialize())
- return item
- def delete(self, href=None):
- if href is None:
- # Delete the collection
- if os.path.isdir(self._filesystem_path):
- parent_dir = os.path.dirname(self._filesystem_path)
- try:
- os.rmdir(self._filesystem_path)
- except OSError:
- with TemporaryDirectory(
- prefix=".Radicale.tmp-", dir=parent_dir) as tmp:
- os.rename(self._filesystem_path, os.path.join(
- tmp, os.path.basename(self._filesystem_path)))
- self._sync_directory(parent_dir)
- else:
- self._sync_directory(parent_dir)
- else:
- # Delete an item
- if not is_safe_filesystem_path_component(href):
- raise UnsafePathError(href)
- path = path_to_filesystem(self._filesystem_path, href)
- if not os.path.isfile(path):
- raise ComponentNotFoundError(href)
- os.remove(path)
- self._sync_directory(os.path.dirname(path))
- def get_meta(self, key):
- if os.path.exists(self._props_path):
- with open(self._props_path, encoding=self.encoding) as prop:
- return json.load(prop).get(key)
- def set_meta(self, props):
- if os.path.exists(self._props_path):
- with open(self._props_path, encoding=self.encoding) as prop:
- old_props = json.load(prop)
- old_props.update(props)
- props = old_props
- props = {key: value for key, value in props.items() if value}
- with self._atomic_write(self._props_path, "w+") as prop:
- json.dump(props, prop)
- @property
- def last_modified(self):
- last = max([os.path.getmtime(self._filesystem_path)] + [
- os.path.getmtime(os.path.join(self._filesystem_path, filename))
- for filename in os.listdir(self._filesystem_path)] or [0])
- return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last))
- def serialize(self):
- if not os.path.exists(self._filesystem_path):
- return None
- items = []
- for href in os.listdir(self._filesystem_path):
- if not is_safe_filesystem_path_component(href):
- self.logger.debug("Skipping component: %s", href)
- continue
- path = os.path.join(self._filesystem_path, href)
- if os.path.isfile(path):
- self.logger.debug("Read object: %s", path)
- with open(path, encoding=self.encoding, newline="") as fd:
- items.append(vobject.readOne(fd.read()))
- if self.get_meta("tag") == "VCALENDAR":
- collection = vobject.iCalendar()
- for item in items:
- for content in ("vevent", "vtodo", "vjournal"):
- if content in item.contents:
- for item_part in getattr(item, "%s_list" % content):
- collection.add(item_part)
- break
- return collection.serialize()
- elif self.get_meta("tag") == "VADDRESSBOOK":
- return "".join([item.serialize() for item in items])
- return ""
- _lock = threading.Lock()
- _waiters = []
- _lock_file = None
- _lock_file_locked = False
- _readers = 0
- _writer = False
- @classmethod
- @contextmanager
- def acquire_lock(cls, mode, user=None):
- def condition():
- if mode == "r":
- return not cls._writer
- else:
- return not cls._writer and cls._readers == 0
- folder = os.path.expanduser(cls.configuration.get(
- "storage", "filesystem_folder"))
- # Use a primitive lock which only works within one process as a
- # precondition for inter-process file-based locking
- with cls._lock:
- if cls._waiters or not condition():
- # Use FIFO for access requests
- waiter = threading.Condition(lock=cls._lock)
- cls._waiters.append(waiter)
- while True:
- waiter.wait()
- if condition():
- break
- cls._waiters.pop(0)
- if mode == "r":
- cls._readers += 1
- # Notify additional potential readers
- if cls._waiters:
- cls._waiters[0].notify()
- else:
- cls._writer = True
- if not cls._lock_file:
- cls._makedirs_synced(folder)
- lock_path = os.path.join(folder, ".Radicale.lock")
- cls._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")
- if not cls._lock_file_locked:
- if os.name == "nt":
- handle = msvcrt.get_osfhandle(cls._lock_file.fileno())
- flags = LOCKFILE_EXCLUSIVE_LOCK if mode == "w" else 0
- overlapped = Overlapped()
- if not lock_file_ex(handle, flags, 0, 1, 0, overlapped):
- cls.logger.debug("Locking not supported")
- elif os.name == "posix":
- _cmd = fcntl.LOCK_EX if mode == "w" else fcntl.LOCK_SH
- try:
- fcntl.flock(cls._lock_file.fileno(), _cmd)
- except OSError:
- cls.logger.debug("Locking not supported")
- cls._lock_file_locked = True
- try:
- yield
- # execute hook
- hook = cls.configuration.get("storage", "hook")
- if mode == "w" and hook:
- cls.logger.debug("Running hook")
- subprocess.check_call(
- hook % {"user": shlex.quote(user or "Anonymous")},
- shell=True, cwd=folder)
- finally:
- with cls._lock:
- if mode == "r":
- cls._readers -= 1
- else:
- cls._writer = False
- if cls._readers == 0:
- if os.name == "nt":
- handle = msvcrt.get_osfhandle(cls._lock_file.fileno())
- overlapped = Overlapped()
- if not unlock_file_ex(handle, 0, 1, 0, overlapped):
- cls.logger.debug("Unlocking not supported")
- elif os.name == "posix":
- try:
- fcntl.flock(cls._lock_file.fileno(), fcntl.LOCK_UN)
- except OSError:
- cls.logger.debug("Unlocking not supported")
- cls._lock_file_locked = False
- if cls._waiters:
- cls._waiters[0].notify()
- if (cls.configuration.getboolean(
- "storage", "filesystem_close_lock_file")
- and cls._readers == 0 and not cls._waiters):
- cls._lock_file.close()
- cls._lock_file = None
|