Skip to content

Commit 3aa5f8d

Browse files
committed
Added LevelDB-based attribute container store
1 parent f4d52d7 commit 3aa5f8d

6 files changed

Lines changed: 588 additions & 2 deletions

File tree

acstore/leveldb_store.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""LevelDB-based attribute container store."""
2+
3+
import ast
4+
import json
5+
import os
6+
7+
import leveldb # pylint: disable=import-error
8+
9+
from acstore import interface
10+
from acstore.containers import interface as containers_interface
11+
from acstore.helpers import json_serializer
12+
13+
14+
class LevelDBAttributeContainerStore(interface.AttributeContainerStoreWithReadCache):
15+
"""LevelDB-based attribute container store.
16+
17+
Attributes:
18+
format_version (int): storage format version.
19+
serialization_format (str): serialization format.
20+
"""
21+
22+
_FORMAT_VERSION = 20230312
23+
24+
def __init__(self):
25+
"""Initializes a LevelDB attribute container store."""
26+
super().__init__()
27+
self._is_open = False
28+
self._json_serializer = json_serializer.AttributeContainerJSONSerializer
29+
self._leveldb_database = None
30+
31+
self.format_version = self._FORMAT_VERSION
32+
self.serialization_format = "json"
33+
34+
def _GetNumberOfAttributeContainerKeys(self, container_type):
35+
"""Retrieves the number of attribute container keys.
36+
37+
Args:
38+
container_type (str): attribute container type.
39+
40+
Returns:
41+
int: the number of keys of a specified attribute container type.
42+
"""
43+
first_key = f"{container_type:s}.1".encode("utf8")
44+
45+
try:
46+
# Check if the first key exists otherwise RangeIter will return all keys.
47+
self._leveldb_database.Get(first_key)
48+
except KeyError:
49+
return 0
50+
51+
return sum(1 for _ in self._leveldb_database.RangeIter(key_from=first_key))
52+
53+
def _RaiseIfNotReadable(self):
54+
"""Raises if the attribute container store is not readable.
55+
56+
Raises:
57+
IOError: when the attribute container store is closed.
58+
OSError: when the attribute container store is closed.
59+
"""
60+
if not self._is_open:
61+
raise IOError("Unable to read from closed attribute container store.")
62+
63+
def _RaiseIfNotWritable(self):
64+
"""Raises if the attribute container store is not writable.
65+
66+
Raises:
67+
IOError: when the attribute container store is closed or read-only.
68+
OSError: when the attribute container store is closed or read-only.
69+
"""
70+
if not self._is_open:
71+
raise IOError("Unable to write to closed attribute container store.")
72+
73+
def _WriteExistingAttributeContainer(self, container):
74+
"""Writes an existing attribute container to the store.
75+
76+
Args:
77+
container (AttributeContainer): attribute container.
78+
"""
79+
identifier = container.GetIdentifier()
80+
81+
key = identifier.CopyToString().encode("utf8")
82+
83+
self._leveldb_database.Delete(key)
84+
85+
json_dict = self._json_serializer.ConvertAttributeContainerToJSON(container)
86+
json_string = json.dumps(json_dict)
87+
value = json_string.encode("utf8")
88+
89+
self._leveldb_database.Put(key=key, value=value)
90+
91+
def _WriteNewAttributeContainer(self, container):
92+
"""Writes a new attribute container to the store.
93+
94+
Args:
95+
container (AttributeContainer): attribute container.
96+
"""
97+
next_sequence_number = self._GetAttributeContainerNextSequenceNumber(
98+
container.CONTAINER_TYPE
99+
)
100+
101+
identifier = containers_interface.AttributeContainerIdentifier(
102+
name=container.CONTAINER_TYPE, sequence_number=next_sequence_number
103+
)
104+
container.SetIdentifier(identifier)
105+
106+
key = identifier.CopyToString().encode("utf8")
107+
108+
json_dict = self._json_serializer.ConvertAttributeContainerToJSON(container)
109+
json_string = json.dumps(json_dict)
110+
value = json_string.encode("utf8")
111+
112+
self._leveldb_database.Put(key=key, value=value)
113+
114+
self._CacheAttributeContainerByIndex(container, next_sequence_number - 1)
115+
116+
def Close(self):
117+
"""Closes the file.
118+
119+
Raises:
120+
IOError: if the attribute container store is already closed.
121+
OSError: if the attribute container store is already closed.
122+
"""
123+
if not self._is_open:
124+
raise IOError("Attribute container store already closed.")
125+
126+
self._leveldb_database = None
127+
128+
self._is_open = False
129+
130+
def GetAttributeContainerByIdentifier(self, container_type, identifier):
131+
"""Retrieves a specific type of container with a specific identifier.
132+
133+
Args:
134+
container_type (str): container type.
135+
identifier (AttributeContainerIdentifier): attribute container identifier.
136+
137+
Returns:
138+
AttributeContainer: attribute container or None if not available.
139+
"""
140+
key = identifier.CopyToString().encode("utf8")
141+
142+
try:
143+
value = self._leveldb_database.Get(key)
144+
except KeyError:
145+
return None
146+
147+
json_string = value.decode("utf8")
148+
json_dict = json.loads(json_string)
149+
150+
container = self._json_serializer.ConvertJSONToAttributeContainer(json_dict)
151+
container.SetIdentifier(identifier)
152+
return container
153+
154+
def GetAttributeContainerByIndex(self, container_type, index):
155+
"""Retrieves a specific attribute container.
156+
157+
Args:
158+
container_type (str): attribute container type.
159+
index (int): attribute container index.
160+
161+
Returns:
162+
AttributeContainer: attribute container or None if not available.
163+
"""
164+
identifier = containers_interface.AttributeContainerIdentifier(
165+
name=container_type, sequence_number=index + 1
166+
)
167+
168+
key = identifier.CopyToString().encode("utf8")
169+
170+
try:
171+
value = self._leveldb_database.Get(key)
172+
except KeyError:
173+
return None
174+
175+
json_string = value.decode("utf8")
176+
json_dict = json.loads(json_string)
177+
178+
container = self._json_serializer.ConvertJSONToAttributeContainer(json_dict)
179+
container.SetIdentifier(identifier)
180+
return container
181+
182+
def GetAttributeContainers(self, container_type, filter_expression=None):
183+
"""Retrieves a specific type of attribute containers.
184+
185+
Args:
186+
container_type (str): attribute container type.
187+
filter_expression (Optional[str]): expression to filter the resulting
188+
attribute containers by.
189+
190+
Yields:
191+
AttributeContainer: attribute container.
192+
"""
193+
last_key_index = self._attribute_container_sequence_numbers[container_type]
194+
195+
first_key = f"{container_type:s}.1".encode("utf8")
196+
last_key = f"{container_type:s}.{last_key_index:d}".encode("utf8")
197+
198+
if filter_expression:
199+
expression_ast = ast.parse(filter_expression, mode="eval")
200+
filter_expression = compile(expression_ast, "<string>", mode="eval")
201+
202+
for key, value in self._leveldb_database.RangeIter(
203+
key_from=first_key, key_to=last_key
204+
):
205+
json_string = value.decode("utf8")
206+
json_dict = json.loads(json_string)
207+
208+
container = self._json_serializer.ConvertJSONToAttributeContainer(json_dict)
209+
if container.MatchesExpression(filter_expression):
210+
key = key.decode("utf8")
211+
identifier = containers_interface.AttributeContainerIdentifier()
212+
identifier.CopyFromString(key)
213+
214+
container.SetIdentifier(identifier)
215+
yield container
216+
217+
def GetNumberOfAttributeContainers(self, container_type):
218+
"""Retrieves the number of a specific type of attribute containers.
219+
220+
Args:
221+
container_type (str): attribute container type.
222+
223+
Returns:
224+
int: the number of containers of a specified type.
225+
"""
226+
return self._attribute_container_sequence_numbers[container_type]
227+
228+
def HasAttributeContainers(self, container_type):
229+
"""Determines if a store contains a specific type of attribute container.
230+
231+
Args:
232+
container_type (str): attribute container type.
233+
234+
Returns:
235+
bool: True if the store contains the specified type of attribute
236+
containers.
237+
"""
238+
return self._attribute_container_sequence_numbers[container_type] > 0
239+
240+
def Open(self, path=None, **unused_kwargs): # pylint: disable=arguments-differ
241+
"""Opens the store.
242+
243+
Args:
244+
path (Optional[str]): path to the attribute container store.
245+
246+
Raises:
247+
IOError: if the attribute container store is already opened or if
248+
the database cannot be connected.
249+
OSError: if the attribute container store is already opened or if
250+
the database cannot be connected.
251+
ValueError: if path is missing.
252+
"""
253+
if self._is_open:
254+
raise IOError("Attribute container store already opened.")
255+
256+
if not path:
257+
raise ValueError("Missing path.")
258+
259+
path = os.path.abspath(path)
260+
261+
self._leveldb_database = leveldb.LevelDB(path)
262+
263+
self._is_open = True
264+
265+
# TODO: read metadata.
266+
267+
# Initialize next_sequence_number based on the file contents so that
268+
# AttributeContainerIdentifier points to the correct attribute container.
269+
for container_type in self._containers_manager.GetContainerTypes():
270+
next_sequence_number = self._GetNumberOfAttributeContainerKeys(
271+
container_type
272+
)
273+
self._SetAttributeContainerNextSequenceNumber(
274+
container_type, next_sequence_number
275+
)

config/appveyor/install.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Script to set up tests on AppVeyor Windows.
22

3-
$Dependencies = "PyYAML"
3+
$Dependencies = "PyYAML leveldb"
44

55
If ($Dependencies.Length -gt 0)
66
{

config/dpkg/control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Homepage: https://github.com/log2timeline/acstore
99

1010
Package: python3-acstore
1111
Architecture: all
12-
Depends: python3-yaml (>= 3.10), ${misc:Depends}
12+
Depends: python3-leveldb (>= 0.20), python3-yaml (>= 3.10), ${misc:Depends}
1313
Description: Python 3 module of ACStore
1414
ACStore, or Attribute Container Storage, provides a stand-alone
1515
implementation to read and write attribute container storage files.

dependencies.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
[leveldb]
2+
dpkg_name: python3-leveldb
3+
is_optional: true
4+
minimum_version: 0.20
5+
rpm_name: python3-leveldb
6+
17
[yaml]
28
dpkg_name: python3-yaml
39
l2tbinaries_name: PyYAML

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ classifiers = [
1818
]
1919
requires-python = ">=3.10"
2020
dependencies = [
21+
"leveldb @ git+https://github.com/joachimmetz/py-leveldb.git@0.202",
2122
"PyYAML >= 3.10",
2223
]
2324

0 commit comments

Comments
 (0)