multifilesystem.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2014 Jean-Marc Martins
  3. # Copyright © 2014-2016 Guillaume Ayoub
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Multi files per calendar filesystem storage backend.
  19. """
  20. import os
  21. import json
  22. import shutil
  23. import time
  24. import sys
  25. from contextlib import contextmanager
  26. from . import filesystem
  27. from .. import ical
  28. from .. import log
  29. from .. import pathutils
  30. class Collection(filesystem.Collection):
  31. """Collection stored in several files per calendar."""
  32. def _create_dirs(self):
  33. if not os.path.exists(self._filesystem_path):
  34. os.makedirs(self._filesystem_path)
  35. @property
  36. def headers(self):
  37. return (
  38. ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
  39. ical.Header("VERSION:%s" % self.version))
  40. def write(self):
  41. self._create_dirs()
  42. for component in self.components:
  43. text = ical.serialize(
  44. self.tag, self.headers, [component] + self.timezones)
  45. name = component.name
  46. if not pathutils.is_safe_filesystem_path_component(name):
  47. log.LOGGER.debug(
  48. "Can't tranlate name safely to filesystem, "
  49. "skipping component: %s", name)
  50. continue
  51. filesystem_path = os.path.join(self._filesystem_path, name)
  52. with filesystem.open(filesystem_path, "w") as fd:
  53. fd.write(text)
  54. def delete(self):
  55. shutil.rmtree(self._filesystem_path)
  56. os.remove(self._props_path)
  57. def remove(self, name):
  58. if not pathutils.is_safe_filesystem_path_component(name):
  59. log.LOGGER.debug(
  60. "Can't tranlate name safely to filesystem, "
  61. "skipping component: %s", name)
  62. return
  63. if name in self.items:
  64. del self.items[name]
  65. filesystem_path = os.path.join(self._filesystem_path, name)
  66. if os.path.exists(filesystem_path):
  67. os.remove(filesystem_path)
  68. @property
  69. def text(self):
  70. components = (
  71. ical.Timezone, ical.Event, ical.Todo, ical.Journal, ical.Card)
  72. items = {}
  73. try:
  74. filenames = os.listdir(self._filesystem_path)
  75. except (OSError, IOError) as e:
  76. log.LOGGER.info(
  77. 'Error while reading collection %r: %r' % (
  78. self._filesystem_path, e))
  79. return ""
  80. for filename in filenames:
  81. path = os.path.join(self._filesystem_path, filename)
  82. try:
  83. with filesystem.open(path) as fd:
  84. items.update(self._parse(fd.read(), components))
  85. except (OSError, IOError) as e:
  86. log.LOGGER.warning(
  87. 'Error while reading item %r: %r' % (path, e))
  88. return ical.serialize(
  89. self.tag, self.headers, sorted(items.values(), key=lambda x: x.name))
  90. @classmethod
  91. def is_node(cls, path):
  92. filesystem_path = pathutils.path_to_filesystem(path, filesystem.FOLDER)
  93. return (
  94. os.path.isdir(filesystem_path) and
  95. not os.path.exists(filesystem_path + ".props"))
  96. @classmethod
  97. def is_leaf(cls, path):
  98. filesystem_path = pathutils.path_to_filesystem(path, filesystem.FOLDER)
  99. return (
  100. os.path.isdir(filesystem_path) and os.path.exists(path + ".props"))
  101. @property
  102. def last_modified(self):
  103. last = max([
  104. os.path.getmtime(os.path.join(self._filesystem_path, filename))
  105. for filename in os.listdir(self._filesystem_path)] or [0])
  106. return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(last))
  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. if os.path.exists(self._props_path):
  119. self._create_dirs()
  120. if old_properties != properties:
  121. with open(self._props_path, "w") as prop_file:
  122. json.dump(properties, prop_file)