diff --git a/CHANGELOG.md b/CHANGELOG.md index e04f4742..6ec12383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to ### Added -- ✨(back) create wopi applcation #2 +- ✨(backend) create wopi applcation #2 +- ✨(backend) expose url_preview on item object ### Changed diff --git a/docker/files/development/etc/nginx/conf.d/default.conf b/docker/files/development/etc/nginx/conf.d/default.conf index 86faa167..e0bc752f 100644 --- a/docker/files/development/etc/nginx/conf.d/default.conf +++ b/docker/files/development/etc/nginx/conf.d/default.conf @@ -17,6 +17,25 @@ server { proxy_set_header X-Amz-Date $authDate; proxy_set_header X-Amz-Content-SHA256 $authContentSha256; + # Get resource from Minio + proxy_pass http://minio:9000/drive-media-storage/; + proxy_set_header Host minio:9000; + add_header Content-Disposition "attachment"; + } + + # Proxy auth for media + location /media/preview/ { + # Auth request configuration + auth_request /media-auth; + auth_request_set $authHeader $upstream_http_authorization; + auth_request_set $authDate $upstream_http_x_amz_date; + auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256; + + # Pass specific headers from the auth response + proxy_set_header Authorization $authHeader; + proxy_set_header X-Amz-Date $authDate; + proxy_set_header X-Amz-Content-SHA256 $authContentSha256; + # Get resource from Minio proxy_pass http://minio:9000/drive-media-storage/; proxy_set_header Host minio:9000; diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 51077a10..bf0eb61c 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -132,6 +132,7 @@ class ListItemSerializer(serializers.ModelSerializer): nb_accesses = serializers.IntegerField(read_only=True) user_roles = serializers.SerializerMethodField() url = serializers.SerializerMethodField() + url_preview = serializers.SerializerMethodField() creator = UserLiteSerializer(read_only=True) hard_delete_at = serializers.SerializerMethodField(read_only=True) is_wopi_supported = serializers.SerializerMethodField() @@ -157,6 +158,7 @@ class ListItemSerializer(serializers.ModelSerializer): "type", "upload_state", "url", + "url_preview", "filename", "mimetype", "main_workspace", @@ -184,6 +186,7 @@ class ListItemSerializer(serializers.ModelSerializer): "type", "upload_state", "url", + "url_preview", "mimetype", "main_workspace", "size", @@ -228,6 +231,17 @@ class ListItemSerializer(serializers.ModelSerializer): return f"{settings.MEDIA_BASE_URL}{settings.MEDIA_URL}{quote(item.file_key)}" + def get_url_preview(self, item): + """Return the URL of the item.""" + if ( + item.type != models.ItemTypeChoices.FILE + or item.upload_state == models.ItemUploadStateChoices.PENDING + or item.filename is None + or not utils.is_previewable_item(item) + ): + return None + return f"{settings.MEDIA_BASE_URL}{settings.MEDIA_URL_PREVIEW}{quote(item.file_key)}" + def get_hard_delete_at(self, item): """Return the hard delete date of the item.""" if item.deleted_at is None: @@ -279,6 +293,7 @@ class ItemSerializer(ListItemSerializer): "type", "upload_state", "url", + "url_preview", "filename", "mimetype", "main_workspace", @@ -304,6 +319,7 @@ class ItemSerializer(ListItemSerializer): "type", "upload_state", "url", + "url_preview", "mimetype", "main_workspace", "size", diff --git a/src/backend/core/api/utils.py b/src/backend/core/api/utils.py index a0c6998b..242a6d55 100644 --- a/src/backend/core/api/utils.py +++ b/src/backend/core/api/utils.py @@ -133,6 +133,22 @@ def generate_upload_policy(item): return policy +def is_previewable_item(item): + """ + Check if a mime type is previewable. + """ + if item.mimetype is None: + return False + + for allowed in settings.ITEM_PREVIEWABLE_MIME_TYPES: + if allowed.endswith("/"): + if item.mimetype.startswith(allowed): + return True + elif item.mimetype == allowed: + return True + return False + + def get_item_file_head_object(item): """ Get the head object of an item file. diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 40b7898c..36311139 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -49,7 +49,7 @@ UUID_REGEX = ( ) FILE_EXT_REGEX = '[^.\\/:*?&"<>|\r\n]+' MEDIA_STORAGE_URL_PATTERN = re.compile( - f"{settings.MEDIA_URL:s}" + f"{settings.MEDIA_URL:s}(?Ppreview/)?" f"(?P{ITEM_FOLDER:s}/(?P{UUID_REGEX:s})/.*{FILE_EXT_REGEX:s})$" ) @@ -1183,6 +1183,10 @@ class ItemViewSet( logger.debug("Item '%s' is not ready", item.id) raise drf.exceptions.PermissionDenied() + if url_params.get("preview") and not utils.is_previewable_item(item): + logger.debug("Item '%s' is not previewable", item.id) + raise drf.exceptions.PermissionDenied() + # Generate S3 authorization headers using the extracted URL parameters request = utils.generate_s3_authorization_headers(f"{url_params.get('key'):s}") diff --git a/src/backend/core/tests/items/test_api_items_children_list.py b/src/backend/core/tests/items/test_api_items_children_list.py index 01213a00..e0229d4e 100644 --- a/src/backend/core/tests/items/test_api_items_children_list.py +++ b/src/backend/core/tests/items/test_api_items_children_list.py @@ -57,6 +57,7 @@ def test_api_items_children_list_anonymous_public_standalone(): if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -91,6 +92,7 @@ def test_api_items_children_list_anonymous_public_standalone(): if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, @@ -129,6 +131,7 @@ def test_api_items_children_list_anonymous_public_parent(): child2.upload_state = models.ItemUploadStateChoices.READY child2.filename = "logo.png" + child2.mimetype = "image/png" child2.save() response = APIClient().get(f"/api/v1.0/items/{item.id!s}/children/") @@ -162,6 +165,7 @@ def test_api_items_children_list_anonymous_public_parent(): "type": models.ItemTypeChoices.FILE, "upload_state": models.ItemUploadStateChoices.PENDING, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -194,7 +198,8 @@ def test_api_items_children_list_anonymous_public_parent(): "type": models.ItemTypeChoices.FILE, "upload_state": models.ItemUploadStateChoices.READY, "url": f"http://localhost:8083/media/item/{child2.id!s}/logo.png", - "mimetype": None, + "url_preview": f"http://localhost:8083/media/preview/item/{child2.id!s}/logo.png", + "mimetype": "image/png", "main_workspace": False, "filename": child2.filename, "size": None, @@ -280,6 +285,7 @@ def test_api_items_children_list_authenticated_unrelated_public_or_authenticated if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -314,6 +320,7 @@ def test_api_items_children_list_authenticated_unrelated_public_or_authenticated if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, @@ -385,6 +392,7 @@ def test_api_items_children_list_authenticated_public_or_authenticated_parent( if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -419,6 +427,7 @@ def test_api_items_children_list_authenticated_public_or_authenticated_parent( if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, @@ -515,6 +524,7 @@ def test_api_items_children_list_authenticated_related_direct(): if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -549,6 +559,7 @@ def test_api_items_children_list_authenticated_related_direct(): if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, @@ -621,6 +632,7 @@ def test_api_items_children_list_authenticated_related_parent(): if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -655,6 +667,7 @@ def test_api_items_children_list_authenticated_related_parent(): if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, @@ -790,6 +803,7 @@ def test_api_items_children_list_authenticated_related_team_members( if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -824,6 +838,7 @@ def test_api_items_children_list_authenticated_related_team_members( if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, @@ -892,6 +907,7 @@ def test_api_items_children_list_filter_type(): if child1.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child1.filename, @@ -939,6 +955,7 @@ def test_api_items_children_list_filter_type(): if child2.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": child2.filename, diff --git a/src/backend/core/tests/items/test_api_items_favorite_list.py b/src/backend/core/tests/items/test_api_items_favorite_list.py index 3a72544c..d0f83983 100644 --- a/src/backend/core/tests/items/test_api_items_favorite_list.py +++ b/src/backend/core/tests/items/test_api_items_favorite_list.py @@ -86,6 +86,7 @@ def test_api_item_favorite_list_authenticated_with_favorite(): "updated_at": item.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": item.upload_state, "url": None, + "url_preview": None, "mimetype": None, "user_roles": ["reader"], "main_workspace": False, diff --git a/src/backend/core/tests/items/test_api_items_list.py b/src/backend/core/tests/items/test_api_items_list.py index af3a6369..5fbf873c 100644 --- a/src/backend/core/tests/items/test_api_items_list.py +++ b/src/backend/core/tests/items/test_api_items_list.py @@ -67,6 +67,7 @@ def test_api_items_list_format(): item2.upload_state = models.ItemUploadStateChoices.READY item2.filename = "logo.png" + item2.mimetype = "image/png" item2.save() item3 = user.get_main_workspace() @@ -106,7 +107,8 @@ def test_api_items_list_format(): "type": models.ItemTypeChoices.FILE, "upload_state": models.ItemUploadStateChoices.READY, "url": f"http://localhost:8083/media/item/{item2.id!s}/logo.png", - "mimetype": None, + "url_preview": f"http://localhost:8083/media/preview/item/{item2.id!s}/logo.png", + "mimetype": "image/png", "main_workspace": False, "filename": item2.filename, "size": None, @@ -138,6 +140,7 @@ def test_api_items_list_format(): "type": models.ItemTypeChoices.FOLDER, "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -170,6 +173,7 @@ def test_api_items_list_format(): "type": models.ItemTypeChoices.FOLDER, "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": True, "filename": item3.filename, diff --git a/src/backend/core/tests/items/test_api_items_retrieve.py b/src/backend/core/tests/items/test_api_items_retrieve.py index efa92028..c76f0c05 100644 --- a/src/backend/core/tests/items/test_api_items_retrieve.py +++ b/src/backend/core/tests/items/test_api_items_retrieve.py @@ -51,6 +51,7 @@ def test_api_items_retrieve_anonymous_public_standalone(): if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -104,6 +105,7 @@ def test_api_items_retrieve_anonymous_public_parent(): if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -202,6 +204,7 @@ def test_api_items_retrieve_authenticated_unrelated_public_or_authenticated(reac if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -261,6 +264,7 @@ def test_api_items_retrieve_authenticated_public_or_authenticated_parent(reach): if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -398,6 +402,7 @@ def test_api_items_retrieve_authenticated_related_direct(): if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -459,6 +464,7 @@ def test_api_items_retrieve_authenticated_related_parent(): if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -638,6 +644,7 @@ def test_api_items_retrieve_authenticated_related_team_members( if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -711,6 +718,7 @@ def test_api_items_retrieve_authenticated_related_team_administrators( if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -784,6 +792,7 @@ def test_api_items_retrieve_authenticated_related_team_owners( if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, @@ -1149,6 +1158,7 @@ def test_api_items_retrieve_file_with_url_property(upload_state): "type": models.ItemTypeChoices.FILE, "upload_state": upload_state, "url": f"http://localhost:8083/media/item/{item.id!s}/logo.png", + "url_preview": f"http://localhost:8083/media/preview/item/{item.id!s}/logo.png", "mimetype": "image/png", "main_workspace": False, "filename": item.filename, @@ -1160,6 +1170,74 @@ def test_api_items_retrieve_file_with_url_property(upload_state): } +@pytest.mark.parametrize( + "upload_state", + [ + models.ItemUploadStateChoices.READY, + models.ItemUploadStateChoices.ANALYZING, + models.ItemUploadStateChoices.FILE_TOO_LARGE_TO_ANALYZE, + models.ItemUploadStateChoices.SUSPICIOUS, + ], +) +def test_api_items_retrieve_file_with_url_property_non_previewable(upload_state): + """ + The `url` property should not be none if the item is not pending but the + url preview should. + """ + + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + item = factories.ItemFactory( + creator=user, + type=models.ItemTypeChoices.FILE, + link_reach="public", + update_upload_state=upload_state, + filename="document.odt", + mimetype="application/vnd.oasis.opendocument.text", + size=8, + users=[(user, models.RoleChoices.OWNER)], + ) + + response = client.get(f"/api/v1.0/items/{item.id!s}/") + + assert response.status_code == 200 + assert response.json() == { + "id": str(item.id), + "abilities": item.get_abilities(user), + "created_at": item.created_at.isoformat().replace("+00:00", "Z"), + "creator": { + "id": str(item.creator.id), + "full_name": item.creator.full_name, + "short_name": item.creator.short_name, + }, + "depth": 1, + "is_favorite": False, + "link_reach": "public", + "link_role": item.link_role, + "nb_accesses": 1, + "numchild": 0, + "numchild_folder": 0, + "path": str(item.path), + "title": item.title, + "updated_at": item.updated_at.isoformat().replace("+00:00", "Z"), + "user_roles": [models.RoleChoices.OWNER], + "type": models.ItemTypeChoices.FILE, + "upload_state": upload_state, + "url": f"http://localhost:8083/media/item/{item.id!s}/document.odt", + "url_preview": None, + "mimetype": "application/vnd.oasis.opendocument.text", + "main_workspace": False, + "filename": item.filename, + "size": 8, + "description": None, + "deleted_at": None, + "hard_delete_at": None, + "is_wopi_supported": False, + } + + def test_api_items_retrieve_file_with_url_property_with_spaces(): """ The `url` property should have white spaces encoded. @@ -1206,6 +1284,10 @@ def test_api_items_retrieve_file_with_url_property_with_spaces(): "type": models.ItemTypeChoices.FILE, "upload_state": models.ItemUploadStateChoices.READY, "url": f"http://localhost:8083/media/item/{item.id!s}/logo%20with%20spaces.png", + "url_preview": ( + f"http://localhost:8083/media/preview/item/{item.id!s}/" + "logo%20with%20spaces.png" + ), "mimetype": "image/png", "main_workspace": False, "filename": item.filename, @@ -1344,6 +1426,7 @@ def test_api_items_retrieve_file_analysing_not_creator(): "type": models.ItemTypeChoices.FILE, "upload_state": models.ItemUploadStateChoices.ANALYZING, "url": f"http://localhost:8083/media/item/{item.id!s}/logo.png", + "url_preview": f"http://localhost:8083/media/preview/item/{item.id!s}/logo.png", "mimetype": "image/png", "main_workspace": False, "filename": item.filename, diff --git a/src/backend/core/tests/items/test_api_items_search.py b/src/backend/core/tests/items/test_api_items_search.py index 938c2f5b..e4b5cf24 100644 --- a/src/backend/core/tests/items/test_api_items_search.py +++ b/src/backend/core/tests/items/test_api_items_search.py @@ -82,6 +82,7 @@ def test_api_items_search_authenticated_without_filters(): .replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "user_roles": ["owner"], }, { @@ -115,6 +116,7 @@ def test_api_items_search_authenticated_without_filters(): "updated_at": top_parent.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "user_roles": [top_parent_access.role], }, { @@ -175,6 +177,7 @@ def test_api_items_search_authenticated_without_filters(): ), "upload_state": None, "url": None, + "url_preview": None, "user_roles": [top_parent_access.role], } ], @@ -185,6 +188,7 @@ def test_api_items_search_authenticated_without_filters(): "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "user_roles": [top_parent_access.role], }, { @@ -245,6 +249,7 @@ def test_api_items_search_authenticated_without_filters(): ), "upload_state": None, "url": None, + "url_preview": None, "user_roles": [top_parent_access.role], }, { @@ -277,6 +282,7 @@ def test_api_items_search_authenticated_without_filters(): "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "user_roles": [top_parent_access.role], }, ], @@ -287,6 +293,7 @@ def test_api_items_search_authenticated_without_filters(): "updated_at": children.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": "pending", "url": None, + "url_preview": None, "user_roles": [top_parent_access.role], }, ] diff --git a/src/backend/core/tests/items/test_api_items_trashbin.py b/src/backend/core/tests/items/test_api_items_trashbin.py index aec25dcc..3e2fcf77 100644 --- a/src/backend/core/tests/items/test_api_items_trashbin.py +++ b/src/backend/core/tests/items/test_api_items_trashbin.py @@ -86,6 +86,7 @@ def test_api_items_trashbin_format(settings): if item.type == models.ItemTypeChoices.FILE else None, "url": None, + "url_preview": None, "mimetype": None, "main_workspace": False, "filename": item.filename, diff --git a/src/backend/core/tests/items/test_api_items_tree.py b/src/backend/core/tests/items/test_api_items_tree.py index 3aa2980a..4e69d4e4 100644 --- a/src/backend/core/tests/items/test_api_items_tree.py +++ b/src/backend/core/tests/items/test_api_items_tree.py @@ -161,6 +161,7 @@ def test_items_api_anonymous_to_a_public_tree_structure(): "updated_at": level2_1.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": [], "main_workspace": False, @@ -194,6 +195,7 @@ def test_items_api_anonymous_to_a_public_tree_structure(): "updated_at": level2_2.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": [], "main_workspace": False, @@ -225,6 +227,7 @@ def test_items_api_anonymous_to_a_public_tree_structure(): "updated_at": level1_2.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": [], "main_workspace": False, @@ -374,6 +377,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): ), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(level2_1.item.get_roles(user)), "main_workspace": False, @@ -417,6 +421,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): ), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(level3_1.item.get_roles(user)), "main_workspace": False, @@ -452,6 +457,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): ), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(level2_2.item.get_roles(user)), "main_workspace": False, @@ -487,6 +493,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): ), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(level1_1.item.get_roles(user)), "main_workspace": False, @@ -524,6 +531,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): ), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(level1_2.item.get_roles(user)), "main_workspace": False, @@ -561,6 +569,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): ), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(level1_3.item.get_roles(user)), "main_workspace": False, @@ -592,6 +601,7 @@ def test_items_api_tree_authenticated_direct_access(django_assert_num_queries): "updated_at": root.item.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": list(root.item.get_roles(user)), "main_workspace": True, @@ -706,6 +716,7 @@ def test_api_items_tree_authenticated_with_access_authenticated(): "updated_at": level1_1.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": [], "main_workspace": False, @@ -740,6 +751,7 @@ def test_api_items_tree_authenticated_with_access_authenticated(): "updated_at": level2_1.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": [], "main_workspace": False, @@ -773,6 +785,7 @@ def test_api_items_tree_authenticated_with_access_authenticated(): "updated_at": level2_2.updated_at.isoformat().replace("+00:00", "Z"), "upload_state": None, "url": None, + "url_preview": None, "mimetype": None, "user_roles": [], "main_workspace": False, diff --git a/src/backend/core/tests/test_api_utils_is_previewable_item.py b/src/backend/core/tests/test_api_utils_is_previewable_item.py new file mode 100644 index 00000000..38664407 --- /dev/null +++ b/src/backend/core/tests/test_api_utils_is_previewable_item.py @@ -0,0 +1,63 @@ +"""Test utils.is_previewable_item""" + +import pytest + +from core import factories, models +from core.api import utils + +pytestmark = pytest.mark.django_db + + +def test_is_previewable_item_returns_false_when_mimetype_is_none(): + """Test is_previewable_item returns False when mimetype is None""" + item = factories.ItemFactory( + type=models.ItemTypeChoices.FILE, + filename="file.bin", + mimetype=None, + ) + + assert utils.is_previewable_item(item) is False + + +def test_is_previewable_item_returns_true_for_prefix_allowed_type_image_png(): + """Test is_previewable_item returns True for prefix allowed type image/png""" + item = factories.ItemFactory( + type=models.ItemTypeChoices.FILE, + filename="image.png", + mimetype="image/png", + ) + + assert utils.is_previewable_item(item) is True + + +def test_is_previewable_item_returns_true_for_exact_allowed_type_pdf(): + """Test is_previewable_item returns True for exact allowed type application/pdf""" + item = factories.ItemFactory( + type=models.ItemTypeChoices.FILE, + filename="document.pdf", + mimetype="application/pdf", + ) + + assert utils.is_previewable_item(item) is True + + +def test_is_previewable_item_returns_false_for_unallowed_type_json(): + """Test is_previewable_item returns False for unallowed type application/json""" + item = factories.ItemFactory( + type=models.ItemTypeChoices.FILE, + filename="data.json", + mimetype="application/json", + ) + + assert utils.is_previewable_item(item) is False + + +def test_is_previewable_item_returns_false_for_exact_mismatch_pdfx(): + """Test is_previewable_item returns False for exact mismatch application/pdfx""" + item = factories.ItemFactory( + type=models.ItemTypeChoices.FILE, + filename="document.pdfx", + mimetype="application/pdfx", + ) + + assert utils.is_previewable_item(item) is False diff --git a/src/backend/drive/settings.py b/src/backend/drive/settings.py index 49ed85e4..2a5f92b7 100755 --- a/src/backend/drive/settings.py +++ b/src/backend/drive/settings.py @@ -102,6 +102,7 @@ class Base(Configuration): STATIC_URL = "/static/" STATIC_ROOT = os.path.join(DATA_DIR, "static") MEDIA_URL = "/media/" + MEDIA_URL_PREVIEW = "/media/preview/" MEDIA_ROOT = os.path.join(DATA_DIR, "media") MEDIA_BASE_URL = values.Value( None, environ_name="MEDIA_BASE_URL", environ_prefix=None @@ -173,6 +174,17 @@ class Base(Configuration): environ_prefix=None, ) + ITEM_PREVIEWABLE_MIME_TYPES = values.ListValue( + [ + "image/", + "video/", + "audio/", + "application/pdf", + ], + environ_name="ITEM_PREVIEWABLE_MIME_TYPES", + environ_prefix=None, + ) + # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/