1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-08-04 18:22:26 +00:00

Merge pull request #1814 from pbiering/improve-writeability-check-on-startup

Improve writeability check on startup
This commit is contained in:
Peter Bieringer 2025-06-29 21:15:45 +02:00 committed by GitHub
commit 1c97345fc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 61 additions and 5 deletions

View file

@ -3,6 +3,8 @@
## 3.5.5.dev ## 3.5.5.dev
* Improve: [auth] ldap: do not read server info by bind to avoid needless network traffic * Improve: [auth] ldap: do not read server info by bind to avoid needless network traffic
* Fix: [storage] broken support of 'folder_umask' * Fix: [storage] broken support of 'folder_umask'
* Improve: add details about platform and effective user on startup
* Improve: display owner+permissions on directories on startup, extend error message in case of missing permissions
## 3.5.4 ## 3.5.4
* Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute) * Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)

View file

@ -1,7 +1,8 @@
# This file is part of Radicale - CalDAV and CardDAV server # This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2014 Jean-Marc Martins # Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <unrud@outlook.com> # Copyright © 2017-2022 Unrud <unrud@outlook.com>
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -23,6 +24,7 @@ Helper functions for working with the file system.
import errno import errno
import os import os
import pathlib
import posixpath import posixpath
import sys import sys
import threading import threading
@ -314,3 +316,17 @@ def name_from_path(path: str, collection: "storage.BaseCollection") -> str:
raise ValueError("%r is not a component in collection %r" % raise ValueError("%r is not a component in collection %r" %
(name, collection.path)) (name, collection.path))
return name return name
def path_permissions(path):
path = pathlib.Path(path)
return [path.owner(), path.group(), path.stat().st_mode]
def path_permissions_as_string(path):
try:
pp = path_permissions(path)
s = "path=%r owner=%s group=%s mode=%o" % (path, pp[0], pp[1], pp[2])
except NotImplementedError:
s = "path=%r owner=UNKNOWN(unsupported on this system)" % (path)
return s

View file

