__init__.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # This file is part of Radicale Server - Calendar 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 contextlib
  19. import os
  20. import time
  21. from itertools import chain
  22. from tempfile import NamedTemporaryFile
  23. from radicale import pathutils, storage
  24. from radicale.storage.multifilesystem.cache import CollectionCacheMixin
  25. from radicale.storage.multifilesystem.create_collection import \
  26. CollectionCreateCollectionMixin
  27. from radicale.storage.multifilesystem.delete import CollectionDeleteMixin
  28. from radicale.storage.multifilesystem.discover import CollectionDiscoverMixin
  29. from radicale.storage.multifilesystem.get import CollectionGetMixin
  30. from radicale.storage.multifilesystem.history import CollectionHistoryMixin
  31. from radicale.storage.multifilesystem.lock import CollectionLockMixin
  32. from radicale.storage.multifilesystem.meta import CollectionMetaMixin
  33. from radicale.storage.multifilesystem.move import CollectionMoveMixin
  34. from radicale.storage.multifilesystem.sync import CollectionSyncMixin
  35. from radicale.storage.multifilesystem.upload import CollectionUploadMixin
  36. from radicale.storage.multifilesystem.verify import CollectionVerifyMixin
  37. class Collection(
  38. CollectionCacheMixin, CollectionCreateCollectionMixin,
  39. CollectionDeleteMixin, CollectionDiscoverMixin, CollectionGetMixin,
  40. CollectionHistoryMixin, CollectionLockMixin, CollectionMetaMixin,
  41. CollectionMoveMixin, CollectionSyncMixin, CollectionUploadMixin,
  42. CollectionVerifyMixin, storage.BaseCollection):
  43. """Collection stored in several files per calendar."""
  44. @classmethod
  45. def static_init(cls):
  46. folder = os.path.expanduser(cls.configuration.get(
  47. "storage", "filesystem_folder"))
  48. cls._makedirs_synced(folder)
  49. super().static_init()
  50. def __init__(self, path, filesystem_path=None):
  51. folder = self._get_collection_root_folder()
  52. # Path should already be sanitized
  53. self.path = pathutils.strip_path(path)
  54. self._encoding = self.configuration.get("encoding", "stock")
  55. if filesystem_path is None:
  56. filesystem_path = pathutils.path_to_filesystem(folder, self.path)
  57. self._filesystem_path = filesystem_path
  58. self._etag_cache = None
  59. super().__init__()
  60. @classmethod
  61. def _get_collection_root_folder(cls):
  62. filesystem_folder = os.path.expanduser(
  63. cls.configuration.get("storage", "filesystem_folder"))
  64. return os.path.join(filesystem_folder, "collection-root")
  65. @contextlib.contextmanager
  66. def _atomic_write(self, path, mode="w", newline=None, sync_directory=True,
  67. replace_fn=os.replace):
  68. directory = os.path.dirname(path)
  69. tmp = NamedTemporaryFile(
  70. mode=mode, dir=directory, delete=False, prefix=".Radicale.tmp-",
  71. newline=newline, encoding=None if "b" in mode else self._encoding)
  72. try:
  73. yield tmp
  74. tmp.flush()
  75. try:
  76. self._fsync(tmp.fileno())
  77. except OSError as e:
  78. raise RuntimeError("Fsync'ing file %r failed: %s" %
  79. (path, e)) from e
  80. tmp.close()
  81. replace_fn(tmp.name, path)
  82. except BaseException:
  83. tmp.close()
  84. os.remove(tmp.name)
  85. raise
  86. if sync_directory:
  87. self._sync_directory(directory)
  88. @classmethod
  89. def _fsync(cls, fd):
  90. if cls.configuration.getboolean("internal", "filesystem_fsync"):
  91. pathutils.fsync(fd)
  92. @classmethod
  93. def _sync_directory(cls, path):
  94. """Sync directory to disk.
  95. This only works on POSIX and does nothing on other systems.
  96. """
  97. if not cls.configuration.getboolean("internal", "filesystem_fsync"):
  98. return
  99. if os.name == "posix":
  100. try:
  101. fd = os.open(path, 0)
  102. try:
  103. cls._fsync(fd)
  104. finally:
  105. os.close(fd)
  106. except OSError as e:
  107. raise RuntimeError("Fsync'ing directory %r failed: %s" %
  108. (path, e)) from e
  109. @classmethod
  110. def _makedirs_synced(cls, filesystem_path):
  111. """Recursively create a directory and its parents in a sync'ed way.
  112. This method acts silently when the folder already exists.
  113. """
  114. if os.path.isdir(filesystem_path):
  115. return
  116. parent_filesystem_path = os.path.dirname(filesystem_path)
  117. # Prevent infinite loop
  118. if filesystem_path != parent_filesystem_path:
  119. # Create parent dirs recursively
  120. cls._makedirs_synced(parent_filesystem_path)
  121. # Possible race!
  122. os.makedirs(filesystem_path, exist_ok=True)
  123. cls._sync_directory(parent_filesystem_path)
  124. @property
  125. def last_modified(self):
  126. relevant_files = chain(
  127. (self._filesystem_path,),
  128. (self._props_path,) if os.path.exists(self._props_path) else (),
  129. (os.path.join(self._filesystem_path, h) for h in self._list()))
  130. last = max(map(os.path.getmtime, relevant_files))
  131. return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last))
  132. @property
  133. def etag(self):
  134. # reuse cached value if the storage is read-only
  135. if self._lock.locked == "w" or self._etag_cache is None:
  136. self._etag_cache = super().etag
  137. return self._etag_cache