upload.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  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 errno
  19. import os
  20. import pickle
  21. import sys
  22. from typing import Iterable, Iterator, TextIO, cast
  23. import radicale.item as radicale_item
  24. from radicale import pathutils
  25. from radicale.storage.multifilesystem.base import CollectionBase
  26. from radicale.storage.multifilesystem.cache import CollectionPartCache
  27. from radicale.storage.multifilesystem.get import CollectionPartGet
  28. from radicale.storage.multifilesystem.history import CollectionPartHistory
  29. class CollectionPartUpload(CollectionPartGet, CollectionPartCache,
  30. CollectionPartHistory, CollectionBase):
  31. def upload(self, href: str, item: radicale_item.Item
  32. ) -> radicale_item.Item:
  33. if not pathutils.is_safe_filesystem_path_component(href):
  34. raise pathutils.UnsafePathError(href)
  35. try:
  36. self._store_item_cache(href, item)
  37. except Exception as e:
  38. raise ValueError("Failed to store item %r in collection %r: %s" %
  39. (href, self.path, e)) from e
  40. path = pathutils.path_to_filesystem(self._filesystem_path, href)
  41. # TODO: better fix for "mypy"
  42. with self._atomic_write(path, newline="") as fo: # type: ignore
  43. f = cast(TextIO, fo)
  44. f.write(item.serialize())
  45. # Clean the cache after the actual item is stored, or the cache entry
  46. # will be removed again.
  47. self._clean_item_cache()
  48. # Track the change
  49. self._update_history_etag(href, item)
  50. self._clean_history()
  51. uploaded_item = self._get(href, verify_href=False)
  52. if uploaded_item is None:
  53. raise RuntimeError("Storage modified externally")
  54. return uploaded_item
  55. def _upload_all_nonatomic(self, items: Iterable[radicale_item.Item],
  56. suffix: str = "") -> None:
  57. """Upload a new set of items non-atomic"""
  58. def is_safe_free_href(href: str) -> bool:
  59. return (pathutils.is_safe_filesystem_path_component(href) and
  60. not os.path.lexists(
  61. os.path.join(self._filesystem_path, href)))
  62. def get_safe_free_hrefs(uid: str) -> Iterator[str]:
  63. for href in [uid if uid.lower().endswith(suffix.lower())
  64. else uid + suffix,
  65. radicale_item.get_etag(uid).strip('"') + suffix]:
  66. if is_safe_free_href(href):
  67. yield href
  68. yield radicale_item.find_available_uid(
  69. lambda href: not is_safe_free_href(href), suffix)
  70. cache_folder = os.path.join(self._filesystem_path,
  71. ".Radicale.cache", "item")
  72. self._storage._makedirs_synced(cache_folder)
  73. for item in items:
  74. uid = item.uid
  75. try:
  76. cache_content = self._item_cache_content(item)
  77. except Exception as e:
  78. raise ValueError(
  79. "Failed to store item %r in temporary collection %r: %s" %
  80. (uid, self.path, e)) from e
  81. for href in get_safe_free_hrefs(uid):
  82. try:
  83. f = open(os.path.join(self._filesystem_path, href),
  84. "w", newline="", encoding=self._encoding)
  85. except OSError as e:
  86. if (sys.platform != "win32" and e.errno == errno.EINVAL or
  87. sys.platform == "win32" and e.errno == 123):
  88. # not a valid filename
  89. continue
  90. raise
  91. break
  92. else:
  93. raise RuntimeError("No href found for item %r in temporary "
  94. "collection %r" % (uid, self.path))
  95. with f:
  96. f.write(item.serialize())
  97. f.flush()
  98. self._storage._fsync(f)
  99. with open(os.path.join(cache_folder, href), "wb") as fb:
  100. pickle.dump(cache_content, fb)
  101. fb.flush()
  102. self._storage._fsync(fb)
  103. self._storage._sync_directory(cache_folder)
  104. self._storage._sync_directory(self._filesystem_path)