Make getCaptchaImage return (bytes, str).
[tor-bridgedb.git] / bridgedb / persistent.py
blob3d52ec5a21f57c55fc758000def419fb65b9bebe
1 # -*- coding: utf-8 ; test-case-name: bridgedb.test.test_persistent -*-
3 # This file is part of BridgeDB, a Tor bridge distribution system.
5 # :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis@torproject.org>
6 # please also see AUTHORS file
7 # :copyright: (c) 2013-2017 Isis Lovecruft
8 # (c) 2007-2017, The Tor Project, Inc.
9 # (c) 2007-2017, all entities within the AUTHORS file
10 # :license: 3-clause BSD, see included LICENSE for information
12 """Module for functionality to persistently store state."""
14 import copy
15 import logging
16 import os.path
18 try:
19 import cPickle as pickle
20 except (ImportError, NameError): # pragma: no cover
21 import pickle
23 from twisted.python.reflect import safe_repr
24 from twisted.spread import jelly
26 from bridgedb import Bridges
27 from bridgedb import filters
28 from bridgedb.distributors.email import distributor as emailDistributor
29 from bridgedb.distributors.https import distributor as httpsDistributor
30 from bridgedb.configure import Conf
31 #from bridgedb.proxy import ProxySet
33 _state = None
35 #: Types and classes which are allowed to be jellied:
36 _security = jelly.SecurityOptions()
37 #_security.allowInstancesOf(ProxySet)
38 _security.allowModules(filters, Bridges, emailDistributor, httpsDistributor)
41 class MissingState(Exception):
42 """Raised when the file or class storing global state is missing."""
45 def _getState():
46 """Retrieve the global state instance.
48 :rtype: :class:`~bridgedb.persistent.State`
49 :returns: An unpickled de-sexp'ed state object, which may contain just
50 about anything, but should contain things like options, loaded config
51 settings, etc.
52 """
53 return _state
55 def _setState(state):
56 """Set the global state.
58 :type state: :class:`~bridgedb.persistent.State`
59 :param state: The state instance to save.
60 """
61 global _state
62 _state = state
64 def load(stateCls=None):
65 """Given a :class:`State`, try to unpickle it's ``statefile``.
67 :param string stateCls: An instance of :class:`~bridgedb.persistent.State`. If
68 not given, try loading from ``_statefile`` if that file exists.
69 :rtype: None or :class:`State`
70 """
71 statefile = None
72 if stateCls and isinstance(stateCls, State):
73 cls = stateCls
74 else:
75 cls = _getState()
77 if not cls:
78 raise MissingState("Could not find a state instance to load.")
79 else:
80 loaded = cls.load()
81 return loaded
84 class State(jelly.Jellyable):
85 """Pickled, jellied storage container for persistent state."""
87 def __init__(self, config=None, **kwargs):
88 """Create a persistent state storage mechanism.
90 Serialisation of certain classes in BridgeDB doesn't work. Classes and
91 modules which are known to be unjelliable/unpicklable so far are:
93 - bridgedb.Dist
94 - bridgedb.Bridges, and all "splitter" and "ring" classes contained
95 within
97 :property statefile: The filename to retrieve a pickled, jellied
98 :class:`~bridgedb.persistent.State` instance from. (default:
99 :attr:`bridgedb.persistent.State._statefile`)
101 self._statefile = os.path.abspath(str(__package__) + '.state')
102 self.proxyList = None
103 self.config = None
104 self.key = None
106 if 'STATEFILE' in kwargs:
107 self.statefile = kwargs['STATEFILE']
109 for key, value in kwargs.items():
110 self.__dict__[key] = value
112 if config is not None:
113 for key, value in config.__dict__.items():
114 self.__dict__[key] = value
116 _setState(self)
118 def _get_statefile(self):
119 """Retrieve the filename of the global statefile.
121 :rtype: string
122 :returns: The filename of the statefile.
124 return self._statefile
126 def _set_statefile(self, filename):
127 """Set the global statefile.
129 :param string statefile: The filename of the statefile.
132 if filename is None:
133 self._statefile = None
134 return
136 filename = os.path.abspath(os.path.expanduser(filename))
137 logging.debug("Setting statefile to '%s'" % filename)
138 self._statefile = filename
140 # Create the parent directory if it doesn't exist:
141 dirname = os.path.dirname(filename)
142 if not os.path.isdir(dirname):
143 os.makedirs(dirname)
145 # Create the statefile if it doesn't exist:
146 if not os.path.exists(filename):
147 open(filename, 'w').close()
149 def _del_statefile(self):
150 """Delete the file containing previously saved state."""
151 try:
152 with open(self._statefile, 'w') as fh:
153 fh.close()
154 os.unlink(self._statefile)
155 self._statefile = None
156 except (IOError, OSError) as error: # pragma: no cover
157 logging.error("There was an error deleting the statefile: '%s'"
158 % self._statefile)
160 statefile = property(_get_statefile, _set_statefile, _del_statefile,
161 """Filename property of a persisent.State.""")
163 def load(self, statefile=None):
164 """Load a previously saved statefile.
166 :raises MissingState: If there was any error loading the **statefile**.
167 :rtype: :class:`State` or None
168 :returns: The state, loaded from :attr:`State.STATEFILE`, or None if
169 an error occurred.
171 if not statefile:
172 if not self.statefile:
173 raise MissingState("Could not find a state file to load.")
174 statefile = self.statefile
175 logging.debug("Retrieving state from: \t'%s'" % statefile)
177 quo= fh = None
178 err = ''
180 try:
181 if isinstance(statefile, (str, bytes)):
182 fh = open(statefile, 'rb')
183 elif not statefile.closed:
184 fh = statefile
185 except (IOError, OSError) as error: # pragma: no cover
186 err += "There was an error reading statefile "
187 err += "'{0}':\n{1}".format(statefile, error)
188 except (AttributeError, TypeError) as error:
189 err += "Failed statefile.open() and statefile.closed:"
190 err += "\n\t{0}\nstatefile type = '{1}'".format(
191 error, type(statefile))
192 else:
193 try:
194 status = pickle.load(fh)
195 except EOFError:
196 err += "The statefile %s was empty." % fh.name
197 else:
198 quo = jelly.unjelly(status)
199 if fh is not None:
200 fh.close()
201 if quo:
202 return quo
204 if err:
205 raise MissingState(err)
207 def save(self, statefile=None):
208 """Save state as a pickled jelly to a file on disk."""
209 if not statefile:
210 if not self._statefile:
211 raise MissingState("Could not find a state file to load.")
212 statefile = self._statefile
213 logging.debug("Saving state to: \t'%s'" % statefile)
215 fh = None
216 try:
217 fh = open(statefile, 'wb')
218 except (IOError, OSError) as error: # pragma: no cover
219 logging.warn("Error writing state file to '%s': %s"
220 % (statefile, error))
221 else:
222 try:
223 pickle.dump(jelly.jelly(self), fh)
224 except AttributeError as error:
225 logging.debug("Tried jellying an unjelliable object: %s"
226 % error)
228 if fh is not None:
229 fh.flush()
230 fh.close()
232 def useChangedSettings(self, config):
233 """Take a new config, compare it to the last one, and update settings.
235 Given a ``config`` object created from the configuration file, compare
236 it to the last :class:`~bridgedb.configure.Conf` that was stored, and apply
237 any settings which were changed to be attributes of the :class:`State`
238 instance.
240 updated = []
241 new = []
243 for key, value in config.__dict__.items():
244 try:
245 # If state.config doesn't have the same value as the new
246 # config, then update the state setting.
248 # Be sure, when updating settings while parsing the config
249 # file, to assign the new settings as attributes of the
250 # :class:`bridgedb.configure.Conf` instance.
251 if value != self.config.__dict__[key]:
252 setattr(self, key, value)
253 updated.append(key)
254 logging.debug("Updated %s setting: %r %r" %
255 (safe_repr(key),
256 self.config.__dict__[key],
257 safe_repr(value)))
258 except (KeyError, AttributeError):
259 setattr(self, key, value)
260 new.append(key)
261 logging.debug("New setting: %s = %r" %
262 (safe_repr(key),
263 safe_repr(value)))
265 logging.info("Updated setting(s): %s" % ' '.join([x for x in updated]))
266 logging.info("New setting(s): %s" % ' '.join([x for x in new]))
267 logging.debug(
268 "Saving newer config as `state.config` for later comparison")
269 self.config = config