filesystem.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2012-2016 Guillaume Ayoub
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Filesystem storage backend.
  18. """
  19. import codecs
  20. import os
  21. import posixpath
  22. import json
  23. import time
  24. import sys
  25. from contextlib import contextmanager
  26. from .. import config, ical, log, pathutils
  27. FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
  28. FILESYSTEM_ENCODING = sys.getfilesystemencoding()
  29. try:
  30. from dulwich.repo import Repo
  31. GIT_REPOSITORY = Repo(FOLDER)
  32. except:
  33. GIT_REPOSITORY = None
  34. # This function overrides the builtin ``open`` function for this module
  35. # pylint: disable=W0622
  36. @contextmanager
  37. def open(path, mode="r"):
  38. """Open a file at ``path`` with encoding set in the configuration."""
  39. # On enter
  40. abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
  41. with codecs.open(abs_path, mode, config.get("encoding", "stock")) as fd:
  42. yield fd
  43. # On exit
  44. if GIT_REPOSITORY and mode == "w":
  45. path = os.path.relpath(abs_path, FOLDER)
  46. GIT_REPOSITORY.stage([path])
  47. committer = config.get("git", "committer")
  48. GIT_REPOSITORY.do_commit(
  49. path.encode("utf-8"), committer=committer.encode("utf-8"))
  50. # pylint: enable=W0622
  51. class Collection(ical.Collection):
  52. """Collection stored in a flat ical file."""
  53. @property
  54. def _filesystem_path(self):
  55. """Absolute path of the file at local ``path``."""
  56. return pathutils.path_to_filesystem(self.path, FOLDER)
  57. @property
  58. def _props_path(self):
  59. """Absolute path of the file storing the collection properties."""
  60. return self._filesystem_path + ".props"
  61. def _create_dirs(self):
  62. """Create folder storing the collection if absent."""
  63. if not os.path.exists(os.path.dirname(self._filesystem_path)):
  64. os.makedirs(os.path.dirname(self._filesystem_path))
  65. def save(self, text):
  66. self._create_dirs()
  67. with open(self._filesystem_path, "w") as fd:
  68. fd.write(text)
  69. def delete(self):
  70. os.remove(self._filesystem_path)
  71. os.remove(self._props_path)
  72. @property
  73. def text(self):
  74. try:
  75. with open(self._filesystem_path) as fd:
  76. return fd.read()
  77. except IOError:
  78. return ""
  79. @classmethod
  80. def children(cls, path):
  81. filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
  82. _, directories, files = next(os.walk(filesystem_path))
  83. for filename in directories + files:
  84. # make sure that the local filename can be translated
  85. # into an internal path
  86. if not pathutils.is_safe_path_component(filename):
  87. log.LOGGER.debug("Skipping unsupported filename: %s", filename)
  88. continue
  89. rel_filename = posixpath.join(path, filename)
  90. if cls.is_node(rel_filename) or cls.is_leaf(rel_filename):
  91. yield cls(rel_filename)
  92. @classmethod
  93. def is_node(cls, path):
  94. filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
  95. return os.path.isdir(filesystem_path)
  96. @classmethod
  97. def is_leaf(cls, path):
  98. filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
  99. return (
  100. os.path.isfile(filesystem_path) and not
  101. filesystem_path.endswith(".props"))
  102. @property
  103. def last_modified(self):
  104. modification_time = time.gmtime(
  105. os.path.getmtime(self._filesystem_path))
  106. return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time)
  107. @property
  108. @contextmanager
  109. def props(self):
  110. # On enter
  111. properties = {}
  112. if os.path.exists(self._props_path):
  113. with open(self._props_path) as prop_file:
  114. properties.update(json.load(prop_file))
  115. old_properties = properties.copy()
  116. yield properties
  117. # On exit
  118. self._create_dirs()
  119. if old_properties != properties:
  120. with open(self._props_path, "w") as prop_file:
  121. json.dump(properties, prop_file)