Skip to content

Commit 97a0b84

Browse files
authored
[ENG-11516] Eliminate the request between metadata and download (#490)
* pass storage meta to provider download method * fix tests * add comments and docstrings * move sentry call and remove dead code
1 parent 996cec5 commit 97a0b84

3 files changed

Lines changed: 69 additions & 48 deletions

File tree

tests/providers/osfstorage/test_provider.py

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ class TestDownload:
7070

7171
@pytest.mark.asyncio
7272
@pytest.mark.aiohttpretty
73-
async def test_download_with_auth(self, provider_and_mock_one, download_response,
74-
download_path, mock_time):
73+
async def test_download(self, provider_and_mock_one, download_response,
74+
download_path, mock_time):
7575

7676
provider, inner_provider = provider_and_mock_one
7777

78-
uri, params = build_signed_url_with_auth(provider, 'GET', download_path.identifier,
79-
'download', version=None, mode=None)
78+
uri, params = build_signed_url_without_auth(provider, 'GET', download_path.identifier,
79+
'download', version=None, mode=None)
8080

8181
aiohttpretty.register_json_uri('GET', uri, body=download_response, params=params)
8282

@@ -92,29 +92,6 @@ async def test_download_with_auth(self, provider_and_mock_one, download_response
9292
inner_provider.download.assert_called_once_with(path=expected_path,
9393
display_name=expected_display_name)
9494

95-
@pytest.mark.asyncio
96-
@pytest.mark.aiohttpretty
97-
async def test_download_without_auth(self, provider_and_mock_one, download_response,
98-
download_path, mock_time):
99-
provider, inner_provider = provider_and_mock_one
100-
101-
provider.auth = {}
102-
url, params = build_signed_url_without_auth(provider, 'GET', download_path.identifier,
103-
'download', version=None, mode=None)
104-
aiohttpretty.register_json_uri('GET', url, params=params, body=download_response)
105-
106-
await provider.download(download_path)
107-
108-
assert provider.make_provider.called
109-
assert inner_provider.download.called
110-
assert aiohttpretty.has_call(method='GET', uri=url, params=params)
111-
provider.make_provider.assert_called_once_with(download_response['settings'])
112-
113-
expected_path = WaterButlerPath('/' + download_response['data']['path'])
114-
expected_display_name = download_response['data']['name']
115-
inner_provider.download.assert_called_once_with(path=expected_path,
116-
display_name=expected_display_name)
117-
11895
@pytest.mark.asyncio
11996
@pytest.mark.aiohttpretty
12097
async def test_download_without_id(self, provider_one, download_response, file_path,
@@ -141,7 +118,7 @@ async def test_download_with_display_name(self, provider_and_mock_one, download_
141118

142119
provider, inner_provider = provider_and_mock_one
143120

144-
uri, params = build_signed_url_with_auth(provider, 'GET', download_path.identifier,
121+
uri, params = build_signed_url_without_auth(provider, 'GET', download_path.identifier,
145122
'download', version=None, mode=None)
146123

147124
aiohttpretty.register_json_uri('GET', uri, body=download_response, params=params)

waterbutler/core/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ async def __anext__(self):
250250
else:
251251
return path.path.replace(self.parent_path.path, '', 1), EmptyStream()
252252

253-
return path.path.replace(self.parent_path.path, '', 1), await self.provider.download(path)
253+
return (
254+
path.path.replace(self.parent_path.path, '', 1),
255+
await self.provider.download(path, metadata=current[1])
256+
)
254257

255258

256259
class ZipStreamGeneratorPaginated(BaseZipStreamGenerator):
@@ -308,7 +311,7 @@ async def __anext__(self):
308311

309312
return (
310313
path.path.replace(self.parent_path.path, '', 1),
311-
await self.provider.download(path),
314+
await self.provider.download(path, metadata=current[1]),
312315
)
313316

314317

waterbutler/providers/osfstorage/provider.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import logging
77
from http import HTTPStatus
88

9+
import sentry_sdk
10+
911
from waterbutler.core import utils
1012
from waterbutler.core import signing
1113
from 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

Comments
 (0)