You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
5.9 KiB

  1. ###################################################################################
  2. #
  3. # Copyright (c) 2017-2019 MuK IT GmbH.
  4. #
  5. # This file is part of MuK Session Store
  6. # (see https://mukit.at).
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ###################################################################################
  22. import pickle
  23. import logging
  24. import psycopg2
  25. import functools
  26. from contextlib import closing
  27. from contextlib import contextmanager
  28. from datetime import datetime, date
  29. from werkzeug.contrib.sessions import SessionStore
  30. from odoo.sql_db import db_connect
  31. from odoo.tools import config
  32. _logger = logging.getLogger(__name__)
  33. def retry_database(func):
  34. @functools.wraps(func)
  35. def wrapper(self, *args, **kwargs):
  36. for attempts in range(1, 6):
  37. try:
  38. return func(self, *args, **kwargs)
  39. except psycopg2.InterfaceError as error:
  40. _logger.warn("SessionStore connection failed! (%s/5)" % attempts)
  41. if attempts >= 5:
  42. raise error
  43. return wrapper
  44. class PostgresSessionStore(SessionStore):
  45. def __init__(self, *args, **kwargs):
  46. super(PostgresSessionStore, self).__init__(*args, **kwargs)
  47. self.dbname = config.get("session_store_dbname", "session_store")
  48. self.dbtable = config.get("session_store_dbtable", "odoo_sessions")
  49. self._setup_database(raise_exception=False)
  50. def _setup_database(self, raise_exception=True):
  51. try:
  52. with db_connect(self.dbname, allow_uri=True).cursor() as cursor:
  53. cursor.autocommit(True)
  54. self._create_table(cursor)
  55. except:
  56. self._create_database()
  57. self._setup_database()
  58. def _create_database(self):
  59. with db_connect("postgres").cursor() as cursor:
  60. cursor.autocommit(True)
  61. cursor.execute(
  62. """
  63. CREATE DATABASE {dbname}
  64. ENCODING 'unicode'
  65. TEMPLATE 'template0';
  66. """.format(
  67. dbname=self.dbname
  68. )
  69. )
  70. def _create_table(self, cursor):
  71. cursor.execute(
  72. """
  73. CREATE TABLE IF NOT EXISTS {dbtable} (
  74. sid varchar PRIMARY KEY,
  75. write_date timestamp without time zone NOT NULL,
  76. payload bytea NOT NULL
  77. );
  78. """.format(
  79. dbtable=self.dbtable
  80. )
  81. )
  82. @contextmanager
  83. def open_cursor(self):
  84. connection = db_connect(self.dbname, allow_uri=True)
  85. cursor = connection.cursor()
  86. cursor.autocommit(True)
  87. yield cursor
  88. cursor.close()
  89. @retry_database
  90. def save(self, session):
  91. with self.open_cursor() as cursor:
  92. cursor.execute(
  93. """
  94. INSERT INTO {dbtable} (sid, write_date, payload)
  95. VALUES (%(sid)s, now() at time zone 'UTC', %(payload)s)
  96. ON CONFLICT (sid)
  97. DO UPDATE SET payload = %(payload)s, write_date = now() at time zone 'UTC';
  98. """.format(
  99. dbtable=self.dbtable
  100. ),
  101. dict(
  102. sid=session.sid,
  103. payload=psycopg2.Binary(
  104. pickle.dumps(dict(session), pickle.HIGHEST_PROTOCOL)
  105. ),
  106. ),
  107. )
  108. @retry_database
  109. def delete(self, session):
  110. with self.open_cursor() as cursor:
  111. cursor.execute(
  112. "DELETE FROM {dbtable} WHERE sid=%s;".format(dbtable=self.dbtable),
  113. [session.sid],
  114. )
  115. @retry_database
  116. def get(self, sid):
  117. if not self.is_valid_key(sid):
  118. return self.new()
  119. with self.open_cursor() as cursor:
  120. cursor.execute(
  121. """
  122. SELECT payload, write_date
  123. FROM {dbtable} WHERE sid=%s;
  124. """.format(
  125. dbtable=self.dbtable
  126. ),
  127. [sid],
  128. )
  129. try:
  130. payload, write_date = cursor.fetchone()
  131. if write_date.date() != datetime.today().date():
  132. cursor.execute(
  133. """
  134. UPDATE {dbtable}
  135. SET write_date = now() at time zone 'UTC'
  136. WHERE sid=%s;
  137. """.format(
  138. dbtable=self.dbtable
  139. ),
  140. [sid],
  141. )
  142. return self.session_class(pickle.loads(payload), sid, False)
  143. except Exception:
  144. return self.session_class({}, sid, False)
  145. @retry_database
  146. def list(self):
  147. with self.open_cursor() as cursor:
  148. cursor.execute("SELECT sid FROM {dbtable};".format(dbtable=self.dbtable))
  149. return [record[0] for record in cursor.fetchall()]
  150. @retry_database
  151. def clean(self):
  152. with self.open_cursor() as cursor:
  153. cursor.execute(
  154. """
  155. DELETE FROM {dbtable}
  156. WHERE now() at time zone 'UTC' - write_date > '7 days';
  157. """.format(
  158. dbtable=self.dbtable
  159. )
  160. )