3030import jsonpatch
3131import jsonpointer
3232
33- from . import util
34- from .util import WalletFileException , profiler , sticky_property
33+ from .util import WalletFileException , profiler , sticky_property , MyEncoder
3534from .logging import Logger
36- from .stored_dict import StoredDict , _FLEX_KEY , registered_names , registered_keys , _convert_dict_key , _convert_dict_value
35+ from .stored_dict import _FLEX_KEY , BaseDB , _convert_dict_key , _convert_dict_value
36+ from .storage import FileStorage
3737
3838
39- if TYPE_CHECKING :
40- from .storage import WalletStorage
41-
4239
4340# We monkeypatch exceptions in the jsonpatch package to ensure they do not contain secrets from the DB.
4441# We often log exceptions and offer to send them to the crash reporter, so they must not contain secrets.
@@ -84,36 +81,155 @@ def wrapper(self, *args, **kwargs):
8481
8582
8683
87-
88- class JsonDB (Logger ):
84+ class JsonDB (BaseDB ):
8985
9086 def __init__ (
91- self ,
92- s : str ,
93- * ,
94- storage : Optional ['WalletStorage' ] = None ,
95- encoder = None ,
96- upgrader = None ,
87+ self ,
88+ path : Optional [str ],
89+ * ,
90+ allow_partial_writes = True ,
91+ init_db = True ,
92+ encoder = MyEncoder ,
93+ upgrader = None ,
9794 ):
98- Logger .__init__ (self )
95+ BaseDB .__init__ (self , path )
96+ self ._is_closed = True
9997 self .lock = threading .RLock ()
100- self .storage = storage
10198 self .encoder = encoder
99+ self .upgrader = upgrader
102100 self .pending_changes = [] # type: List[str]
103101 self ._modified = False
104- # load data
105- data = self .load_data (s )
106- if upgrader :
107- data , was_upgraded = upgrader (data )
102+ if self .path :
103+ self .storage = FileStorage (path , allow_partial_writes = allow_partial_writes )
104+ if init_db and not self .is_encrypted ():
105+ # open DB if file is not encrypted
106+ # otherwise, this will be called in self.decrypt
107+ self .init_db ()
108+ else :
109+ self .storage = None
110+ self .set_data ('{}' )
111+ self ._is_closed = False
112+
113+ def set_data (self , json_str ):
114+ data = self .load_data (json_str )
115+ if self .upgrader :
116+ data , was_upgraded = self .upgrader (data )
108117 self ._modified |= was_upgraded
109- # convert json to python objects
110- data = self ._convert_dict ([], data )
111- # convert dict to StoredDict
112- self .data = StoredDict (data , self )
113- self .data .set_parent (key = '' , parent = None )
118+ self .json_data = self ._convert_dict ([], data )
119+
120+ def init_db (self ):
121+ if self .storage .is_encrypted ():
122+ assert self .storage .is_past_initial_decryption ()
123+ json_str = self .storage .read ()
124+ self .set_data (json_str )
114125 # write file in case there was a db upgrade
115- if self .storage and self .storage .file_exists ():
116- self .write_and_force_consolidation ()
126+ self .write_and_force_consolidation ()
127+ self ._is_closed = False
128+
129+ def decrypt (self , password : str ):
130+ self .storage .decrypt (password )
131+ json_str = self .storage .read ()
132+ self .json_data = self .load_data (json_str )
133+ self ._is_closed = False
134+
135+ def check_password (self , password ):
136+ self .storage .check_password (password )
137+
138+ def supports_file_encryption (self ):
139+ return bool (self .storage )
140+
141+ def get_encryption_version (self ):
142+ return self .storage .get_encryption_version ()
143+
144+ def is_encrypted (self ):
145+ return self .storage and self .storage .is_encrypted ()
146+
147+ def is_encrypted_with_user_pw (self ) -> bool :
148+ return self .storage and self .storage .is_encrypted_with_user_pw ()
149+
150+ def is_encrypted_with_hw_device (self ) -> bool :
151+ return self .storage and self .storage .is_encrypted_with_hw_device ()
152+
153+ def set_password (self , password : str , enc_version = None ):
154+ self .storage .set_password (password , enc_version = enc_version )
155+
156+ def file_exists (self ):
157+ return self .storage and self .storage .file_exists ()
158+
159+ def _subdict (self , path ):
160+ d = self .json_data
161+ for k in path [1 :]:
162+ d = d [k ]
163+ return d
164+
165+ def iter_keys (self , path ):
166+ d = self ._subdict (path )
167+ return d .__iter__ ()
168+
169+ def dict_len (self , path ):
170+ d = self ._subdict (path )
171+ return len (d )
172+
173+ def contains (self , path , key ):
174+ d = self ._subdict (path )
175+ return key in d
176+
177+ def replace (self , path , key , value ):
178+ # called by setattr
179+ self .db_replace (path , key , value )
180+
181+ @modifier
182+ def put (self , path , key , value ):
183+ d = self ._subdict (path )
184+ is_new = key not in d
185+ d [key ] = value
186+ self .db_add (path , key , value ) if is_new else self .db_replace (path , key , value )
187+
188+ @modifier
189+ def clear (self , path ):
190+ d = self ._subdict (path )
191+ d .clear ()
192+
193+ def get (self , hint , key ):
194+ return hint [key ]
195+
196+ def get_hint (self , path ):
197+ return self ._subdict (path )
198+
199+ @modifier
200+ def remove (self , path , key ):
201+ d = self ._subdict (path )
202+ d .pop (key )
203+ self .db_remove (path , key )
204+
205+ @modifier
206+ def list_append (self , path , item ):
207+ _list = self ._subdict (path )
208+ n = len (_list )
209+ _list .append (item )
210+ self .db_add (path , str (n ), item )
211+
212+ def list_index (self , path , item ):
213+ _list = self ._subdict (path )
214+ return _list .index (item )
215+
216+ def list_len (self , path ):
217+ _list = self ._subdict (path )
218+ return len (_list )
219+
220+ @modifier
221+ def list_clear (self , path ):
222+ _list = self ._subdict (path )
223+ _list .clear ()
224+ self .db_remove (path [:- 1 ], path [- 1 ])
225+ self .db_add (path [:- 1 ], path [- 1 ], [])
226+
227+ @modifier
228+ def list_remove (self , path , item ):
229+ _list = self ._subdict (path )
230+ n = _list .index (item )
231+ _list .remove (item )
232+ self .db_remove (path , str (n )) # fixme: keys
117233
118234 def load_data (self , s : str ) -> Dict [str , Any ]:
119235 if s == '' :
@@ -185,71 +301,30 @@ def add_patch(self, patch):
185301 self .pending_changes .append (json .dumps (patch , cls = self .encoder ))
186302 self .set_modified (True )
187303
188- def add (self , path , key : _FLEX_KEY , value ) -> None :
304+ def db_add (self , path , key : _FLEX_KEY , value ) -> None :
189305 assert isinstance (key , _FLEX_KEY ), repr (key )
190306 self .add_patch ({'op' : 'add' , 'path' : key_path (path , key ), 'value' : value })
191307
192- def replace (self , path , key : _FLEX_KEY , value ) -> None :
308+ def db_replace (self , path , key : _FLEX_KEY , value ) -> None :
193309 assert isinstance (key , _FLEX_KEY ), repr (key )
194310 self .add_patch ({'op' : 'replace' , 'path' : key_path (path , key ), 'value' : value })
195311
196- def remove (self , path , key : _FLEX_KEY ) -> None :
312+ def db_remove (self , path , key : _FLEX_KEY ) -> None :
197313 assert isinstance (key , _FLEX_KEY ), repr (key )
198314 self .add_patch ({'op' : 'remove' , 'path' : key_path (path , key )})
199315
200- @locked
201- def get (self , key , default = None ):
202- v = self .data .get (key )
203- if v is None :
204- v = default
205- return v
206-
207- @modifier
208- def put (self , key , value ):
209- try :
210- json .dumps (key , cls = self .encoder )
211- json .dumps (value , cls = self .encoder )
212- except Exception :
213- self .logger .info (f"json error: cannot save { repr (key )} ({ repr (value )} )" )
214- return False
215- if value is not None :
216- if self .data .get (key ) != value :
217- self .data [key ] = copy .deepcopy (value )
218- return True
219- elif key in self .data :
220- self .data .pop (key )
221- return True
222- return False
223-
224- @locked
225- def get_dict (self , name ) -> dict :
226- # Warning: interacts un-intuitively with 'put': certain parts
227- # of 'data' will have pointers saved as separate variables.
228- if name not in self .data :
229- self .data [name ] = {}
230- return self .data [name ]
231-
232- @locked
233- def get_stored_item (self , key , default ) -> dict :
234- if key not in self .data :
235- self .data [key ] = default
236- return self .data [key ]
237-
238316 @locked
239317 def dump (self , * , human_readable : bool = True ) -> str :
240318 """Serializes the DB as a string.
241319 'human_readable': makes the json indented and sorted, but this is ~2x slower
242320 """
243321 return json .dumps (
244- self .data ,
322+ self .json_data ,
245323 indent = 4 if human_readable else None ,
246324 sort_keys = bool (human_readable ),
247325 cls = self .encoder ,
248326 )
249327
250- def _should_convert_to_stored_dict (self , key ) -> bool :
251- return True
252-
253328 def _convert_dict_key (self , path : List [str ], key : str ) -> _FLEX_KEY :
254329 return _convert_dict_key (path , key )
255330
@@ -272,11 +347,20 @@ def _convert_dict(self, path: List[str], data: dict):
272347
273348 @locked
274349 def write (self ):
350+ if not self .storage :
351+ return
275352 if self .storage .should_do_full_write_next ():
276353 self .write_and_force_consolidation ()
277354 else :
278355 self ._append_pending_changes ()
279356
357+ def close (self ):
358+ # do not call write
359+ self ._is_closed = True
360+
361+ def is_closed (self ):
362+ return self ._is_closed
363+
280364 @locked
281365 def _append_pending_changes (self ):
282366 if threading .current_thread ().daemon :
0 commit comments