Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit 463c36e

Browse files
committed
Copied pyabstractstorage into oidcmsg. Decided we don't need another repo to keep track on.
1 parent e282b9b commit 463c36e

File tree

8 files changed

+729
-0
lines changed

8 files changed

+729
-0
lines changed

src/oidcmsg/storage/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from .utils import importer
2+
3+
4+
class AbstractStorage(object):
5+
"""
6+
An AbstractStorage that take a storage engine and offer a standard set
7+
of methods and I/O to data.
8+
"""
9+
10+
def __init__(self, conf_dict):
11+
if isinstance(conf_dict['handler'], str):
12+
_handler = importer(conf_dict['handler'])
13+
_args = {k: v for k, v in conf_dict.items() if k != 'handler'}
14+
self.storage = _handler(_args)
15+
else:
16+
self.storage = conf_dict['handler'](conf_dict)
17+
18+
def get(self, k, default=None):
19+
return self.storage.get(k, default)
20+
21+
def set(self, k, v):
22+
return self.storage.set(k, v)
23+
24+
def update(self, k, v):
25+
return self.storage.update(k, v)
26+
27+
def delete(self, k, v):
28+
return self.storage.delete(v, k=k)
29+
30+
def __getitem__(self, k):
31+
return self.storage.get(k)
32+
33+
def __setitem__(self, k, v):
34+
return self.storage.set(k, v)
35+
36+
def __delitem__(self, v):
37+
return self.storage.delete(v)
38+
39+
def __call__(self):
40+
return self.storage()
41+
42+
def __repr__(self):
43+
return self.__str__()
44+
45+
def __len__(self):
46+
return len(self.storage())
47+
48+
def __contains__(self, k):
49+
return self.storage.__contains__(k)
50+
51+
def __str__(self):
52+
return self.storage.__str__()
53+
54+
def __iter__(self):
55+
return iter(self.storage.__iter__())
56+
57+
def flush(self):
58+
return self.storage.flush()
59+
60+
def keys(self):
61+
return self.storage.keys()

