1
0

get.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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-2018 Unrud <unrud@outlook.com>
  5. #
  6. # This library is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  18. import os
  19. import sys
  20. import time
  21. from typing import Iterable, Iterator, Optional, Tuple
  22. import radicale.item as radicale_item
  23. from radicale import pathutils
  24. from radicale.log import logger
  25. from radicale.storage import multifilesystem
  26. from radicale.storage.multifilesystem.base import CollectionBase
  27. from radicale.storage.multifilesystem.cache import CollectionPartCache
  28. from radicale.storage.multifilesystem.lock import CollectionPartLock
  29. class CollectionPartGet(CollectionPartCache, CollectionPartLock,
  30. CollectionBase):
  31. _item_cache_cleaned: bool
  32. def __init__(self, storage_: "multifilesystem.Storage", path: str,
  33. filesystem_path: Optional[str] = None) -> None:
  34. super().__init__(storage_, path, filesystem_path)
  35. self._item_cache_cleaned = False
  36. def _list(self) -> Iterator[str]:
  37. for entry in os.scandir(self._filesystem_path):
  38. if not entry.is_file():
  39. continue
  40. href = entry.name
  41. if not pathutils.is_safe_filesystem_path_component(href):
  42. if not href.startswith(".Radicale"):
  43. logger.debug("Skipping item %r in %r", href, self.path)
  44. continue
  45. yield href
  46. def _get(self, href: str, verify_href: bool = True
  47. ) -> Optional[radicale_item.Item]:
  48. if verify_href:
  49. try:
  50. if not pathutils.is_safe_filesystem_path_component(href):
  51. raise pathutils.UnsafePathError(href)
  52. path = pathutils.path_to_filesystem(self._filesystem_path,
  53. href)
  54. except ValueError as e:
  55. logger.debug(
  56. "Can't translate name %r safely to filesystem in %r: %s",
  57. href, self.path, e, exc_info=True)
  58. return None
  59. else:
  60. path = os.path.join(self._filesystem_path, href)
  61. try:
  62. with open(path, "rb") as f:
  63. raw_text = f.read()
  64. except (FileNotFoundError, IsADirectoryError):
  65. return None
  66. except PermissionError:
  67. # Windows raises ``PermissionError`` when ``path`` is a directory
  68. if (sys.platform == "win32" and
  69. os.path.isdir(path) and os.access(path, os.R_OK)):
  70. return None
  71. raise
  72. # The hash of the component in the file system. This is used to check,
  73. # if the entry in the cache is still valid.
  74. cache_hash = self._item_cache_hash(raw_text)
  75. cache_content = self._load_item_cache(href, cache_hash)
  76. if cache_content is None:
  77. with self._acquire_cache_lock("item"):
  78. # Lock the item cache to prevent multpile processes from
  79. # generating the same data in parallel.
  80. # This improves the performance for multiple requests.
  81. if self._storage._lock.locked == "r":
  82. # Check if another process created the file in the meantime
  83. cache_content = self._load_item_cache(href, cache_hash)
  84. if cache_content is None:
  85. try:
  86. vobject_items = radicale_item.read_components(
  87. raw_text.decode(self._encoding))
  88. radicale_item.check_and_sanitize_items(
  89. vobject_items, tag=self.tag)
  90. vobject_item, = vobject_items
  91. temp_item = radicale_item.Item(
  92. collection=self, vobject_item=vobject_item)
  93. cache_content = self._store_item_cache(
  94. href, temp_item, cache_hash)
  95. except Exception as e:
  96. raise RuntimeError("Failed to load item %r in %r: %s" %
  97. (href, self.path, e)) from e
  98. # Clean cache entries once after the data in the file
  99. # system was edited externally.
  100. if not self._item_cache_cleaned:
  101. self._item_cache_cleaned = True
  102. self._clean_item_cache()
  103. last_modified = time.strftime(
  104. "%a, %d %b %Y %H:%M:%S GMT",
  105. time.gmtime(os.path.getmtime(path)))
  106. # Don't keep reference to ``vobject_item``, because it requires a lot
  107. # of memory.
  108. return radicale_item.Item(
  109. collection=self, href=href, last_modified=last_modified,
  110. etag=cache_content.etag, text=cache_content.text,
  111. uid=cache_content.uid, name=cache_content.name,
  112. component_name=cache_content.tag,
  113. time_range=(cache_content.start, cache_content.end))
  114. def get_multi(self, hrefs: Iterable[str]
  115. ) -> Iterator[Tuple[str, Optional[radicale_item.Item]]]:
  116. # It's faster to check for file name collissions here, because
  117. # we only need to call os.listdir once.
  118. files = None
  119. for href in hrefs:
  120. if files is None:
  121. # List dir after hrefs returned one item, the iterator may be
  122. # empty and the for-loop is never executed.
  123. files = os.listdir(self._filesystem_path)
  124. path = os.path.join(self._filesystem_path, href)
  125. if (not pathutils.is_safe_filesystem_path_component(href) or
  126. href not in files and os.path.lexists(path)):
  127. logger.debug("Can't translate name safely to filesystem: %r",
  128. href)
  129. yield (href, None)
  130. else:
  131. yield (href, self._get(href, verify_href=False))
  132. def get_all(self) -> Iterator[radicale_item.Item]:
  133. for href in self._list():
  134. # We don't need to check for collissions, because the file names
  135. # are from os.listdir.
  136. item = self._get(href, verify_href=False)
  137. if item is not None:
  138. yield item