diff --git a/dandiapi/api/storage.py b/dandiapi/api/storage.py index e833e3538..1ce1899f9 100644 --- a/dandiapi/api/storage.py +++ b/dandiapi/api/storage.py @@ -171,6 +171,17 @@ def generate_presigned_download_url(self, key: str, path: str) -> str: }, ) + def generate_presigned_inline_url(self, key: str, path: str, content_type: str) -> str: + return self.connection.meta.client.generate_presigned_url( + 'get_object', + Params={ + 'Bucket': self.bucket_name, + 'Key': key, + 'ResponseContentDisposition': f'inline; filename="{path}"', + 'ResponseContentType': content_type, + }, + ) + def sha256_checksum(self, key: str) -> str: calculator = ChecksumCalculatorFile() obj = self.bucket.Object(key) @@ -213,6 +224,16 @@ def generate_presigned_download_url(self, key: str, path: str) -> str: response_headers={'response-content-disposition': f'attachment; filename="{path}"'}, ) + def generate_presigned_inline_url(self, key: str, path: str, content_type: str) -> str: + return self.base_url_client.presigned_get_object( + self.bucket_name, + key, + response_headers={ + 'response-content-disposition': f'inline; filename="{path}"', + 'response-content-type': content_type, + }, + ) + def sha256_checksum(self, key: str) -> str: calculator = ChecksumCalculatorFile() obj = self.client.get_object(self.bucket_name, key) diff --git a/dandiapi/api/views/asset.py b/dandiapi/api/views/asset.py index 14e81cf4c..91e3a71b4 100644 --- a/dandiapi/api/views/asset.py +++ b/dandiapi/api/views/asset.py @@ -27,7 +27,7 @@ from django.conf import settings from django.db import transaction from django.db.models import QuerySet -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.utils.decorators import method_decorator from django_filters import rest_framework as filters from drf_yasg.utils import swagger_auto_schema @@ -50,6 +50,7 @@ ) from dandiapi.api.views.serializers import ( AssetDetailSerializer, + AssetDownloadQueryParameterSerializer, AssetListSerializer, AssetPathsQueryParameterSerializer, AssetPathsSerializer, @@ -111,14 +112,14 @@ def retrieve(self, request, **kwargs): method='GET', operation_summary='Get the download link for an asset.', operation_description='', - manual_parameters=[ASSET_ID_PARAM], + query_serializer=AssetDownloadQueryParameterSerializer, responses={ 200: None, # This disables the auto-generated 200 response 301: 'Redirect to object store', }, ) @action(methods=['GET', 'HEAD'], detail=True) - def download(self, *args, **kwargs): + def download(self, request, *args, **kwargs): asset = self.get_object() # Raise error if zarr @@ -137,11 +138,35 @@ def download(self, *args, **kwargs): # Redirect to correct presigned URL storage = asset_blob.blob.storage - return HttpResponseRedirect( - storage.generate_presigned_download_url( - asset_blob.blob.name, os.path.basename(asset.path) + + serializer = AssetDownloadQueryParameterSerializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + content_disposition = serializer.validated_data['content_disposition'] + content_type = asset.metadata.get('encodingFormat', 'application/octet-stream') + asset_basename = os.path.basename(asset.path) + + if content_disposition == 'attachment': + return HttpResponseRedirect( + storage.generate_presigned_download_url(asset_blob.blob.name, asset_basename) ) - ) + elif content_disposition == 'inline': + url = storage.generate_presigned_inline_url( + asset_blob.blob.name, + asset_basename, + content_type, + ) + + if content_type == 'video/x-matroska': + return HttpResponse( + f""" + + """, + content_type='text/html', + ) + + return HttpResponseRedirect(url) @swagger_auto_schema( method='GET', diff --git a/dandiapi/api/views/serializers.py b/dandiapi/api/views/serializers.py index 831d2d743..d29ee37eb 100644 --- a/dandiapi/api/views/serializers.py +++ b/dandiapi/api/views/serializers.py @@ -199,6 +199,10 @@ class Meta: fields = ['status', 'validation_errors'] +class AssetDownloadQueryParameterSerializer(serializers.Serializer): + content_disposition = serializers.ChoiceField(['attachment', 'inline'], default='attachment') + + class EmbargoedSlugRelatedField(serializers.SlugRelatedField): """ A Field for cleanly serializing embargoed model fields. diff --git a/web/src/rest.ts b/web/src/rest.ts index bae605aa6..dd35c51af 100644 --- a/web/src/rest.ts +++ b/web/src/rest.ts @@ -243,6 +243,9 @@ const dandiRest = new Vue({ assetDownloadURI(identifier: string, version: string, uuid: string) { return `${dandiApiRoot}assets/${uuid}/download/`; }, + assetInlineURI(identifier: string, version: string, uuid: string) { + return `${dandiApiRoot}assets/${uuid}/download?content_disposition=inline`; + }, assetMetadataURI(identifier: string, version: string, uuid: string) { return `${dandiApiRoot}dandisets/${identifier}/versions/${version}/assets/${uuid}`; }, diff --git a/web/src/views/FileBrowserView/FileBrowser.vue b/web/src/views/FileBrowserView/FileBrowser.vue index 82d751f0f..2ba08d82b 100644 --- a/web/src/views/FileBrowserView/FileBrowser.vue +++ b/web/src/views/FileBrowserView/FileBrowser.vue @@ -144,6 +144,18 @@ + + + + mdi-eye + + + +