database.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Radicale Server - Calendar Server
  4. # Copyright © 2013 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. SQLAlchemy storage backend.
  20. """
  21. import time
  22. from datetime import datetime
  23. from contextlib import contextmanager
  24. from sqlalchemy import create_engine, Column, Unicode, Integer, ForeignKey
  25. from sqlalchemy import func
  26. from sqlalchemy.orm import sessionmaker, relationship
  27. from sqlalchemy.ext.declarative import declarative_base
  28. from .. import config, ical
  29. # These are classes, not constants
  30. # pylint: disable=C0103
  31. Base = declarative_base()
  32. Session = sessionmaker()
  33. Session.configure(bind=create_engine(config.get("storage", "database_url")))
  34. # pylint: enable=C0103
  35. class DBCollection(Base):
  36. """Table of collections."""
  37. __tablename__ = "collection"
  38. path = Column(Unicode, primary_key=True)
  39. parent_path = Column(Unicode, ForeignKey("collection.path"))
  40. parent = relationship(
  41. "DBCollection", backref="children", remote_side=[path])
  42. class DBItem(Base):
  43. """Table of collection's items."""
  44. __tablename__ = "item"
  45. name = Column(Unicode, primary_key=True)
  46. tag = Column(Unicode)
  47. collection_path = Column(Unicode, ForeignKey("collection.path"))
  48. collection = relationship("DBCollection", backref="items")
  49. class DBHeader(Base):
  50. """Table of item's headers."""
  51. __tablename__ = "header"
  52. name = Column(Unicode, primary_key=True)
  53. value = Column(Unicode)
  54. collection_path = Column(
  55. Unicode, ForeignKey("collection.path"), primary_key=True)
  56. collection = relationship("DBCollection", backref="headers")
  57. class DBLine(Base):
  58. """Table of item's lines."""
  59. __tablename__ = "line"
  60. name = Column(Unicode)
  61. value = Column(Unicode)
  62. item_name = Column(Unicode, ForeignKey("item.name"))
  63. timestamp = Column(
  64. Integer, default=lambda: time.time() * 10 ** 6, primary_key=True)
  65. item = relationship("DBItem", backref="lines", order_by=timestamp)
  66. class DBProperty(Base):
  67. """Table of collection's properties."""
  68. __tablename__ = "property"
  69. name = Column(Unicode, primary_key=True)
  70. value = Column(Unicode)
  71. collection_path = Column(
  72. Unicode, ForeignKey("collection.path"), primary_key=True)
  73. collection = relationship(
  74. "DBCollection", backref="properties", cascade="delete")
  75. class Collection(ical.Collection):
  76. """Collection stored in a database."""
  77. def __init__(self, path, principal=False):
  78. self.session = Session()
  79. super(Collection, self).__init__(path, principal)
  80. def __del__(self):
  81. self.session.commit()
  82. def _query(self, item_types):
  83. """Get collection's items matching ``item_types``."""
  84. item_objects = []
  85. for item_type in item_types:
  86. items = (
  87. self.session.query(DBItem)
  88. .filter_by(collection_path=self.path, tag=item_type.tag)
  89. .order_by(DBItem.name).all())
  90. for item in items:
  91. text = "\n".join(
  92. "%s:%s" % (line.name, line.value) for line in item.lines)
  93. item_objects.append(item_type(text, item.name))
  94. return item_objects
  95. @property
  96. def _modification_time(self):
  97. """Collection's last modification time."""
  98. timestamp = (
  99. self.session.query(func.max(DBLine.timestamp))
  100. .join(DBItem).filter_by(collection_path=self.path).first()[0])
  101. if timestamp:
  102. return datetime.fromtimestamp(float(timestamp) / 10 ** 6)
  103. else:
  104. return datetime.now()
  105. @property
  106. def _db_collection(self):
  107. """Collection's object mapped to the table line."""
  108. return self.session.query(DBCollection).get(self.path)
  109. def write(self, headers=None, items=None):
  110. headers = headers or self.headers or (
  111. ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
  112. ical.Header("VERSION:%s" % self.version))
  113. items = items if items is not None else self.items
  114. if self._db_collection:
  115. for item in self._db_collection.items:
  116. for line in item.lines:
  117. self.session.delete(line)
  118. self.session.delete(item)
  119. for header in self._db_collection.headers:
  120. self.session.delete(header)
  121. else:
  122. db_collection = DBCollection()
  123. db_collection.path = self.path
  124. db_collection.parent_path = "/".join(self.path.split("/")[:-1])
  125. self.session.add(db_collection)
  126. for header in headers:
  127. db_header = DBHeader()
  128. db_header.name, db_header.value = header.text.split(":", 1)
  129. db_header.collection_path = self.path
  130. self.session.add(db_header)
  131. for item in items:
  132. db_item = DBItem()
  133. db_item.name = item.name
  134. db_item.tag = item.tag
  135. db_item.collection_path = self.path
  136. self.session.add(db_item)
  137. for line in ical.unfold(item.text):
  138. db_line = DBLine()
  139. db_line.name, db_line.value = line.split(":", 1)
  140. db_line.item_name = item.name
  141. self.session.add(db_line)
  142. def delete(self):
  143. self.session.delete(self._db_collection)
  144. @property
  145. def text(self):
  146. return ical.serialize(self.tag, self.headers, self.items)
  147. @property
  148. def etag(self):
  149. return '"%s"' % hash(self._modification_time)
  150. @property
  151. def headers(self):
  152. headers = (
  153. self.session.query(DBHeader)
  154. .filter_by(collection_path=self.path)
  155. .order_by(DBHeader.name).all())
  156. return [
  157. ical.Header("%s:%s" % (header.name, header.value))
  158. for header in headers]
  159. @classmethod
  160. def children(cls, path):
  161. session = Session()
  162. children = (
  163. session.query(DBCollection)
  164. .filter_by(parent_path=path or "").all())
  165. collections = [cls(child.path) for child in children]
  166. session.close()
  167. return collections
  168. @classmethod
  169. def is_node(cls, path):
  170. if not path:
  171. return True
  172. session = Session()
  173. result = (
  174. session.query(DBCollection)
  175. .filter_by(parent_path=path or "").count() > 0)
  176. session.close()
  177. return result
  178. @classmethod
  179. def is_leaf(cls, path):
  180. if not path:
  181. return False
  182. session = Session()
  183. result = (
  184. session.query(DBItem)
  185. .filter_by(collection_path=path or "").count() > 0)
  186. session.close()
  187. return result
  188. @property
  189. def last_modified(self):
  190. return time.strftime(
  191. "%a, %d %b %Y %H:%M:%S +0000", self._modification_time.timetuple())
  192. @property
  193. @contextmanager
  194. def props(self):
  195. # On enter
  196. properties = {}
  197. db_properties = (
  198. self.session.query(DBProperty)
  199. .filter_by(collection_path=self.path).all())
  200. for prop in db_properties:
  201. properties[prop.name] = prop.value
  202. old_properties = properties.copy()
  203. yield properties
  204. # On exit
  205. if self._db_collection and old_properties != properties:
  206. for prop in db_properties:
  207. self.session.delete(prop)
  208. for name, value in properties.items():
  209. prop = DBProperty()
  210. prop.name = name
  211. prop.value = value
  212. prop.collection_path = self.path
  213. self.session.add(prop)
  214. @property
  215. def items(self):
  216. return self._query(
  217. (ical.Event, ical.Todo, ical.Journal, ical.Card, ical.Timezone))
  218. @property
  219. def components(self):
  220. return self._query((ical.Event, ical.Todo, ical.Journal, ical.Card))
  221. @property
  222. def events(self):
  223. return self._query((ical.Event,))
  224. @property
  225. def todos(self):
  226. return self._query((ical.Todo,))
  227. @property
  228. def journals(self):
  229. return self._query((ical.Journal,))
  230. @property
  231. def timezones(self):
  232. return self._query((ical.Timezone,))
  233. @property
  234. def cards(self):
  235. return self._query((ical.Card,))
  236. def save(self):
  237. """Save the text into the collection.
  238. This method is not used for databases.
  239. """