upload.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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-2022 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. import errno
  20. import os
  21. import pickle
  22. import sys
  23. from typing import Iterable, Iterator, TextIO, cast
  24. import radicale.item as radicale_item
  25. from radicale import pathutils
  26. from radicale.log import logger
  27. from radicale.storage.multifilesystem.base import CollectionBase
  28. from radicale.storage.multifilesystem.cache import CollectionPartCache
  29. from radicale.storage.multifilesystem.get import CollectionPartGet
  30. from radicale.storage.multifilesystem.history import CollectionPartHistory
  31. class CollectionPartUpload(CollectionPartGet, CollectionPartCache,
  32. CollectionPartHistory, CollectionBase):
  33. def upload(self, href: str, item: radicale_item.Item
  34. ) -> radicale_item.Item:
  35. if not pathutils.is_safe_filesystem_path_component(href):
  36. raise pathutils.UnsafePathError(href)
  37. path = pathutils.path_to_filesystem(self._filesystem_path, href)
  38. try:
  39. with self._atomic_write(path, newline="") as fo: # type: ignore
  40. f = cast(TextIO, fo)
  41. f.write(item.serialize())
  42. except Exception as e:
  43. raise ValueError("Failed to store item %r in collection %r: %s" %
  44. (href, self.path, e)) from e
  45. # store cache file
  46. if self._storage._use_mtime_and_size_for_item_cache is True:
  47. cache_hash = self._item_cache_mtime_and_size(os.stat(path).st_size, os.stat(path).st_mtime_ns)
  48. if self._storage._debug_cache_actions is True:
  49. logger.debug("Item cache store for: %r with mtime and size %r", path, cache_hash)
  50. else:
  51. cache_hash = self._item_cache_hash(item.serialize().encode(self._encoding))
  52. if self._storage._debug_cache_actions is True:
  53. logger.debug("Item cache store for: %r with hash %r", path, cache_hash)
  54. try:
  55. self._store_item_cache(href, item, cache_hash)
  56. except Exception as e:
  57. raise ValueError("Failed to store item cache of %r in collection %r: %s" %
  58. (href, self.path, e)) from e
  59. # Track the change
  60. self._update_history_etag(href, item)
  61. self._clean_history()
  62. uploaded_item = self._get(href, verify_href=False)
  63. if uploaded_item is None:
  64. raise RuntimeError("Storage modified externally")
  65. return uploaded_item
  66. def _upload_all_nonatomic(self, items: Iterable[radicale_item.Item],
  67. suffix: str = "") -> None:
  68. """Upload a new set of items non-atomic"""
  69. def is_safe_free_href(href: str) -> bool:
  70. return (pathutils.is_safe_filesystem_path_component(href) and
  71. not os.path.lexists(
  72. os.path.join(self._filesystem_path, href)))
  73. def get_safe_free_hrefs(uid: str) -> Iterator[str]:
  74. for href in [uid if uid.lower().endswith(suffix.lower())
  75. else uid + suffix,
  76. radicale_item.get_etag(uid).strip('"') + suffix]:
  77. if is_safe_free_href(href):
  78. yield href
  79. yield radicale_item.find_available_uid(
  80. lambda href: not is_safe_free_href(href), suffix)
  81. cache_folder = self._storage._get_collection_cache_subfolder(self._filesystem_path, ".Radicale.cache", "item")
  82. self._storage._makedirs_synced(cache_folder)
  83. for item in items:
  84. uid = item.uid
  85. logger.debug("Store item from list with uid: '%s'" % uid)
  86. cache_content = self._item_cache_content(item)
  87. for href in get_safe_free_hrefs(uid):
  88. path = os.path.join(self._filesystem_path, href)
  89. try:
  90. f = open(path,
  91. "w", newline="", encoding=self._encoding)
  92. except OSError as e:
  93. if (sys.platform != "win32" and e.errno == errno.EINVAL or
  94. sys.platform == "win32" and e.errno == 123):
  95. # not a valid filename
  96. continue
  97. raise
  98. break
  99. else:
  100. raise RuntimeError("No href found for item %r in temporary "
  101. "collection %r" % (uid, self.path))
  102. try:
  103. with f:
  104. f.write(item.serialize())
  105. f.flush()
  106. self._storage._fsync(f)
  107. except Exception as e:
  108. raise ValueError(
  109. "Failed to store item %r in temporary collection %r: %s" %
  110. (uid, self.path, e)) from e
  111. # store cache file
  112. if self._storage._use_mtime_and_size_for_item_cache is True:
  113. cache_hash = self._item_cache_mtime_and_size(os.stat(path).st_size, os.stat(path).st_mtime_ns)
  114. if self._storage._debug_cache_actions is True:
  115. logger.debug("Item cache store for: %r with mtime and size %r", path, cache_hash)
  116. else:
  117. cache_hash = self._item_cache_hash(item.serialize().encode(self._encoding))
  118. if self._storage._debug_cache_actions is True:
  119. logger.debug("Item cache store for: %r with hash %r", path, cache_hash)
  120. path_cache = os.path.join(cache_folder, href)
  121. if self._storage._debug_cache_actions is True:
  122. logger.debug("Item cache store into: %r", path_cache)
  123. with open(os.path.join(cache_folder, href), "wb") as fb:
  124. pickle.dump((cache_hash, *cache_content), fb)
  125. fb.flush()
  126. self._storage._fsync(fb)
  127. self._storage._sync_directory(cache_folder)
  128. self._storage._sync_directory(self._filesystem_path)