__init__.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2014 Jean-Marc Martins
  3. # Copyright © 2012-2017 Guillaume Ayoub
  4. # Copyright © 2017-2021 Unrud <unrud@outlook.com>
  5. # Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
  6. #
  7. # This library is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  19. """
  20. Storage backend that stores data in the file system.
  21. Uses one folder per collection and one file per collection entry.
  22. """
  23. import os
  24. import sys
  25. import time
  26. from typing import ClassVar, Iterator, Optional, Type
  27. from radicale import config
  28. from radicale.log import logger
  29. from radicale.storage.multifilesystem.base import CollectionBase, StorageBase
  30. from radicale.storage.multifilesystem.cache import CollectionPartCache
  31. from radicale.storage.multifilesystem.create_collection import \
  32. StoragePartCreateCollection
  33. from radicale.storage.multifilesystem.delete import CollectionPartDelete
  34. from radicale.storage.multifilesystem.discover import StoragePartDiscover
  35. from radicale.storage.multifilesystem.get import CollectionPartGet
  36. from radicale.storage.multifilesystem.history import CollectionPartHistory
  37. from radicale.storage.multifilesystem.lock import (CollectionPartLock,
  38. StoragePartLock)
  39. from radicale.storage.multifilesystem.meta import CollectionPartMeta
  40. from radicale.storage.multifilesystem.move import StoragePartMove
  41. from radicale.storage.multifilesystem.sync import CollectionPartSync
  42. from radicale.storage.multifilesystem.upload import CollectionPartUpload
  43. from radicale.storage.multifilesystem.verify import StoragePartVerify
  44. # 999 second, 999 ms, 999 us, 999 ns
  45. MTIME_NS_TEST: int = 999999999999
  46. class Collection(
  47. CollectionPartDelete, CollectionPartMeta, CollectionPartSync,
  48. CollectionPartUpload, CollectionPartGet, CollectionPartCache,
  49. CollectionPartLock, CollectionPartHistory, CollectionBase):
  50. _etag_cache: Optional[str]
  51. def __init__(self, storage_: "Storage", path: str,
  52. filesystem_path: Optional[str] = None) -> None:
  53. super().__init__(storage_, path, filesystem_path)
  54. self._etag_cache = None
  55. @property
  56. def path(self) -> str:
  57. return self._path
  58. @property
  59. def last_modified(self) -> str:
  60. def relevant_files_iter() -> Iterator[str]:
  61. yield self._filesystem_path
  62. if os.path.exists(self._props_path):
  63. yield self._props_path
  64. for href in self._list():
  65. yield os.path.join(self._filesystem_path, href)
  66. last = max(map(os.path.getmtime, relevant_files_iter()))
  67. return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last))
  68. @property
  69. def etag(self) -> str:
  70. # reuse cached value if the storage is read-only
  71. if self._storage._lock.locked == "w" or self._etag_cache is None:
  72. self._etag_cache = super().etag
  73. return self._etag_cache
  74. class Storage(
  75. StoragePartCreateCollection, StoragePartLock, StoragePartMove,
  76. StoragePartVerify, StoragePartDiscover, StorageBase):
  77. _collection_class: ClassVar[Type[Collection]] = Collection
  78. def _analyse_mtime(self):
  79. # calculate and display mtime resolution
  80. path = os.path.join(self._filesystem_folder, ".Radicale.mtime_test")
  81. try:
  82. with open(path, "w") as f:
  83. f.write("mtime_test")
  84. f.close
  85. except Exception as e:
  86. logger.error("Storage item mtime resolution test not possible, cannot write file: %r (%s)", path, e)
  87. raise
  88. # set mtime_ns for tests
  89. os.utime(path, times=None, ns=(MTIME_NS_TEST, MTIME_NS_TEST))
  90. logger.debug("Storage item mtime resoultion test set: %d" % MTIME_NS_TEST)
  91. mtime_ns = os.stat(path).st_mtime_ns
  92. logger.debug("Storage item mtime resoultion test get: %d" % mtime_ns)
  93. # start analysis
  94. precision = 1
  95. mtime_ns_test = MTIME_NS_TEST
  96. while mtime_ns > 0:
  97. if mtime_ns == mtime_ns_test:
  98. break
  99. factor = 2
  100. if int(mtime_ns / factor) == int(mtime_ns_test / factor):
  101. precision = precision * factor
  102. break
  103. factor = 5
  104. if int(mtime_ns / factor) == int(mtime_ns_test / factor):
  105. precision = precision * factor
  106. break
  107. precision = precision * 10
  108. mtime_ns = int(mtime_ns / 10)
  109. mtime_ns_test = int(mtime_ns_test / 10)
  110. unit = "ns"
  111. precision_unit = precision
  112. if precision >= 1000000000:
  113. precision_unit = int(precision / 1000000000)
  114. unit = "s"
  115. elif precision >= 1000000:
  116. precision_unit = int(precision / 1000000)
  117. unit = "ms"
  118. elif precision >= 1000:
  119. precision_unit = int(precision / 1000)
  120. unit = "us"
  121. os.remove(path)
  122. return (precision, precision_unit, unit)
  123. def __init__(self, configuration: config.Configuration) -> None:
  124. super().__init__(configuration)
  125. logger.info("Storage location: %r", self._filesystem_folder)
  126. self._makedirs_synced(self._filesystem_folder)
  127. logger.info("Storage location subfolder: %r", self._get_collection_root_folder())
  128. logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item)
  129. logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history)
  130. logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken)
  131. logger.info("Storage cache use mtime and size for 'item': %s", self._use_mtime_and_size_for_item_cache)
  132. (precision, precision_unit, unit) = self._analyse_mtime()
  133. if precision >= 100000000:
  134. # >= 100 ms
  135. logger.warning("Storage item mtime resolution test result: %d %s (VERY RISKY ON PRODUCTION SYSTEMS)" % (precision_unit, unit))
  136. elif precision >= 10000000:
  137. # >= 10 ms
  138. logger.warning("Storage item mtime resolution test result: %d %s (RISKY ON PRODUCTION SYSTEMS)" % (precision_unit, unit))
  139. else:
  140. logger.info("Storage item mtime resolution test result: %d %s" % (precision_unit, unit))
  141. if self._use_mtime_and_size_for_item_cache is False:
  142. logger.info("Storage cache using mtime and size for 'item' may be an option in case of performance issues")
  143. logger.debug("Storage cache action logging: %s", self._debug_cache_actions)
  144. if self._use_cache_subfolder_for_item is True or self._use_cache_subfolder_for_history is True or self._use_cache_subfolder_for_synctoken is True:
  145. logger.info("Storage cache subfolder: %r", self._get_collection_cache_folder())
  146. self._makedirs_synced(self._get_collection_cache_folder())
  147. if sys.platform != "win32":
  148. if not self._folder_umask:
  149. # retrieve current umask by setting a dummy umask
  150. current_umask = os.umask(0o0022)
  151. logger.info("Storage folder umask (from system): '%04o'", current_umask)
  152. # reset to original
  153. os.umask(current_umask)
  154. else:
  155. try:
  156. config_umask = int(self._folder_umask, 8)
  157. except Exception:
  158. logger.critical("storage folder umask defined but invalid: '%s'", self._folder_umask)
  159. raise
  160. logger.info("storage folder umask defined: '%04o'", config_umask)
  161. self._config_umask = config_umask