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
* Improve: [auth] ldap: do not read server info by bind to avoid needless network traffic
* 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
* 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
# Copyright © 2014 Jean-Marc Martins
# 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
# 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 os
import pathlib
import posixpath
import sys
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" %
(name, collection.path))
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 os
import platform
import select
import socket
import socketserver
@ -297,7 +298,7 @@ def serve(configuration: config.Configuration,
info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH")
else:
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
configuration = configuration.copy()
configuration.update({"server": {"_internal_server": "True"}}, "server",

View file

@ -29,7 +29,7 @@ import sys
import time
from typing import ClassVar, Iterator, Optional, Type
from radicale import config
from radicale import config, pathutils, utils
from radicale.log import logger
from radicale.storage.multifilesystem.base import CollectionBase, StorageBase
from radicale.storage.multifilesystem.cache import CollectionPartCache
@ -165,10 +165,12 @@ class Storage(
if not os.path.exists(self._filesystem_folder):
logger.warning("Storage location: %r does not exist, creating now", 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())
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())
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 'history': %s", self._use_cache_subfolder_for_history)
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))
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")
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:
logger.warning("Storage item mtime resolution test result not successful")
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()):
logger.warning("Storage cache subfolder: %r does not exist, creating now", 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 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
@ -161,7 +161,13 @@ class StorageBase(storage.BaseStorage):
# Create parent dirs recursively
self._makedirs_synced(parent_filesystem_path)
# 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)
if sys.platform != "win32" and self._folder_umask:
os.umask(oldmask)

View file

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import os
import ssl
import sys
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.log import logger
if sys.platform != "win32":
import grp
import pwd
_T_co = TypeVar("_T_co", covariant=True)
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):
protocols.append("TLSv1.3")
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