__init__.py 6.2 KB

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