From 42f307421ace7f962cb313f2e1756bd2ad45ea64 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 31 Jan 2024 18:43:02 +0530 Subject: [PATCH] dev: update the response for assets --- apiserver/plane/app/serializers/asset.py | 5 +- apiserver/plane/app/serializers/base.py | 51 ++++++++++++++++--- apiserver/plane/app/serializers/issue.py | 14 ++--- apiserver/plane/db/models/asset.py | 7 +-- apiserver/plane/db/models/issue.py | 2 +- apiserver/plane/settings/common.py | 4 +- apiserver/plane/settings/storage.py | 15 ++++++ .../plane/utils/presigned_url_generator.py | 24 +++++++++ 8 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 apiserver/plane/settings/storage.py create mode 100644 apiserver/plane/utils/presigned_url_generator.py diff --git a/apiserver/plane/app/serializers/asset.py b/apiserver/plane/app/serializers/asset.py index 136e2264b..96f687c61 100644 --- a/apiserver/plane/app/serializers/asset.py +++ b/apiserver/plane/app/serializers/asset.py @@ -1,8 +1,9 @@ -from .base import BaseSerializer +from .base import BaseFileSerializer from plane.db.models import FileAsset -class FileAssetSerializer(BaseSerializer): +class FileAssetSerializer(BaseFileSerializer): + class Meta: model = FileAsset fields = "__all__" diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index 446fdb6d5..20be382a8 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from plane.settings.storage import S3PrivateBucketStorage class BaseSerializer(serializers.ModelSerializer): @@ -60,7 +61,7 @@ class DynamicBaseSerializer(BaseSerializer): CycleIssueSerializer, IssueFlatSerializer, IssueRelationSerializer, - InboxIssueLiteSerializer + InboxIssueLiteSerializer, ) # Expansion mapper @@ -81,10 +82,22 @@ class DynamicBaseSerializer(BaseSerializer): "issue_cycle": CycleIssueSerializer, "parent": IssueSerializer, "issue_relation": IssueRelationSerializer, - "issue_inbox" : InboxIssueLiteSerializer, + "issue_inbox": InboxIssueLiteSerializer, } - - self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False) + + self.fields[field] = expansion[field]( + many=True + if field + in [ + "members", + "assignees", + "labels", + "issue_cycle", + "issue_relation", + "issue_inbox", + ] + else False + ) return self.fields @@ -105,7 +118,7 @@ class DynamicBaseSerializer(BaseSerializer): LabelSerializer, CycleIssueSerializer, IssueRelationSerializer, - InboxIssueLiteSerializer + InboxIssueLiteSerializer, ) # Expansion mapper @@ -126,7 +139,7 @@ class DynamicBaseSerializer(BaseSerializer): "issue_cycle": CycleIssueSerializer, "parent": IssueSerializer, "issue_relation": IssueRelationSerializer, - "issue_inbox" : InboxIssueLiteSerializer, + "issue_inbox": InboxIssueLiteSerializer, } # Check if field in expansion then expand the field if expand in expansion: @@ -146,3 +159,29 @@ class DynamicBaseSerializer(BaseSerializer): ) return response + + +class BaseFileSerializer(DynamicBaseSerializer): + download_url = serializers.SerializerMethodField() + + class Meta: + abstract = True # Make this serializer abstract + + def get_download_url(self, obj): + if hasattr(obj, "asset") and obj.asset: + storage = S3PrivateBucketStorage() + return storage.download_url(obj.asset.name) + return None + + def to_representation(self, instance): + """ + Object instance -> Dict of primitive datatypes. + """ + response = super().to_representation(instance) + response[ + "asset" + ] = ( + instance.asset.name + ) # Ensure 'asset' field is consistently serialized + # Apply custom method to get download URL + return response diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index be98bc312..e6425a6a3 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -5,7 +5,7 @@ from django.utils import timezone from rest_framework import serializers # Module imports -from .base import BaseSerializer, DynamicBaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer, BaseFileSerializer from .user import UserLiteSerializer from .state import StateSerializer, StateLiteSerializer from .project import ProjectLiteSerializer @@ -444,7 +444,8 @@ class IssueLinkSerializer(BaseSerializer): return IssueLink.objects.create(**validated_data) -class IssueAttachmentSerializer(BaseSerializer): +class IssueAttachmentSerializer(BaseFileSerializer): + class Meta: model = IssueAttachment fields = "__all__" @@ -503,9 +504,7 @@ class IssueCommentSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer( read_only=True, source="workspace" ) - comment_reactions = CommentReactionSerializer( - read_only=True, many=True - ) + comment_reactions = CommentReactionSerializer(read_only=True, many=True) is_member = serializers.BooleanField(read_only=True) class Meta: @@ -615,7 +614,10 @@ class IssueSerializer(DynamicBaseSerializer): def get_module_ids(self, obj): # Access the prefetched modules and extract module IDs - return [module for module in obj.issue_module.values_list("module_id", flat=True)] + return [ + module + for module in obj.issue_module.values_list("module_id", flat=True) + ] class IssueLiteSerializer(DynamicBaseSerializer): diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index 713508613..b327d83bb 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -8,12 +8,12 @@ from django.conf import settings # Module import from . import BaseModel - +from plane.settings.storage import S3PrivateBucketStorage def get_upload_path(instance, filename): if instance.workspace_id is not None: - return f"{instance.workspace.id}/{uuid4().hex}-{filename}" - return f"user-{uuid4().hex}-{filename}" + return f"{instance.workspace.id}/{uuid4().hex}" + return f"user-{uuid4().hex}" def file_size(value): @@ -32,6 +32,7 @@ class FileAsset(BaseModel): validators=[ file_size, ], + storage=S3PrivateBucketStorage(), ) workspace = models.ForeignKey( "db.Workspace", diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index d5ed4247a..05cb8d482 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -337,7 +337,7 @@ class IssueLink(ProjectBaseModel): def get_upload_path(instance, filename): - return f"{instance.workspace.id}/{uuid4().hex}-{filename}" + return f"{instance.workspace.id}/{uuid4().hex}" def file_size(value): diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 444248382..569469426 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -226,14 +226,14 @@ STORAGES = { }, } STORAGES["default"] = { - "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", + "BACKEND": "plane.settings.storage.S3PrivateBucketStorage", } AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") AWS_REGION = os.environ.get("AWS_REGION", "") AWS_DEFAULT_ACL = "public-read" -AWS_QUERYSTRING_AUTH = False +AWS_QUERYSTRING_AUTH = True AWS_S3_FILE_OVERWRITE = False AWS_S3_ENDPOINT_URL = os.environ.get( "AWS_S3_ENDPOINT_URL", None diff --git a/apiserver/plane/settings/storage.py b/apiserver/plane/settings/storage.py new file mode 100644 index 000000000..414495259 --- /dev/null +++ b/apiserver/plane/settings/storage.py @@ -0,0 +1,15 @@ +# Third party imports +from storages.backends.s3boto3 import S3Boto3Storage + +# Module imports +from plane.utils.presigned_url_generator import generate_download_presigned_url + + +class S3PrivateBucketStorage(S3Boto3Storage): + + def url(self, name): + # Return an empty string or None, or implement custom logic here + return name + + def download_url(self, name): + return generate_download_presigned_url(name) diff --git a/apiserver/plane/utils/presigned_url_generator.py b/apiserver/plane/utils/presigned_url_generator.py new file mode 100644 index 000000000..a891cf310 --- /dev/null +++ b/apiserver/plane/utils/presigned_url_generator.py @@ -0,0 +1,24 @@ +import boto3 +from django.conf import settings + +def generate_download_presigned_url(object_name, expiration=3600): + """ + Generate a presigned URL to download an object from S3. + :param object_name: The key name of the object in the S3 bucket. + :param expiration: Time in seconds for the presigned URL to remain valid (default is 1 hour). + :return: Presigned URL as a string. If error, returns None. + """ + s3_client = boto3.client('s3', + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + region_name=settings.AWS_REGION, + endpoint_url=settings.AWS_S3_ENDPOINT_URL) + try: + response = s3_client.generate_presigned_url('get_object', + Params={'Bucket': settings.AWS_STORAGE_BUCKET_NAME, + 'Key': object_name}, + ExpiresIn=expiration) + return response + except Exception as e: + print(f"Error generating presigned download URL: {e}") + return None