src/oidcmsg/storage/abfile.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import logging
2+
import os
3+
import time
4+
5+
from filelock import FileLock
6+
7+
from .converter import PassThru
8+
from .converter import QPKey
9+
from .utils import importer
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class AbstractFileSystem(object):
15+
"""
16+
FileSystem implements a simple file based database.
17+
It has a dictionary like interface.
18+
Each key maps one-to-one to a file on disc, where the content of the
19+
file is the value.
20+
ONLY goes one level deep.
21+
Not directories in directories.
22+
"""
23+
24+
def __init__(self, conf_dict):
25+
"""
26+
items = FileSystem(
27+
{
28+
'fdir': fdir,
29+
'key_conv':{'to': quote_plus, 'from': unquote_plus},
30+
'value_conv':{'to': keyjar_to_jwks, 'from': jwks_to_keyjar}
31+
})
32+
33+
fdir: The root of the directory
34+
key_conv: Converts to/from the key displayed by this class to
35+
users of it to something that can be used as a file name.
36+
The value of key_conv is a class that has the methods 'serialize'/'deserialize'.
37+
value_conv: As with key_conv you can convert/translate
38+
the value bound to a key in the database to something that can easily
39+
be stored in a file. Like with key_conv the value of this parameter
40+
is a class that has the methods 'serialize'/'deserialize'.
41+
"""
42+
43+
_fdir = conf_dict.get('fdir', '')
44+
if '{issuer}' in _fdir:
45+
issuer = conf_dict.get('issuer')
46+
if not issuer:
47+
raise ValueError('Missing issuer value')
48+
self.fdir = _fdir.format(issuer=issuer)
49+
else:
50+
self.fdir = _fdir
51+
52+
self.fmtime = {}
53+
self.db = {}
54+
55+
key_conv = conf_dict.get('key_conv')
56+
if key_conv:
57+
self.key_conv = importer(key_conv)()
58+
else:
59+
self.key_conv = QPKey()
60+
61+
value_conv = conf_dict.get('value_conv')
62+
if value_conv:
63+
self.value_conv = importer(value_conv)()
64+
else:
65+
self.value_conv = PassThru()
66+
67+
if not os.path.isdir(self.fdir):
68+
os.makedirs(self.fdir)
69+
70+
self.sync()
71+
72+
def get(self, item, default=None):
73+
"""
74+
Return the value bound to an identifier.
75+
76+
:param item: The identifier.
77+
:return:
78+
"""
79+
item = self.key_conv.serialize(item)
80+
81+
try:
82+
if self.is_changed(item):
83+
logger.info("File content change in {}".format(item))
84+
fname = os.path.join(self.fdir, item)
85+
self.db[item] = self._read_info(fname)
86+
except KeyError:
87+
return default
88+
else:
89+
return self.db[item]
90+
91+
def set(self, key, value):
92+
"""
93+
Binds a value to a specific key. If the file that the key maps to
94+
does not exist it will be created. The content of the file will be
95+
set to the value given.
96+
97+
:param key: Identifier
98+
:param value: Value that should be bound to the identifier.
99+
:return:
100+
"""
101+
102+
if not os.path.isdir(self.fdir):
103+
os.makedirs(self.fdir, exist_ok=True)
104+
105+
try:
106+
_key = self.key_conv.serialize(key)
107+
except KeyError:
108+
_key = key
109+
110+
fname = os.path.join(self.fdir, _key)
111+
lock = FileLock('{}.lock'.format(fname))
112+
with lock:
113+
with open(fname, 'w') as fp:
114+
fp.write(self.value_conv.serialize(value))
115+
116+
self.db[_key] = value
117+
self.fmtime[_key] = self.get_mtime(fname)
118+
119+
def delete(self, key):
120+
fname = os.path.join(self.fdir, key)
121+
if os.path.isfile(fname):
122+
lock = FileLock('{}.lock'.format(fname))
123+
with lock:
124+
os.unlink(fname)
125+
126+
try:
127+
del self.db[key]
128+
except KeyError:
129+
pass
130+
131+
def keys(self):
132+
"""
133+
Implements the dict.keys() method
134+
"""
135+
self.sync()
136+
for k in self.db.keys():
137+
yield self.key_conv.deserialize(k)
138+
139+
@staticmethod
140+
def get_mtime(fname):
141+
"""
142+
Find the time this file was last modified.
143+
144+
:param fname: File name
145+
:return: The last time the file was modified.
146+
"""
147+
try:
148+
mtime = os.stat(fname).st_mtime_ns
149+
except OSError:
150+
# The file might be right in the middle of being written
151+
# so sleep
152+
time.sleep(1)
153+
mtime = os.stat(fname).st_mtime_ns
154+
155+
return mtime
156+
157+
def is_changed(self, item):
158+
"""
159+
Find out if this item has been modified since last
160+
161+
:param item: A key
162+
:return: True/False
163+
"""
164+
fname = os.path.join(self.fdir, item)
165+
if os.path.isfile(fname):
166+
mtime = self.get_mtime(fname)
167+
168+
try:
169+
_ftime = self.fmtime[item]
170+
except KeyError: # Never been seen before
171+
self.fmtime[item] = mtime
172+
return True
173+
174+
if mtime > _ftime: # has changed
175+
self.fmtime[item] = mtime
176+
return True
177+
else:
178+
return False
179+
else:
180+
logger.error('Could not access {}'.format(fname))
181+
raise KeyError(item)
182+
183+
def _read_info(self, fname):
184+
if os.path.isfile(fname):
185+
try:
186+
lock = FileLock('{}.lock'.format(fname))
187+
with lock:
188+
info = open(fname, 'r').read().strip()
189+
return self.value_conv.deserialize(info)
190+
except Exception as err:
191+
logger.error(err)
192+
raise
193+
else:
194+
logger.error('No such file: {}'.format(fname))
195+
return None
196+
197+
def sync(self):
198+
"""
199+
Goes through the directory and builds a local cache based on
200+
the content of the directory.
201+
"""
202+
if not os.path.isdir(self.fdir):
203+
os.makedirs(self.fdir)
204+
# raise ValueError('No such directory: {}'.format(self.fdir))
205+
for f in os.listdir(self.fdir):
206+
fname = os.path.join(self.fdir, f)
207+
208+
if not os.path.isfile(fname):
209+
continue
210+
if fname.endswith('.lock'):
211+
continue
212+
213+
if f in self.fmtime:
214+
if self.is_changed(f):
215+
self.db[f] = self._read_info(fname)
216+
else:
217+
mtime = self.get_mtime(fname)
218+
try:
219+
self.db[f] = self._read_info(fname)
220+
except Exception as err:
221+
logger.warning('Bad content in {} ({})'.format(fname, err))
222+
else:
223+
self.fmtime[f] = mtime
224+
225+
def items(self):
226+
"""
227+
Implements the dict.items() method
228+
"""
229+
self.sync()
230+
for k, v in self.db.items():
231+
yield self.key_conv.deserialize(k), v
232+
233+
def clear(self):
234+
"""
235+
Completely resets the database. This means that all information in
236+
the local cache and on disc will be erased.
237+
"""
238+
if not os.path.isdir(self.fdir):
239+
os.makedirs(self.fdir, exist_ok=True)
240+
return
241+
242+
for f in os.listdir(self.fdir):
243+
self.delete(f)
244+
245+
def update(self, ava):
246+
"""
247+
Replaces what's in the database with a set of key, value pairs.
248+
Only data bound to keys that appear in ava will be affected.
249+
250+
:param ava: Dictionary
251+
"""
252+
for key, val in ava.items():
253+
self.set(key, val)
254+
255+
def __contains__(self, item):
256+
return self.key_conv.serialize(item) in self.db
257+
258+
def __iter__(self):
259+
return self.items()
260+
261+
def __call__(self, *args, **kwargs):
262+
return [self.key_conv.deserialize(k) for k in self.db.keys()]

0 commit comments

Comments
 (0)