Skip to content

Commit

Permalink
Merge pull request #90 from amercader/feature/content-type
Browse files Browse the repository at this point in the history
Handle Content-Type and Content-Disposition headers
  • Loading branch information
shevron authored May 14, 2021
2 parents 640d61f + 9953832 commit 06f0f2c
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 16 deletions.
8 changes: 8 additions & 0 deletions giftless/storage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import mimetypes
from abc import ABC
from typing import Any, BinaryIO, Dict, Iterable, Optional

Expand Down Expand Up @@ -32,6 +33,9 @@ def exists(self, prefix: str, oid: str) -> bool:
def get_size(self, prefix: str, oid: str) -> int:
pass

def get_mime_type(self, prefix: str, oid: str) -> Optional[str]:
return "application/octet-stream"

def verify_object(self, prefix: str, oid: str, size: int):
"""Verify that an object exists
"""
Expand Down Expand Up @@ -85,3 +89,7 @@ def verify_object(self, prefix: str, oid: str, size: int) -> bool:
return self.get_size(prefix, oid) == size
except exc.ObjectNotFound:
return False


def guess_mime_type_from_filename(filename: str) -> Optional[str]:
return mimetypes.guess_type(filename)[0]
11 changes: 9 additions & 2 deletions giftless/storage/amazon_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,16 @@ def get_download_action(self, prefix: str, oid: str, size: int, expires_in: int,
'Bucket': self.bucket_name,
'Key': self._get_blob_path(prefix, oid)
}
if extra and 'filename' in extra:
filename = safe_filename(extra['filename'])

filename = extra.get('filename') if extra else None
disposition = extra.get('disposition', 'attachment') if extra else 'attachment'

if filename and disposition:
filename = safe_filename(filename)
params['ResponseContentDisposition'] = f'attachment; filename="{filename}"'
elif disposition:
params['ResponseContentDisposition'] = disposition

