Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle filenames for basic streaming transfer adapter. #68

Merged
merged 8 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions giftless/transfer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
from abc import ABC
from functools import partial
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from urllib.parse import urlencode

from giftless.auth import Authentication, authentication
from giftless.util import get_callable
from giftless.util import add_query_params, get_callable
from giftless.view import ViewProvider

_registered_adapters: Dict[str, 'TransferAdapter'] = {}
Expand Down Expand Up @@ -55,9 +54,8 @@ def _preauth_url(self, original_url: str, org: str, repo: str, actions: Optional

params = self._auth_module.preauth_handler.get_authz_query_params(identity, org, repo, actions, oid,
lifetime=lifetime)
qs = urlencode(params)
sep = '&' if '?' in original_url else '?'
return f'{original_url}{sep}{qs}'

return add_query_params(original_url, params)

def _preauth_headers(self, org: str, repo: str, actions: Optional[Set[str]] = None,
oid: Optional[str] = None, lifetime: Optional[int] = None) -> Dict[str, str]:
Expand Down
17 changes: 14 additions & 3 deletions giftless/transfer/basic_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from giftless.schema import ObjectSchema
from giftless.storage import StreamingStorage, VerifiableStorage
from giftless.transfer import PreAuthorizingTransferAdapter, ViewProvider
from giftless.util import get_callable
from giftless.util import add_query_params, get_callable, safe_filename
from giftless.view import BaseView


Expand Down Expand Up @@ -80,9 +80,14 @@ def get(self, organization, repo, oid):
"""
self._check_authorization(organization, repo, Permission.READ, oid=oid)
path = os.path.join(organization, repo)

filename = request.args.get('filename')
filename = safe_filename(filename)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'} if filename else None

if self.storage.exists(path, oid):
file = self.storage.get(path, oid)
return Response(file, direct_passthrough=True, status=200)
return Response(file, direct_passthrough=True, status=200, headers=headers)
else:
raise NotFound("The object was not found")

Expand Down Expand Up @@ -144,9 +149,15 @@ def download(self, organization: str, repo: str, oid: str, size: int,

else:
download_url = ObjectsView.get_storage_url('get', organization, repo, oid)
preauth_url = self._preauth_url(download_url, organization, repo, actions={'read'}, oid=oid)

if extra and 'filename' in extra:
params = {'filename': extra['filename']}
preauth_url = add_query_params(preauth_url, params)

response['actions'] = {
"download": {
"href": self._preauth_url(download_url, organization, repo, actions={'read'}, oid=oid),
"href": preauth_url,
"header": {},
"expires_in": self.action_lifetime
}
Expand Down
32 changes: 31 additions & 1 deletion giftless/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Miscellanea
"""
import importlib
from typing import Any, Callable, Iterable, Optional
from typing import Any, Callable, Dict, Iterable, Optional
from urllib.parse import urlencode


def get_callable(callable_str: str, base_package: Optional[str] = None) -> Callable:
Expand Down Expand Up @@ -52,3 +53,32 @@ def to_iterable(val: Any) -> Iterable:
if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
return val
return (val,)


def add_query_params(url: str, params: Dict[str, Any]) -> str:
"""Safely add query params to a url that may or may not already contain
query params.

>>> add_query_params('https://example.org', {'param1': 'value1', 'param2': 'value2'})
'https://example.org?param1=value1&param2=value2'

>>> add_query_params('https://example.org?param1=value1', {'param2': 'value2'})
'https://example.org?param1=value1&param2=value2'
"""
urlencoded_params = urlencode(params)
separator = '&' if '?' in url else '?'
return f'{url}{separator}{urlencoded_params}'


def safe_filename(original_filename: str) -> str:
"""Returns a filename safe to use in HTTP headers, formed from the
given original filename.

>>> safe_filename("example(1).txt")
'example1.txt'

>>> safe_filename("_ex@mple 2%.old.xlsx")
'_exmple2.old.xlsx'
"""
valid_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.'
return ''.join(c for c in original_filename if c in valid_chars)