database.py 8.1 KB

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