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
+
+
+
+