@ -25,6 +25,7 @@ Built-in WSGI server.
import http import http
import os import os
import platform
import select import select
import socket import socket
import socketserver import socketserver
@ -297,7 +298,7 @@ def serve(configuration: config.Configuration,
info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH") info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH")
else: else:
info = "" info = ""
logger.info("Starting Radicale %s(%s)", info, utils.packages_version()) logger.info("Starting Radicale %s(%s) as %s on %s", info, utils.packages_version(), utils.user_groups_as_string(), platform.platform())
# Copy configuration before modifying # Copy configuration before modifying
configuration = configuration.copy() configuration = configuration.copy()
configuration.update({"server": {"_internal_server": "True"}}, "server", configuration.update({"server": {"_internal_server": "True"}}, "server",

View file

@ -29,7 +29,7 @@ import sys
import time import time
from typing import ClassVar, Iterator, Optional, Type from typing import ClassVar, Iterator, Optional, Type
from radicale import config from radicale import config, pathutils, utils
from radicale.log import logger from radicale.log import logger
from radicale.storage.multifilesystem.base import CollectionBase, StorageBase from radicale.storage.multifilesystem.base import CollectionBase, StorageBase
from radicale.storage.multifilesystem.cache import CollectionPartCache from radicale.storage.multifilesystem.cache import CollectionPartCache
@ -165,10 +165,12 @@ class Storage(
if not os.path.exists(self._filesystem_folder): if not os.path.exists(self._filesystem_folder):
logger.warning("Storage location: %r does not exist, creating now", self._filesystem_folder) logger.warning("Storage location: %r does not exist, creating now", self._filesystem_folder)
self._makedirs_synced(self._filesystem_folder) self._makedirs_synced(self._filesystem_folder)
logger.info("Storage location permissions: %s", pathutils.path_permissions_as_string(self._filesystem_folder))
logger.info("Storage location subfolder: %r", self._get_collection_root_folder()) logger.info("Storage location subfolder: %r", self._get_collection_root_folder())
if not os.path.exists(self._get_collection_root_folder()): if not os.path.exists(self._get_collection_root_folder()):
logger.warning("Storage location subfolder: %r does not exist, creating now", self._get_collection_root_folder()) logger.warning("Storage location subfolder: %r does not exist, creating now", self._get_collection_root_folder())
self._makedirs_synced(self._get_collection_root_folder()) self._makedirs_synced(self._get_collection_root_folder())
logger.info("Storage location subfolder permissions: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder()))
logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item) logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item)
logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history) logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history)
logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken) logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken)
@ -185,6 +187,9 @@ class Storage(
logger.info("Storage item mtime resolution test result: %d %s" % (precision_unit, unit)) logger.info("Storage item mtime resolution test result: %d %s" % (precision_unit, unit))
if self._use_mtime_and_size_for_item_cache is False: if self._use_mtime_and_size_for_item_cache is False:
logger.info("Storage cache using mtime and size for 'item' may be an option in case of performance issues") logger.info("Storage cache using mtime and size for 'item' may be an option in case of performance issues")
except PermissionError as e:
logger.error("Directory permissions: %s / Effective user: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder()), utils.user_groups_as_string())
raise e
except Exception: except Exception:
logger.warning("Storage item mtime resolution test result not successful") logger.warning("Storage item mtime resolution test result not successful")
logger.debug("Storage cache action logging: %s", self._debug_cache_actions) logger.debug("Storage cache action logging: %s", self._debug_cache_actions)
@ -193,3 +198,4 @@ class Storage(
if not os.path.exists(self._get_collection_cache_folder()): if not os.path.exists(self._get_collection_cache_folder()):
logger.warning("Storage cache subfolder: %r does not exist, creating now", self._get_collection_cache_folder()) logger.warning("Storage cache subfolder: %r does not exist, creating now", self._get_collection_cache_folder())
self._makedirs_synced(self._get_collection_cache_folder()) self._makedirs_synced(self._get_collection_cache_folder())
logger.info("Storage cache subfolder permissions: %s", pathutils.path_permissions_as_string(self._get_collection_cache_folder()))

View file

@ -22,7 +22,7 @@ import sys
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import IO, AnyStr, ClassVar, Iterator, Optional, Type from typing import IO, AnyStr, ClassVar, Iterator, Optional, Type
from radicale import config, pathutils, storage, types from radicale import config, logger, pathutils, storage, types, utils
from radicale.storage import multifilesystem # noqa:F401 from radicale.storage import multifilesystem # noqa:F401
@ -161,7 +161,13 @@ class StorageBase(storage.BaseStorage):
# Create parent dirs recursively # Create parent dirs recursively
self._makedirs_synced(parent_filesystem_path) self._makedirs_synced(parent_filesystem_path)
# Possible race! # Possible race!
os.makedirs(filesystem_path, exist_ok=True) try:
os.makedirs(filesystem_path, exist_ok=True)
except PermissionError as e:
logger.error("Directory permissions: %s / Effective user: %s", pathutils.path_permissions_as_string(parent_filesystem_path), utils.user_groups_as_string())
raise e
except Exception:
raise
self._sync_directory(parent_filesystem_path) self._sync_directory(parent_filesystem_path)
if sys.platform != "win32" and self._folder_umask: if sys.platform != "win32" and self._folder_umask:
os.umask(oldmask) os.umask(oldmask)

View file

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import os
import ssl import ssl
import sys import sys
from importlib import import_module, metadata from importlib import import_module, metadata
@ -25,6 +26,10 @@ from typing import Callable, Sequence, Tuple, Type, TypeVar, Union
from radicale import config from radicale import config
from radicale.log import logger from radicale.log import logger
if sys.platform != "win32":
import grp
import pwd
_T_co = TypeVar("_T_co", covariant=True) _T_co = TypeVar("_T_co", covariant=True)
RADICALE_MODULES: Sequence[str] = ("radicale", "vobject", "passlib", "defusedxml", RADICALE_MODULES: Sequence[str] = ("radicale", "vobject", "passlib", "defusedxml",
@ -214,3 +219,23 @@ def ssl_get_protocols(context):
if (context.minimum_version <= ssl.TLSVersion.TLSv1_3) and (context.maximum_version >= ssl.TLSVersion.TLSv1_3): if (context.minimum_version <= ssl.TLSVersion.TLSv1_3) and (context.maximum_version >= ssl.TLSVersion.TLSv1_3):
protocols.append("TLSv1.3") protocols.append("TLSv1.3")
return protocols return protocols
def user_groups_as_string():
if sys.platform != "win32":
euid = os.geteuid()
egid = os.getegid()
username = pwd.getpwuid(euid)[0]
gids = os.getgrouplist(username, egid)
groups = []
for gid in gids:
try:
gi = grp.getgrgid(gid)
groups.append("%s(%d)" % (gi.gr_name, gid))
except Exception:
groups.append("%s(%d)" % (gid, gid))
s = "user=%s(%d) groups=%s" % (username, euid, ','.join(groups))
else:
username = os.getlogin()
s = "user=%s" % (username)
return s