response = self.s3_client.generate_presigned_url('get_object',
Params=params,
ExpiresIn=expires_in
Expand Down
47 changes: 37 additions & 10 deletions giftless/storage/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from azure.core.exceptions import ResourceNotFoundError
from azure.storage.blob import BlobClient, BlobSasPermissions, BlobServiceClient, generate_blob_sas # type: ignore

from giftless.storage import ExternalStorage, MultipartStorage, StreamingStorage
from giftless.storage import ExternalStorage, MultipartStorage, StreamingStorage, guess_mime_type_from_filename

from .exc import ObjectNotFound

Expand Down Expand Up @@ -63,28 +63,50 @@ def get_size(self, prefix: str, oid: str) -> int:
except ResourceNotFoundError:
raise ObjectNotFound("Object does not exist")

def get_mime_type(self, prefix: str, oid: str) -> Optional[str]:
try:
blob_client = self.blob_svc_client.get_blob_client(container=self.container_name,
blob=self._get_blob_path(prefix, oid))
props = blob_client.get_blob_properties()
mime_type = props.content_settings.get(
"content_type", "application/octet-stream")
return mime_type # type: ignore
except ResourceNotFoundError:
raise ObjectNotFound("Object does not exist")

def get_upload_action(self, prefix: str, oid: str, size: int, expires_in: int,
extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
filename = extra.get('filename') if extra else None
return {
headers = {
"x-ms-blob-type": "BlockBlob",
}
reply = {
"actions": {
"upload": {
"href": self._get_signed_url(prefix, oid, expires_in, filename, create=True),
"header": {
"x-ms-blob-type": "BlockBlob",
},
"expires_in": expires_in
}
}
}

if filename:
mime_type = guess_mime_type_from_filename(filename)
if mime_type:
headers["x-ms-blob-content-type"] = mime_type

reply["actions"]["upload"]["header"] = headers

return reply

def get_download_action(self, prefix: str, oid: str, size: int, expires_in: int,
extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
filename = extra.get('filename') if extra else None
disposition = extra.get('disposition', 'attachment') if extra else 'attachment'

return {
"actions": {
"download": {
"href": self._get_signed_url(prefix, oid, expires_in, filename, read=True),
"href": self._get_signed_url(prefix, oid, expires_in, filename, disposition=disposition, read=True),
"header": {},
"expires_in": expires_in
}
Expand All @@ -104,7 +126,6 @@ def get_multipart_actions(self, prefix: str, oid: str, size: int, part_size: int
_log.info("There are %d uncommitted blocks pre-uploaded; %d parts still need to be uploaded",
len(uncommitted), len(parts))
commit_body = self._create_commit_body(blocks)

reply: Dict[str, Any] = {
"actions": {
"commit": {
Expand All @@ -123,6 +144,10 @@ def get_multipart_actions(self, prefix: str, oid: str, size: int, part_size: int
}
}
}
if filename:
mime_type = guess_mime_type_from_filename(filename)
if mime_type:
reply["actions"]["commit"]["header"]["x-ms-blob-content-type"] = mime_type

if parts:
reply['actions']['parts'] = parts
Expand All @@ -141,14 +166,16 @@ def _get_blob_path(self, prefix: str, oid: str) -> str:
return os.path.join(storage_prefix, prefix, oid)

def _get_signed_url(self, prefix: str, oid: str, expires_in: int, filename: Optional[str] = None,
**permissions: bool) -> str:
disposition: Optional[str] = None, **permissions: bool) -> str:
blob_name = self._get_blob_path(prefix, oid)
permissions = BlobSasPermissions(**permissions)
token_expires = (datetime.now(tz=timezone.utc) + timedelta(seconds=expires_in))

extra_args = {}
if filename:
extra_args['content_disposition'] = f'attachment; filename="{filename}"'
if filename and disposition:
extra_args['content_disposition'] = f'{disposition}; filename="{filename}"'
elif disposition:
extra_args['content_disposition'] = f'{disposition};"'

sas_token = generate_blob_sas(account_name=self.blob_svc_client.account_name,
account_key=self.blob_svc_client.credential.account_key,
Expand Down
10 changes: 8 additions & 2 deletions giftless/storage/google_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ def get_upload_action(self, prefix: str, oid: str, size: int, expires_in: int,
def get_download_action(self, prefix: str, oid: str, size: int, expires_in: int,
extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
filename = extra.get('filename') if extra else None
disposition = extra.get('disposition', 'attachment') if extra else 'attachment'

return {
"actions": {
"download": {
"href": self._get_signed_url(prefix, oid, expires_in=expires_in, filename=filename),
"href": self._get_signed_url(
prefix, oid, expires_in=expires_in, filename=filename, disposition=disposition),
"header": {},
"expires_in": expires_in
}
Expand All @@ -90,10 +93,13 @@ def _get_blob_path(self, prefix: str, oid: str) -> str:
return os.path.join(storage_prefix, prefix, oid)

def _get_signed_url(self, prefix: str, oid: str, expires_in: int, http_method: str = 'GET',
filename: Optional[str] = None) -> str:
filename: Optional[str] = None, disposition: Optional[str] = None) -> str:
bucket = self.storage_client.bucket(self.bucket_name)
blob = bucket.blob(self._get_blob_path(prefix, oid))
disposition = f'attachment; filename={filename}' if filename else None
if filename and disposition:
disposition = f'{disposition}; filename="{filename}"'

url: str = blob.generate_signed_url(expiration=timedelta(seconds=expires_in), method=http_method, version='v4',
response_disposition=disposition, credentials=self.credentials)
return url
Expand Down
5 changes: 5 additions & 0 deletions giftless/storage/local_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def get_size(self, prefix: str, oid: str) -> int:
return os.path.getsize(self._get_path(prefix, oid))
raise exc.ObjectNotFound("Object was not found")

def get_mime_type(self, prefix: str, oid: str) -> str:
if self.exists(prefix, oid):
return "application/octet-stream"
raise exc.ObjectNotFound("Object was not found")

def get_multipart_actions(self, prefix: str, oid: str, size: int, part_size: int, expires_in: int,
extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return super().get_multipart_actions(prefix, oid, size, part_size, expires_in, extra)
Expand Down
10 changes: 9 additions & 1 deletion giftless/transfer/basic_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,18 @@ def get(self, organization, repo, oid):

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

headers = {}
if filename and disposition:
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
elif disposition:
headers = {'Content-Disposition': disposition}

if self.storage.exists(path, oid):
file = self.storage.get(path, oid)
mime_type = self.storage.get_mime_type(path, oid)
headers['Content-Type'] = mime_type
return Response(file, direct_passthrough=True, status=200, headers=headers)
else:
raise NotFound("The object was not found")
Expand Down
6 changes: 5 additions & 1 deletion tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ def test_batch_request_default_transfer():


def test_object_schema_accepts_x_fields():
payload = {"oid": "123abc", "size": 1212, "x-filename": "foobarbaz", "x-mtime": 123123123123}
payload = {
"oid": "123abc", "size": 1212, "x-filename": "foobarbaz",
"x-mtime": 123123123123, "x-disposition": "inline"
}
parsed = schema.ObjectSchema().load(payload)
assert "foobarbaz" == parsed['extra']['filename']
assert 123123123123 == parsed['extra']['mtime']
assert "123abc" == parsed['oid']
assert "inline" == parsed['extra']['disposition']


def test_object_schema_rejects_unknown_fields():
Expand Down

0 comments on commit 06f0f2c

Please sign in to comment.