Bump version number to 0.10.1.
[tor-bridgedb.git] / bridgedb / persistent.py
blobc572758bd5f632dc476a65bef9a03eb0533c5aa8
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 bridgerings
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,
39 bridgerings,
40 emailDistributor,
41 httpsDistributor)
44 class MissingState(Exception):
45 """Raised when the file or class storing global state is missing."""
48 def _getState():
49 """Retrieve the global state instance.
51 :rtype: :class:`~bridgedb.persistent.State`
52 :returns: An unpickled de-sexp'ed state object, which may contain just
53 about anything, but should contain things like options, loaded config
54 settings, etc.
55 """
56 return _state
58 def _setState(state):
59 """Set the global state.
61 :type state: :class:`~bridgedb.persistent.State`
62 :param state: The state instance to save.
63 """
64 global _state
65 _state = state
67 def load(stateCls=None):
68 """Given a :class:`State`, try to unpickle it's ``statefile``.
70 :param string stateCls: An instance of :class:`~bridgedb.persistent.State`. If
71 not given, try loading from ``_statefile`` if that file exists.
72 :rtype: None or :class:`State`
73 """
74 statefile = None
75 if stateCls and isinstance(stateCls, State):
76 cls = stateCls
77 else:
78 cls = _getState()
80 if not cls:
81 raise MissingState("Could not find a state instance to load.")
82 else:
83 loaded = cls.load()
84 return loaded
87 class State(jelly.Jellyable):
88 """Pickled, jellied storage container for persistent state."""
90 def __init__(self, config=None, **kwargs):
91 """Create a persistent state storage mechanism.
93 Serialisation of certain classes in BridgeDB doesn't work. Classes and
94 modules which are known to be unjelliable/unpicklable so far are:
96 - bridgedb.Dist
97 - bridgedb.bridgerings, and all "splitter" and "ring" classes
98 contained within
100 :property statefile: The filename to retrieve a pickled, jellied
101 :class:`~bridgedb.persistent.State` instance from. (default:
102 :attr:`bridgedb.persistent.State._statefile`)
104 self._statefile = os.path.abspath(str(__package__) + '.state')
105 self.proxyList = None
106 self.config = None
107 self.key = None
109 if 'STATEFILE' in kwargs:
110 self.statefile = kwargs['STATEFILE']
112 for key, value in kwargs.items():
113 self.__dict__[key] = value
115 if config is not None:
116 for key, value in config.__dict__.items():
117 self.__dict__[key] = value
119 _setState(self)
121 def _get_statefile(self):
122 """Retrieve the filename of the global statefile.
124 :rtype: string
125 :returns: The filename of the statefile.
127 return self._statefile
129 def _set_statefile(self, filename):
130 """Set the global statefile.
132 :param string statefile: The filename of the statefile.
135 if filename is None:
136 self._statefile = None
137 return
139 filename = os.path.abspath(os.path.expanduser(filename))
140 logging.debug("Setting statefile to '%s'" % filename)
141 self._statefile = filename
143 # Create the parent directory if it doesn't exist:
144 dirname = os.path.dirname(filename)
145 if not os.path.isdir(dirname):
146 os.makedirs(dirname)
148 # Create the statefile if it doesn't exist:
149 if not os.path.exists(filename):
150 open(filename, 'w').close()
152 def _del_statefile(self):
153 """Delete the file containing previously saved state."""
154 try:
155 with open(self._statefile, 'w') as fh:
156 fh.close()
157 os.unlink(self._statefile)
158 self._statefile = None
159 except (IOError, OSError) as error: # pragma: no cover
160 logging.error("There was an error deleting the statefile: '%s'"
161 % self._statefile)
163 statefile = property(_get_statefile, _set_statefile, _del_statefile,
164 """Filename property of a persisent.State.""")
166 def load(self, statefile=None):
167 """Load a previously saved statefile.
169 :raises MissingState: If there was any error loading the **statefile**.
170 :rtype: :class:`State` or None
171 :returns: The state, loaded from :attr:`State.STATEFILE`, or None if
172 an error occurred.
174 if not statefile:
175 if not self.statefile:
176 raise MissingState("Could not find a state file to load.")
177 statefile = self.statefile
178 logging.debug("Retrieving state from: \t'%s'" % statefile)
180 quo= fh = None
181 err = ''
183 try:
184 if isinstance(statefile, (str, bytes)):
185 fh = open(statefile, 'rb')
186 elif not statefile.closed:
187 fh = statefile
188 except (IOError, OSError) as error: # pragma: no cover
189 err += "There was an error reading statefile "
190 err += "'{0}':\n{1}".format(statefile, error)
191 except (AttributeError, TypeError) as error:
192 err += "Failed statefile.open() and statefile.closed:"
193 err += "\n\t{0}\nstatefile type = '{1}'".format(
194 error, type(statefile))
195 else:
196 try:
197 status = pickle.load(fh)
198 except EOFError:
199 err += "The statefile %s was empty." % fh.name
200 else:
201 quo = jelly.unjelly(status)
202 if fh is not None:
203 fh.close()
204 if quo:
205 return quo
207 if err:
208 raise MissingState(err)
210 def save(self, statefile=None):
211 """Save state as a pickled jelly to a file on disk."""
212 if not statefile:
213 if not self._statefile:
214 raise MissingState("Could not find a state file to load.")
215 statefile = self._statefile
216 logging.debug("Saving state to: \t'%s'" % statefile)
218 fh = None
219 try:
220 fh = open(statefile, 'wb')
221 except (IOError, OSError) as error: # pragma: no cover
222 logging.warn("Error writing state file to '%s': %s"
223 % (statefile, error))
224 else:
225 try:
226 pickle.dump(jelly.jelly(self), fh)
227 except AttributeError as error:
228 logging.debug("Tried jellying an unjelliable object: %s"
229 % error)
231 if fh is not None:
232 fh.flush()
233 fh.close()
235 def useChangedSettings(self, config):
236 """Take a new config, compare it to the last one, and update settings.
238 Given a ``config`` object created from the configuration file, compare
239 it to the last :class:`~bridgedb.configure.Conf` that was stored, and apply
240 any settings which were changed to be attributes of the :class:`State`
241 instance.
243 updated = []
244 new = []
246 for key, value in config.__dict__.items():
247 try:
248 # If state.config doesn't have the same value as the new
249 # config, then update the state setting.
251 # Be sure, when updating settings while parsing the config
252 # file, to assign the new settings as attributes of the
253 # :class:`bridgedb.configure.Conf` instance.
254 if value != self.config.__dict__[key]:
255 setattr(self, key, value)
256 updated.append(key)
257 logging.debug("Updated %s setting: %r %r" %
258 (safe_repr(key),
259 self.config.__dict__[key],
260 safe_repr(value)))
261 except (KeyError, AttributeError):
262 setattr(self, key, value)
263 new.append(key)
264 logging.debug("New setting: %s = %r" %
265 (safe_repr(key),
266 safe_repr(value)))
268 logging.info("Updated setting(s): %s" % ' '.join([x for x in updated]))
269 logging.info("New setting(s): %s" % ' '.join([x for x in new]))
270 logging.debug(
271 "Saving newer config as `state.config` for later comparison")
272 self.config = config