66import logging
77from http import HTTPStatus
88
9+ import sentry_sdk
10+
911from waterbutler .core import utils
1012from waterbutler .core import signing
1113from waterbutler .core import streams
@@ -195,7 +197,7 @@ def build_signed_url(method, url, data=None, params=None, ttl=100, **kwargs):
195197
196198 return url , data , params
197199
198- async def download (self , path , version = None , revision = None , mode = None , ** kwargs ):
200+ async def download (self , path , version = None , revision = None , mode = None , metadata = None , ** kwargs ):
199201 if not path .identifier :
200202 raise exceptions .NotFoundError (str (path ))
201203
@@ -209,28 +211,46 @@ async def download(self, path, version=None, revision=None, mode=None, **kwargs)
209211 # version could be 0 here
210212 version = revision
211213
212- # Capture user_id for analytics if user is logged in
213- user_param = {}
214- if self .auth .get ('id' , None ):
215- user_param = {'user' : self .auth ['id' ]}
216-
217- # osf storage metadata will return a virtual path within the provider
218- resp = await self .make_signed_request (
219- 'GET' ,
220- self .build_url (path .identifier , 'download' , version = version , mode = mode ),
221- expects = (200 , ),
222- params = user_param ,
223- throws = exceptions .DownloadError ,
224- )
225- data = await resp .json ()
214+ storage = None
215+ use_embedded_storage = False
216+ if metadata is not None :
217+ storage = metadata .raw .get ('storage' , None )
218+ use_embedded_storage = version is None and bool (storage and storage .get ('data' , None ))
219+ if not use_embedded_storage :
220+ with sentry_sdk .new_scope () as scope :
221+ scope .set_context ('osfstorage_data' , {
222+ 'node' : self .nid ,
223+ 'path' : str (path ),
224+ })
225+ sentry_sdk .capture_message (
226+ 'osfstorage download fell back to OSF /download endpoint' ,
227+ level = 'info' ,
228+ )
229+
230+ # DAZ passes minimal child metadata with an embedded ``storage`` block so we can
231+ # call the inner storage provider directly. When that payload is usable we skip
232+ # the per-file OSF ``/download`` hop; otherwise we fall back to fetching it.
233+ if use_embedded_storage :
234+ data = storage
235+ else :
236+ # osf storage metadata will return a virtual path within the provider
237+ resp = await self .make_signed_request (
238+ 'GET' ,
239+ self .build_url (path .identifier , 'download' , version = version , mode = mode ),
240+ expects = (200 , ),
241+ throws = exceptions .DownloadError ,
242+ )
243+ data = await resp .json ()
226244
227245 provider_object = self .make_provider (data ['settings' ])
228- name = data ['data' ].pop ('name' )
229- data ['data' ]['path' ] = await provider_object .validate_path ('/' + data ['data' ]['path' ])
246+ file_data = dict (data ['data' ])
247+ name = file_data .pop ('name' )
248+ file_data ['path' ] = await provider_object .validate_path ('/' + file_data ['path' ])
230249 download_kwargs = {}
231250 download_kwargs .update (kwargs )
232- download_kwargs .update (data [ 'data' ] )
251+ download_kwargs .update (file_data )
233252 download_kwargs ['display_name' ] = kwargs .get ('display_name' ) or name
253+
234254 return await provider_object .download (** download_kwargs )
235255
236256 async def upload (self , stream , path , ** kwargs ):
@@ -538,6 +558,12 @@ async def iter_children_pages(self, path, **kwargs):
538558 # ========== private ==========
539559
540560 async def _item_metadata (self , path , revision = None ):
561+ """Fetch metadata for a single file from the OSF.
562+
563+ :param path: file path whose ``identifier`` is the OSF file node id
564+ :param revision: optional file version identifier passed to OSF as ``revision``
565+ :return: :class:`OsfStorageFileMetadata` for the file
566+ """
541567 resp = await self .make_signed_request (
542568 'GET' ,
543569 self .build_url (path .identifier , revision = revision ),
@@ -546,15 +572,30 @@ async def _item_metadata(self, path, revision=None):
546572 return OsfStorageFileMetadata ((await resp .json ()), str (path ))
547573
548574 async def _children_metadata (self , path , limit = None , after = None , ** kwargs ):
575+ """Fetch folder children metadata from the OSF.
576+
577+ :param path: folder path whose ``identifier`` is the OSF folder node id
578+ :param limit: page size; when set, enables ORM pagination on OSF
579+ :param after: cursor (child node pk) for the next ORM page
580+ :return: list of :class:`OsfStorageFolderMetadata` and
581+ :class:`OsfStorageFileMetadata` instances
582+ """
549583 query = {
550584 'user_id' : self .auth .get ('id' ),
551585 'minimal' : kwargs .get ('minimal' , False ),
552586 }
587+
588+ # By default OSF serves minimal children via raw SQL on the backend. The Django
589+ # ORM implementation is used only when ``orm`` is explicitly requested or when
590+ # ``limit`` is set (pagination requires ORM). The raw SQL path may be removed in
591+ # the future in favor of ORM-only responses.
553592 if limit is not None :
554593 query ['orm' ] = True
555594 query ['limit' ] = limit
556595 if after is not None :
557596 query ['after' ] = after
597+ elif kwargs .get ('orm' ) is not None :
598+ query ['orm' ] = kwargs ['orm' ]
558599
559600 resp = await self .make_signed_request (
560601 'GET' ,
0 commit comments