filesystem.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Radicale Server - Calendar Server
  4. # Copyright © 2012-2015 Guillaume Ayoub
  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. Filesystem storage backend.
  20. """
  21. import codecs
  22. import os
  23. import posixpath
  24. import json
  25. import time
  26. import sys
  27. from contextlib import contextmanager
  28. from .. import config, ical, log, pathutils
  29. FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
  30. FILESYSTEM_ENCODING = sys.getfilesystemencoding()
  31. try:
  32. from dulwich.repo import Repo
  33. GIT_REPOSITORY = Repo(FOLDER)
  34. except:
  35. GIT_REPOSITORY = None
  36. # This function overrides the builtin ``open`` function for this module
  37. # pylint: disable=W0622
  38. @contextmanager
  39. def open(path, mode="r"):
  40. """Open a file at ``path`` with encoding set in the configuration."""
  41. # On enter
  42. abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
  43. with codecs.open(abs_path, mode, config.get("encoding", "stock")) as fd:
  44. yield fd
  45. # On exit
  46. if GIT_REPOSITORY and mode == "w":
  47. path = os.path.relpath(abs_path, FOLDER)
  48. GIT_REPOSITORY.stage([path])
  49. committer = config.get("git", "committer")
  50. GIT_REPOSITORY.do_commit(
  51. path.encode("utf-8"), committer=committer.encode("utf-8"))
  52. # pylint: enable=W0622
  53. class Collection(ical.Collection):
  54. """Collection stored in a flat ical file."""
  55. @property
  56. def _filesystem_path(self):
  57. """Absolute path of the file at local ``path``."""
  58. return pathutils.path_to_filesystem(self.path, FOLDER)
  59. @property
  60. def _props_path(self):
  61. """Absolute path of the file storing the collection properties."""
  62. return self._filesystem_path + ".props"
  63. def _create_dirs(self):
  64. """Create folder storing the collection if absent."""
  65. if not os.path.exists(os.path.dirname(self._filesystem_path)):
  66. os.makedirs(os.path.dirname(self._filesystem_path))
  67. def save(self, text):
  68. self._create_dirs()
  69. with open(self._filesystem_path, "w") as fd:
  70. fd.write(text)
  71. def delete(self):
  72. os.remove(self._filesystem_path)
  73. os.remove(self._props_path)
  74. @property
  75. def text(self):
  76. try:
  77. with open(self._filesystem_path) as fd:
  78. return fd.read()
  79. except IOError:
  80. return ""
  81. @classmethod
  82. def children(cls, path):
  83. filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
  84. _, directories, files = next(os.walk(filesystem_path))
  85. for filename in directories + files:
  86. # make sure that the local filename can be translated
  87. # into an internal path
  88. if not pathutils.is_safe_path_component(filename):
  89. log.LOGGER.debug("Skipping unsupported filename: %s", filename)
  90. continue
  91. rel_filename = posixpath.join(path, filename)
  92. if cls.is_node(rel_filename) or cls.is_leaf(rel_filename):
  93. yield cls(rel_filename)
  94. @classmethod
  95. def is_node(cls, path):
  96. filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
  97. return os.path.isdir(filesystem_path)
  98. @classmethod
  99. def is_leaf(cls, path):
  100. filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
  101. return (
  102. os.path.isfile(filesystem_path) and not
  103. filesystem_path.endswith(".props"))
  104. @property
  105. def last_modified(self):
  106. modification_time = time.gmtime(
  107. os.path.getmtime(self._filesystem_path))
  108. return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time)
  109. @property
  110. @contextmanager
  111. def props(self):
  112. # On enter
  113. properties = {}
  114. if os.path.exists(self._props_path):
  115. with open(self._props_path) as prop_file:
  116. properties.update(json.load(prop_file))
  117. old_properties = properties.copy()
  118. yield properties
  119. # On exit
  120. self._create_dirs()
  121. if old_properties != properties:
  122. with open(self._props_path, "w") as prop_file:
  123. json.dump(properties, prop_file)