Skip to content

Commit a1073ba

Browse files
Add pagination support to ZipStreamGenerator and update OSFStorageProvider for minimal metadata retrieval
1 parent e913300 commit a1073ba

2 files changed

Lines changed: 133 additions & 2 deletions

File tree

waterbutler/core/utils.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,78 @@ async def __anext__(self):
223223
return path.path.replace(self.parent_path.path, '', 1), await self.provider.download(path)
224224

225225

226+
class ZipStreamGeneratorPaginated:
227+
228+
def __init__(self, provider, root_path, **kwargs):
229+
self.provider = provider
230+
self.parent_path = root_path.parent if root_path.is_file else root_path
231+
self.root_path = root_path
232+
self.kwargs = kwargs
233+
234+
self.remaining = []
235+
self.initialized = False
236+
237+
async def _initialize(self):
238+
if self.initialized:
239+
return
240+
241+
self.initialized = True
242+
243+
if self.root_path.is_file:
244+
metadata = await self.provider.metadata(
245+
self.root_path,
246+
**self.kwargs,
247+
)
248+
self.remaining.append((self.root_path.parent, metadata))
249+
return
250+
251+
async for page in self.provider.iter_children_pages(
252+
self.root_path,
253+
**self.kwargs,
254+
):
255+
256+
self.remaining.extend(
257+
(self.root_path, item)
258+
for item in page
259+
)
260+
261+
async def __aiter__(self):
262+
return self
263+
264+
async def __anext__(self):
265+
if not self.initialized:
266+
await self._initialize()
267+
268+
if not self.remaining:
269+
raise StopAsyncIteration
270+
271+
current = self.remaining.pop(0)
272+
path = self.provider.path_from_metadata(*current)
273+
274+
if path.is_dir:
275+
async for page in self.provider.iter_children_pages(
276+
path,
277+
**self.kwargs,
278+
):
279+
if not page:
280+
return (
281+
path.path.replace(self.parent_path.path, '', 1),
282+
EmptyStream(),
283+
)
284+
285+
self.remaining.extend(
286+
(path, item)
287+
for item in page
288+
)
289+
290+
return await self.__anext__()
291+
292+
return (
293+
path.path.replace(self.parent_path.path, '', 1),
294+
await self.provider.download(path),
295+
)
296+
297+
226298
class RequestHandlerContext:
227299

228300
def __init__(self, request_coro):

waterbutler/providers/osfstorage/provider.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,13 @@ async def delete(self, path, confirm_delete=0, **kwargs):
317317
)).release()
318318

319319
async def zip(self, path: wb_path.WaterButlerPath, **kwargs) -> asyncio.StreamReader:
320-
# add query param 'minimal' to avoid unnecessary metadata in the response.
321-
return await super().zip(path, **kwargs, minimal=True)
320+
"""Streams a Zip archive of the given folder
321+
322+
:param path: ( :class:`.WaterButlerPath` ) The folder to compress
323+
"""
324+
kwargs['minimal'] = True
325+
326+
return streams.ZipStreamReader(utils.ZipStreamGeneratorPaginated(self, path, **kwargs)) # type: ignore
322327

323328
async def metadata(self, path, **kwargs):
324329
if path.identifier is None:
@@ -519,6 +524,32 @@ async def copy(self,
519524

520525
return meta_data, created
521526

527+
async def iter_children_pages(self, path, **kwargs):
528+
url = self.build_url(
529+
path.identifier,
530+
'children',
531+
user_id=self.auth.get('id'),
532+
minimal=kwargs.get('minimal'),
533+
limit=kwargs.get('limit', 5),
534+
orm=True
535+
)
536+
537+
page, next_url = await self._fetch_metadata_page(url, path, **kwargs)
538+
while True:
539+
next_task = None
540+
541+
if next_url:
542+
next_task = asyncio.create_task(
543+
self._fetch_metadata_page(next_url, path, **kwargs)
544+
)
545+
546+
yield page
547+
548+
if next_task is None:
549+
break
550+
551+
page, next_url = await next_task
552+
522553
# ========== private ==========
523554

524555
async def _item_metadata(self, path, revision=None):
@@ -545,6 +576,34 @@ async def _children_metadata(self, path, **kwargs):
545576
ret.append(OsfStorageFileMetadata(item, str(path.child(item['name']))))
546577
return ret
547578

579+
async def _fetch_metadata_page(self, url, path, **kwargs):
580+
resp = await self.make_signed_request(
581+
'GET',
582+
url,
583+
expects=(200,),
584+
)
585+
resp_json = await resp.json()
586+
587+
if resp_json:
588+
next_url = self.build_url(
589+
path.identifier,
590+
'children',
591+
user_id=self.auth.get('id'),
592+
minimal=kwargs.get('minimal'),
593+
limit=kwargs.get('limit', 5),
594+
after=resp_json[-1]['id'] if resp_json else None,
595+
orm=True
596+
)
597+
else:
598+
return [], None
599+
ret = []
600+
for item in resp_json:
601+
if item['kind'] == 'folder':
602+
ret.append(OsfStorageFolderMetadata(item, str(path.child(item['name'], folder=True))))
603+
else:
604+
ret.append(OsfStorageFileMetadata(item, str(path.child(item['name']))))
605+
return ret, next_url
606+
548607
async def _delete_folder_contents(self, path, **kwargs):
549608
"""Delete the contents of a folder. For use against provider root.
550609

0 commit comments

Comments
 (0)