mirror of
https://github.com/Kozea/Radicale.git
synced 2025-06-26 16:45:52 +00:00
Synced with origin
This commit is contained in:
parent
9b3bb2de2b
commit
cf81d1f9a7
94 changed files with 5096 additions and 3560 deletions
|
@ -1,4 +1,4 @@
|
|||
# This file is part of Radicale Server - Calendar Server
|
||||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2014 Jean-Marc Martins
|
||||
# Copyright © 2012-2017 Guillaume Ayoub
|
||||
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
|
||||
|
@ -21,17 +21,23 @@ Helper functions for working with the file system.
|
|||
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
import threading
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Iterator, Type, Union
|
||||
|
||||
if os.name == "nt":
|
||||
from radicale import storage, types
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import msvcrt
|
||||
|
||||
LOCKFILE_EXCLUSIVE_LOCK = 2
|
||||
LOCKFILE_EXCLUSIVE_LOCK: int = 2
|
||||
ULONG_PTR: Union[Type[ctypes.c_uint32], Type[ctypes.c_uint64]]
|
||||
if ctypes.sizeof(ctypes.c_void_p) == 4:
|
||||
ULONG_PTR = ctypes.c_uint32
|
||||
else:
|
||||
|
@ -45,7 +51,8 @@ if os.name == "nt":
|
|||
("offset_high", ctypes.wintypes.DWORD),
|
||||
("h_event", ctypes.wintypes.HANDLE)]
|
||||
|
||||
lock_file_ex = ctypes.windll.kernel32.LockFileEx
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
lock_file_ex = kernel32.LockFileEx
|
||||
lock_file_ex.argtypes = [
|
||||
ctypes.wintypes.HANDLE,
|
||||
ctypes.wintypes.DWORD,
|
||||
|
@ -54,7 +61,7 @@ if os.name == "nt":
|
|||
ctypes.wintypes.DWORD,
|
||||
ctypes.POINTER(Overlapped)]
|
||||
lock_file_ex.restype = ctypes.wintypes.BOOL
|
||||
unlock_file_ex = ctypes.windll.kernel32.UnlockFileEx
|
||||
unlock_file_ex = kernel32.UnlockFileEx
|
||||
unlock_file_ex.argtypes = [
|
||||
ctypes.wintypes.HANDLE,
|
||||
ctypes.wintypes.DWORD,
|
||||
|
@ -62,21 +69,46 @@ if os.name == "nt":
|
|||
ctypes.wintypes.DWORD,
|
||||
ctypes.POINTER(Overlapped)]
|
||||
unlock_file_ex.restype = ctypes.wintypes.BOOL
|
||||
elif os.name == "posix":
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
if sys.platform == "linux":
|
||||
import ctypes
|
||||
|
||||
RENAME_EXCHANGE: int = 2
|
||||
renameat2 = None
|
||||
try:
|
||||
renameat2 = ctypes.CDLL(None, use_errno=True).renameat2
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
renameat2.argtypes = [
|
||||
ctypes.c_int, ctypes.c_char_p,
|
||||
ctypes.c_int, ctypes.c_char_p,
|
||||
ctypes.c_uint]
|
||||
renameat2.restype = ctypes.c_int
|
||||
|
||||
if sys.platform == "darwin":
|
||||
# Definition missing in PyPy
|
||||
F_FULLFSYNC: int = getattr(fcntl, "F_FULLFSYNC", 51)
|
||||
|
||||
|
||||
class RwLock:
|
||||
"""A readers-Writer lock that locks a file."""
|
||||
|
||||
def __init__(self, path):
|
||||
_path: str
|
||||
_readers: int
|
||||
_writer: bool
|
||||
_lock: threading.Lock
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
self._path = path
|
||||
self._readers = 0
|
||||
self._writer = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def locked(self):
|
||||
def locked(self) -> str:
|
||||
with self._lock:
|
||||
if self._readers > 0:
|
||||
return "r"
|
||||
|
@ -84,28 +116,28 @@ class RwLock:
|
|||
return "w"
|
||||
return ""
|
||||
|
||||
@contextlib.contextmanager
|
||||
def acquire(self, mode):
|
||||
@types.contextmanager
|
||||
def acquire(self, mode: str) -> Iterator[None]:
|
||||
if mode not in "rw":
|
||||
raise ValueError("Invalid mode: %r" % mode)
|
||||
with open(self._path, "w+") as lock_file:
|
||||
if os.name == "nt":
|
||||
if sys.platform == "win32":
|
||||
handle = msvcrt.get_osfhandle(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):
|
||||
raise RuntimeError("Locking the storage failed: %s" %
|
||||
ctypes.FormatError())
|
||||
elif os.name == "posix":
|
||||
try:
|
||||
if not lock_file_ex(handle, flags, 0, 1, 0, overlapped):
|
||||
raise ctypes.WinError()
|
||||
except OSError as e:
|
||||
raise RuntimeError("Locking the storage failed: %s" % e
|
||||
) from e
|
||||
else:
|
||||
_cmd = fcntl.LOCK_EX if mode == "w" else fcntl.LOCK_SH
|
||||
try:
|
||||
fcntl.flock(lock_file.fileno(), _cmd)
|
||||
except OSError as e:
|
||||
raise RuntimeError("Locking the storage failed: %s" %
|
||||
e) from e
|
||||
else:
|
||||
raise RuntimeError("Locking the storage failed: "
|
||||
"Unsupported operating system")
|
||||
raise RuntimeError("Locking the storage failed: %s" % e
|
||||
) from e
|
||||
with self._lock:
|
||||
if self._writer or mode == "w" and self._readers != 0:
|
||||
raise RuntimeError("Locking the storage failed: "
|
||||
|
@ -123,19 +155,65 @@ class RwLock:
|
|||
self._writer = False
|
||||
|
||||
|
||||
def fsync(fd):
|
||||
if os.name == "posix" and hasattr(fcntl, "F_FULLFSYNC"):
|
||||
fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
|
||||
else:
|
||||
os.fsync(fd)
|
||||
def rename_exchange(src: str, dst: str) -> None:
|
||||
"""Exchange the files or directories `src` and `dst`.
|
||||
|
||||
Both `src` and `dst` must exist but may be of different types.
|
||||
|
||||
On Linux with renameat2 the operation is atomic.
|
||||
On other platforms it's not atomic.
|
||||
|
||||
"""
|
||||
src_dir, src_base = os.path.split(src)
|
||||
dst_dir, dst_base = os.path.split(dst)
|
||||
src_dir = src_dir or os.curdir
|
||||
dst_dir = dst_dir or os.curdir
|
||||
if not src_base or not dst_base:
|
||||
raise ValueError("Invalid arguments: %r -> %r" % (src, dst))
|
||||
if sys.platform == "linux" and renameat2:
|
||||
src_base_bytes = os.fsencode(src_base)
|
||||
dst_base_bytes = os.fsencode(dst_base)
|
||||
src_dir_fd = os.open(src_dir, 0)
|
||||
try:
|
||||
dst_dir_fd = os.open(dst_dir, 0)
|
||||
try:
|
||||
if renameat2(src_dir_fd, src_base_bytes,
|
||||
dst_dir_fd, dst_base_bytes,
|
||||
RENAME_EXCHANGE) == 0:
|
||||
return
|
||||
errno_ = ctypes.get_errno()
|
||||
# Fallback if RENAME_EXCHANGE not supported by filesystem
|
||||
if errno_ != errno.EINVAL:
|
||||
raise OSError(errno_, os.strerror(errno_))
|
||||
finally:
|
||||
os.close(dst_dir_fd)
|
||||
finally:
|
||||
os.close(src_dir_fd)
|
||||
with TemporaryDirectory(prefix=".Radicale.tmp-", dir=src_dir
|
||||
) as tmp_dir:
|
||||
os.rename(dst, os.path.join(tmp_dir, "interim"))
|
||||
os.rename(src, dst)
|
||||
os.rename(os.path.join(tmp_dir, "interim"), src)
|
||||
|
||||
|
||||
def strip_path(path):
|
||||
def fsync(fd: int) -> None:
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
fcntl.fcntl(fd, F_FULLFSYNC)
|
||||
return
|
||||
except OSError as e:
|
||||
# Fallback if F_FULLFSYNC not supported by filesystem
|
||||
if e.errno != errno.EINVAL:
|
||||
raise
|
||||
os.fsync(fd)
|
||||
|
||||
|
||||
def strip_path(path: str) -> str:
|
||||
assert sanitize_path(path) == path
|
||||
return path.strip("/")
|
||||
|
||||
|
||||
def unstrip_path(stripped_path, trailing_slash=False):
|
||||
def unstrip_path(stripped_path: str, trailing_slash: bool = False) -> str:
|
||||
assert strip_path(sanitize_path(stripped_path)) == stripped_path
|
||||
assert stripped_path or trailing_slash
|
||||
path = "/%s" % stripped_path
|
||||
|
@ -144,7 +222,7 @@ def unstrip_path(stripped_path, trailing_slash=False):
|
|||
return path
|
||||
|
||||
|
||||
def sanitize_path(path):
|
||||
def sanitize_path(path: str) -> str:
|
||||
"""Make path absolute with leading slash to prevent access to other data.
|
||||
|
||||
Preserve potential trailing slash.
|
||||
|
@ -161,16 +239,16 @@ def sanitize_path(path):
|
|||
return new_path + trailing_slash
|
||||
|
||||
|
||||
def is_safe_path_component(path):
|
||||
def is_safe_path_component(path: str) -> bool:
|
||||
"""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 (".", "..")
|
||||
return bool(path) and "/" not in path and path not in (".", "..")
|
||||
|
||||
|
||||
def is_safe_filesystem_path_component(path):
|
||||
def is_safe_filesystem_path_component(path: str) -> bool:
|
||||
"""Check if path is a single component of a local and posix filesystem
|
||||
path.
|
||||
|
||||
|
@ -178,13 +256,14 @@ def is_safe_filesystem_path_component(path):
|
|||
|
||||
"""
|
||||
return (
|
||||
path and not os.path.splitdrive(path)[0] and
|
||||
bool(path) and not os.path.splitdrive(path)[0] and
|
||||
(sys.platform != "win32" or ":" not in path) and # Block NTFS-ADS
|
||||
not os.path.split(path)[0] and path not in (os.curdir, os.pardir) and
|
||||
not path.startswith(".") and not path.endswith("~") and
|
||||
is_safe_path_component(path))
|
||||
|
||||
|
||||
def path_to_filesystem(root, sane_path):
|
||||
def path_to_filesystem(root: str, sane_path: str) -> str:
|
||||
"""Convert `sane_path` to a local filesystem path relative to `root`.
|
||||
|
||||
`root` must be a secure filesystem path, it will be prepend to the path.
|
||||
|
@ -206,25 +285,25 @@ def path_to_filesystem(root, sane_path):
|
|||
# Check for conflicting files (e.g. case-insensitive file systems
|
||||
# or short names on Windows file systems)
|
||||
if (os.path.lexists(safe_path) and
|
||||
part not in (e.name for e in
|
||||
os.scandir(safe_path_parent))):
|
||||
part not in (e.name for e in os.scandir(safe_path_parent))):
|
||||
raise CollidingPathError(part)
|
||||
return safe_path
|
||||
|
||||
|
||||
class UnsafePathError(ValueError):
|
||||
def __init__(self, path):
|
||||
message = "Can't translate name safely to filesystem: %r" % path
|
||||
super().__init__(message)
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
super().__init__("Can't translate name safely to filesystem: %r" %
|
||||
path)
|
||||
|
||||
|
||||
class CollidingPathError(ValueError):
|
||||
def __init__(self, path):
|
||||
message = "File name collision: %r" % path
|
||||
super().__init__(message)
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
super().__init__("File name collision: %r" % path)
|
||||
|
||||
|
||||
def name_from_path(path, collection):
|
||||
def name_from_path(path: str, collection: "storage.BaseCollection") -> str:
|
||||
"""Return Radicale item name from ``path``."""
|
||||
assert sanitize_path(path) == path
|
||||
start = unstrip_path(collection.path, True)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue