From 5363c185b84fedab4e4928409f2174bd2875bfae Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 12 Jun 2025 18:50:38 +0530 Subject: [PATCH 1/2] chore: new endpoints to download an asset --- apiserver/plane/app/urls/asset.py | 12 +++++++++ apiserver/plane/app/views/__init__.py | 2 ++ apiserver/plane/app/views/asset/v2.py | 35 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/apiserver/plane/app/urls/asset.py b/apiserver/plane/app/urls/asset.py index 77dd3d00efa..93356b04cb4 100644 --- a/apiserver/plane/app/urls/asset.py +++ b/apiserver/plane/app/urls/asset.py @@ -13,6 +13,8 @@ ProjectAssetEndpoint, ProjectBulkAssetEndpoint, AssetCheckEndpoint, + WorkspaceAssetDownloadEndpoint, + ProjectAssetDownloadEndpoint, ) @@ -89,4 +91,14 @@ AssetCheckEndpoint.as_view(), name="asset-check", ), + path( + "assets/v2/workspaces//download//", + WorkspaceAssetDownloadEndpoint.as_view(), + name="workspace-asset-download", + ), + path( + "assets/v2/workspaces//projects//download//", + ProjectAssetDownloadEndpoint.as_view(), + name="project-asset-download", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 55642a53358..6d56473e3f1 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -107,6 +107,8 @@ ProjectAssetEndpoint, ProjectBulkAssetEndpoint, AssetCheckEndpoint, + WorkspaceAssetDownloadEndpoint, + ProjectAssetDownloadEndpoint, ) from .issue.base import ( IssueListEndpoint, diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index aecba04b8c3..f0e583f91b7 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -718,3 +718,38 @@ def get(self, request, slug, asset_id): id=asset_id, workspace__slug=slug, deleted_at__isnull=True ).exists() return Response({"exists": asset}, status=status.HTTP_200_OK) + + +class WorkspaceAssetDownloadEndpoint(BaseAPIView): + """Endpoint to generate a download link for an asset with content-disposition=attachment.""" + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def get(self, request, slug, asset_id): + asset = FileAsset.all_objects.get( + id=asset_id, workspace__slug=slug, deleted_at__isnull=True + ) + storage = S3Storage(request=request) + signed_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition=f"attachment; filename={asset.asset.name}", + ) + return HttpResponseRedirect(signed_url) + + +class ProjectAssetDownloadEndpoint(BaseAPIView): + """Endpoint to generate a download link for an asset with content-disposition=attachment.""" + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT") + def get(self, request, slug, project_id, asset_id): + asset = FileAsset.all_objects.get( + id=asset_id, + workspace__slug=slug, + project_id=project_id, + deleted_at__isnull=True, + ) + storage = S3Storage(request=request) + signed_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition=f"attachment; filename={asset.asset.name}", + ) + return HttpResponseRedirect(signed_url) From 813f1fda3016f3be4d78766ddba5b80824a2ac43 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 12 Jun 2025 21:31:20 +0530 Subject: [PATCH 2/2] chore: add exception handling --- apiserver/plane/app/views/asset/v2.py | 36 ++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index f0e583f91b7..5994ffd8c16 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -725,14 +725,24 @@ class WorkspaceAssetDownloadEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def get(self, request, slug, asset_id): - asset = FileAsset.all_objects.get( - id=asset_id, workspace__slug=slug, deleted_at__isnull=True - ) + try: + asset = FileAsset.objects.get( + id=asset_id, + workspace__slug=slug, + is_uploaded=True, + ) + except FileAsset.DoesNotExist: + return Response( + {"error": "The requested asset could not be found."}, + status=status.HTTP_404_NOT_FOUND, + ) + storage = S3Storage(request=request) signed_url = storage.generate_presigned_url( object_name=asset.asset.name, disposition=f"attachment; filename={asset.asset.name}", ) + return HttpResponseRedirect(signed_url) @@ -741,15 +751,23 @@ class ProjectAssetDownloadEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT") def get(self, request, slug, project_id, asset_id): - asset = FileAsset.all_objects.get( - id=asset_id, - workspace__slug=slug, - project_id=project_id, - deleted_at__isnull=True, - ) + try: + asset = FileAsset.objects.get( + id=asset_id, + workspace__slug=slug, + project_id=project_id, + is_uploaded=True, + ) + except FileAsset.DoesNotExist: + return Response( + {"error": "The requested asset could not be found."}, + status=status.HTTP_404_NOT_FOUND, + ) + storage = S3Storage(request=request) signed_url = storage.generate_presigned_url( object_name=asset.asset.name, disposition=f"attachment; filename={asset.asset.name}", ) + return HttpResponseRedirect(signed_url)