88- recursive .list method
99- soft deletion
1010"""
11+
1112from binascii import hexlify
1213from collections import Counter
1314from contextlib import contextmanager
15+ import logging
1416import os
1517import time
1618from typing import Iterator , Optional
2426from .backends .s3 import get_s3_backend
2527from .constants import DEL_SUFFIX
2628
29+ logger = logging .getLogger (__name__ )
30+
2731
2832def get_backend (url ):
2933 """parse backend URL and return a backend instance (or None)"""
@@ -118,7 +122,7 @@ def close(self) -> None:
118122 self .backend .close ()
119123
120124 @contextmanager
121- def _stats_updater (self , key ):
125+ def _stats_updater (self , key , msg ):
122126 """update call counters and overall times, also emulate latency and bandwidth"""
123127 # do not use this in generators!
124128 volume_before = self ._stats_get_volume (key )
@@ -132,8 +136,10 @@ def _stats_updater(self, key):
132136 if remaining_time > 0.0 :
133137 time .sleep (remaining_time )
134138 end = time .perf_counter_ns ()
139+ overall_time = end - start
135140 self ._stats [f"{ key } _calls" ] += 1
136- self ._stats [f"{ key } _time" ] += end - start
141+ self ._stats [f"{ key } _time" ] += overall_time
142+ logger .debug (f"borgstore { msg } ({ volume } B { overall_time / 1e6 :0.1f} ms)" )
137143
138144 def _stats_update_volume (self , key , amount ):
139145 self ._stats [f"{ key } _volume" ] += amount
@@ -196,11 +202,11 @@ def find(self, name: str, *, deleted=False) -> str:
196202 return nested_name
197203
198204 def info (self , name : str , * , deleted = False ) -> ItemInfo :
199- with self ._stats_updater ("info" ):
205+ with self ._stats_updater ("info" , f"info { name } deleted: { deleted } " ):
200206 return self .backend .info (self .find (name , deleted = deleted ))
201207
202208 def load (self , name : str , * , size = None , offset = 0 , deleted = False ) -> bytes :
203- with self ._stats_updater ("load" ):
209+ with self ._stats_updater ("load" , f"load { name } offset: { offset } size: { size } deleted: { deleted } " ):
204210 result = self .backend .load (self .find (name , deleted = deleted ), size = size , offset = offset )
205211 self ._stats_update_volume ("load" , len (result ))
206212 return result
@@ -209,7 +215,7 @@ def store(self, name: str, value: bytes) -> None:
209215 # note: using .find here will:
210216 # - overwrite an existing item (level stays same)
211217 # - write to the last level if no existing item is found.
212- with self ._stats_updater ("store" ):
218+ with self ._stats_updater ("store" , f"store { name } " ):
213219 self .backend .store (self .find (name ), value )
214220 self ._stats_update_volume ("store" , len (value ))
215221
@@ -219,7 +225,7 @@ def delete(self, name: str, *, deleted=False) -> None:
219225
220226 See also .move(name, delete=True) for "soft" deletion.
221227 """
222- with self ._stats_updater ("delete" ):
228+ with self ._stats_updater ("delete" , f"delete { name } deleted: { deleted } " ):
223229 self .backend .delete (self .find (name , deleted = deleted ))
224230
225231 def move (
@@ -236,22 +242,26 @@ def move(
236242 # use case: keep name, but soft "delete" the item
237243 nested_name = self .find (name , deleted = False )
238244 nested_new_name = nested_name + DEL_SUFFIX
245+ msg = f"soft_delete { name } deleted: { deleted } "
239246 elif undelete :
240247 # use case: keep name, undelete a previously soft "deleted" item
241248 nested_name = self .find (name , deleted = True )
242249 nested_new_name = nested_name .removesuffix (DEL_SUFFIX )
250+ msg = f"soft_undelete { name } deleted: { deleted } "
243251 elif change_level :
244252 # use case: keep name, changing to another nesting level
245253 suffix = DEL_SUFFIX if deleted else None
246254 nested_name = self .find (name , deleted = deleted )
247255 nested_new_name = nest (name , self ._get_levels (name )[- 1 ], add_suffix = suffix )
256+ msg = f"change_level { name } deleted: { deleted } "
248257 else :
249258 # generic use (be careful!)
250259 if not new_name :
251260 raise ValueError ("generic move needs new_name to be given." )
252261 nested_name = self .find (name , deleted = deleted )
253262 nested_new_name = self .find (new_name , deleted = deleted )
254- with self ._stats_updater ("move" ):
263+ msg = f"rename { name } -> { new_name } ] deleted: { deleted } "
264+ with self ._stats_updater ("move" , msg + f" [{ nested_name } -> { nested_new_name } ]" ):
255265 self .backend .move (nested_name , nested_new_name )
256266
257267 def list (self , name : str , deleted : bool = False ) -> Iterator [ItemInfo ]:
@@ -264,8 +274,15 @@ def list(self, name: str, deleted: bool = False) -> Iterator[ItemInfo]:
264274 backend.list giving us sorted names implies store.list is also sorted, if all items are stored on same level.
265275 """
266276 # we need this wrapper due to the recursion - we only want to increment list_calls once:
277+ logger .debug (f"borgstore list_start { name } deleted: { deleted } " )
267278 self ._stats ["list_calls" ] += 1
268- yield from self ._list (name , deleted = deleted )
279+ count = 0
280+ for info in self ._list (name , deleted = deleted ):
281+ count += 1
282+ yield info
283+ # note: as this is a generator, we do not measure the execution time because
284+ # that would include the time needed by the caller to process the infos.
285+ logger .debug (f"borgstore list_end { name } deleted: { deleted } ({ count } )" )
269286
270287 def _list (self , name : str , deleted : bool = False ) -> Iterator [ItemInfo ]:
271288 # as the backend.list method only supports non-recursive listing and
0 commit comments