Skip to content

[WIKI-419] chore: new asset duplicate endpoint added #7172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: preview
Choose a base branch
from

Conversation

aaryan610
Copy link
Member

@aaryan610 aaryan610 commented Jun 5, 2025

Description

This PR introduces a new endpoint to duplicate assets and attach them to an entiyt without the need to re-upload them.

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)

Summary by CodeRabbit

  • New Features
    • Introduced the ability to duplicate multiple assets within a workspace through a new API endpoint and supporting functionality in the application. Users can now easily create copies of selected assets and associate them with different entities or projects.
    • Added rate limiting to enhance API request management for asset duplication.

Copy link
Contributor

coderabbitai bot commented Jun 5, 2025

"""

Walkthrough

A new API endpoint for duplicating assets within a workspace has been introduced. The backend adds the DuplicateAssetEndpoint class, exposes it via a new URL route, and updates imports. The frontend adds a corresponding duplicateAssets method to the file service for invoking this endpoint. Throttling settings were also added to support rate limiting for this endpoint.

Changes

File(s) Change Summary
apiserver/plane/app/urls/asset.py Added URL route for DuplicateAssetEndpoint to handle asset duplication requests.
apiserver/plane/app/views/init.py Imported DuplicateAssetEndpoint for broader view accessibility.
apiserver/plane/app/views/asset/v2.py Introduced DuplicateAssetEndpoint class with logic for duplicating assets and copying storage objects.
apiserver/plane/app/throttles/asset.py Added AssetRateThrottle class for scoped rate limiting based on asset ID.
apiserver/plane/settings/common.py Added REST framework throttling classes and rates for anonymous users and asset duplication scope.
web/core/services/file.service.ts Added duplicateAssets method to interact with the new backend duplication endpoint via POST request.

Sequence Diagram(s)

sequenceDiagram
    participant Frontend as FileService (frontend)
    participant Backend as DuplicateAssetEndpoint (backend)
    participant DB as Database
    participant S3 as S3Storage

    Frontend->>Backend: POST /workspaces/{slug}/duplicate-assets/ (asset_ids, entity_type, entity_id, ...)
    Backend->>DB: Validate workspace, project, and fetch original assets
    loop For each asset_id
        Backend->>S3: Copy storage object to new asset key
        Backend->>DB: Create new FileAsset record with copied attributes
    end
    Backend->>DB: Set is_uploaded=True for new assets
    Backend-->>Frontend: Return mapping {original_id: new_id}
Loading

Suggested labels

ready to merge

Suggested reviewers

  • pablohashescobar
  • NarayanBavisetti
  • Palanikannan1437

Poem

In the land of code, a hop and a leap,
Assets now duplicate, no need to peep!
With endpoints and routes, the journey is sweet,
Files multiply swiftly—oh, what a treat!
🐇✨

Let’s toast to new features, with carrots to eat!
"""

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

makeplane bot commented Jun 5, 2025

Pull Request Linked with Plane Work Items

Comment Automatically Generated by Plane

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🔭 Outside diff range comments (1)
web/core/services/file.service.ts (1)

36-45: 🛠️ Refactor suggestion

Remove duplicate enum definition.

The local TFileAssetType enum duplicates the imported EFileAssetType enum. This creates unnecessary duplication and potential maintenance issues.

Since EFileAssetType is now imported from @plane/types, remove the local enum definition and use the imported one consistently throughout the codebase.

- export enum TFileAssetType {
-   COMMENT_DESCRIPTION = "COMMENT_DESCRIPTION",
-   ISSUE_ATTACHMENT = "ISSUE_ATTACHMENT",
-   ISSUE_DESCRIPTION = "ISSUE_DESCRIPTION",
-   PAGE_DESCRIPTION = "PAGE_DESCRIPTION",
-   PROJECT_COVER = "PROJECT_COVER",
-   USER_AVATAR = "USER_AVATAR",
-   USER_COVER = "USER_COVER",
-   WORKSPACE_LOGO = "WORKSPACE_LOGO",
- }
🧹 Nitpick comments (1)
apiserver/plane/app/views/asset/v2.py (1)

783-783: Fix line length violation.

The line exceeds the maximum length limit (106 > 88 characters) as flagged by static analysis.

Break the long line for better readability:

- destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
+ destination_key = (
+     f"{workspace.id}/{uuid.uuid4().hex}-"
+     f"{original_asset.attributes.get('name')}"
+ )
🧰 Tools
🪛 Ruff (0.11.9)

783-783: Line too long (106 > 88)

(E501)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 986f29d and d52cce1.

📒 Files selected for processing (4)
  • apiserver/plane/app/urls/asset.py (2 hunks)
  • apiserver/plane/app/views/__init__.py (1 hunks)
  • apiserver/plane/app/views/asset/v2.py (1 hunks)
  • web/core/services/file.service.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apiserver/plane/app/urls/asset.py (1)
apiserver/plane/app/views/asset/v2.py (1)
  • DuplicateAssetEndpoint (723-810)
🪛 Ruff (0.11.9)
apiserver/plane/app/views/asset/v2.py

783-783: Line too long (106 > 88)

(E501)

🪛 Pylint (3.3.7)
apiserver/plane/app/views/asset/v2.py

[refactor] 724-724: Too many return statements (7/6)

(R0911)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (5)
apiserver/plane/app/views/__init__.py (1)

110-110: LGTM!

The import addition is correctly placed and follows the existing pattern for asset v2 endpoints.

apiserver/plane/app/urls/asset.py (1)

16-16: LGTM!

The import addition follows the existing pattern and is correctly placed.

web/core/services/file.service.ts (2)

4-4: Good import addition.

The import of EFileAssetType from the types package is the correct approach for type consistency.


262-276: LGTM!

The duplicateAssets method is well-implemented with:

  • Correct parameter types and return type
  • Proper endpoint URL construction
  • Consistent error handling pattern
  • Clear method signature that matches the backend API
apiserver/plane/app/views/asset/v2.py (1)

804-808: LGTM!

The bulk update to set is_uploaded=True for all duplicated assets is efficient and correct. The conditional check ensures the update only happens when there are successfully duplicated assets.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
apiserver/plane/app/views/asset/v2.py (4)

728-759: 🛠️ Refactor suggestion

Extract duplicated method to reduce code duplication.

This method is duplicated across multiple endpoint classes (WorkspaceFileAssetEndpoint, ProjectAssetEndpoint, and now DuplicateAssetEndpoint), violating the DRY principle.

Extract this method to the BaseAPIView class or create a mixin. Additionally, refactor to use a dictionary mapping to reduce the number of return statements:

def get_entity_id_field(self, entity_type, entity_id):
    entity_field_mapping = {
        FileAsset.EntityTypeContext.WORKSPACE_LOGO: {"workspace_id": entity_id},
        FileAsset.EntityTypeContext.PROJECT_COVER: {"project_id": entity_id},
        FileAsset.EntityTypeContext.USER_AVATAR: {"user_id": entity_id},
        FileAsset.EntityTypeContext.USER_COVER: {"user_id": entity_id},
        FileAsset.EntityTypeContext.ISSUE_ATTACHMENT: {"issue_id": entity_id},
        FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: {"issue_id": entity_id},
        FileAsset.EntityTypeContext.PAGE_DESCRIPTION: {"page_id": entity_id},
        FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: {"comment_id": entity_id},
    }
    return entity_field_mapping.get(entity_type, {})
🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 728-728: Too many return statements (7/6)

(R0911)


761-767: 🛠️ Refactor suggestion

Add validation for entity_type parameter.

The entity_type parameter is retrieved but not validated against allowed values, which could lead to invalid data being stored.

Add validation after line 766:

entity_type = request.data.get("entity_type", None)
+ 
+ if entity_type and entity_type not in FileAsset.EntityTypeContext.values:
+     return Response(
+         {"error": "Invalid entity type.", "status": False},
+         status=status.HTTP_400_BAD_REQUEST,
+     )

783-785: 🛠️ Refactor suggestion

Add validation for asset existence and upload status.

The code fetches assets but doesn't validate that all requested asset_ids exist or that the assets are uploaded, which could lead to silent failures.

Add validation after fetching the assets:

original_assets = FileAsset.objects.filter(
    workspace=workspace, id__in=asset_ids
)
+ 
+ # Validate all requested assets exist
+ found_asset_ids = set(str(asset.id) for asset in original_assets)
+ missing_asset_ids = set(asset_ids) - found_asset_ids
+ if missing_asset_ids:
+     return Response(
+         {"error": f"Assets not found: {', '.join(missing_asset_ids)}"},
+         status=status.HTTP_404_NOT_FOUND,
+     )
+ 
+ # Validate all assets are uploaded
+ unuploaded_assets = [str(asset.id) for asset in original_assets if not asset.is_uploaded]
+ if unuploaded_assets:
+     return Response(
+         {"error": f"Assets not uploaded: {', '.join(unuploaded_assets)}"},
+         status=status.HTTP_400_BAD_REQUEST,
+     )

805-806: ⚠️ Potential issue

Add error handling for storage copy operation.

The storage.copy_object() call lacks error handling, which could leave the database in an inconsistent state if the copy fails.

Wrap the storage operation in error handling:

- storage.copy_object(original_asset.asset, destination_key)
- duplicated_assets[str(original_asset.id)] = str(duplicated_asset.id)
+ try:
+     storage.copy_object(original_asset.asset, destination_key)
+     duplicated_assets[str(original_asset.id)] = str(duplicated_asset.id)
+ except Exception as e:
+     # Clean up the created asset record if storage copy fails
+     duplicated_asset.delete()
+     return Response(
+         {"error": f"Failed to copy storage object: {str(e)}"},
+         status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+     )
🧹 Nitpick comments (1)
apiserver/plane/app/views/asset/v2.py (1)

808-814: Consider transaction safety for bulk operations.

The bulk update operation and the preceding asset creation loop could benefit from database transaction wrapping to ensure atomicity.

Consider wrapping the duplication logic in a database transaction:

+ from django.db import transaction
+
+ @transaction.atomic
  def post(self, request, slug):

This ensures that if any part of the duplication process fails, all changes are rolled back consistently.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e369f8 and 9704f21.

📒 Files selected for processing (2)
  • apiserver/plane/app/views/asset/v2.py (2 hunks)
  • apiserver/plane/settings/common.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
apiserver/plane/app/views/asset/v2.py

787-787: Line too long (106 > 88)

(E501)

🪛 Pylint (3.3.7)
apiserver/plane/app/views/asset/v2.py

[refactor] 728-728: Too many return statements (7/6)

(R0911)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apiserver/plane/settings/common.py (1)

74-78: LGTM! Throttling configuration is appropriate.

The throttling settings are well-configured for the new duplicate asset endpoint:

  • AnonRateThrottle provides rate limiting for anonymous users
  • The image_duplicate scope with 10 requests/minute is reasonable for asset duplication operations
  • The anonymous rate of 30 requests/minute is balanced
apiserver/plane/app/views/asset/v2.py (2)

14-14: Import addition looks good.

The ScopedRateThrottle import is necessary for the new duplicate endpoint's throttling functionality.


723-727: Throttling configuration is properly implemented.

The class-level throttling configuration using ScopedRateThrottle with the "image_duplicate" scope addresses the rate limiting requirement mentioned in past reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
apiserver/plane/app/views/asset/v2.py (4)

727-758: 🛠️ Refactor suggestion

Extract duplicated method to base class.

The get_entity_id_field method is duplicated across multiple endpoint classes in this file (WorkspaceFileAssetEndpoint, ProjectAssetEndpoint, and now DuplicateAssetEndpoint). This violates the DRY principle and creates maintenance overhead.

Consider extracting this method to the BaseAPIView class or creating a mixin class that can be shared across these asset endpoints.

Additionally, the static analysis correctly identifies that this method has too many return statements (7/6), which affects readability. Consider using a dictionary lookup pattern:

def get_entity_id_field(self, entity_type, entity_id):
    entity_field_mapping = {
        FileAsset.EntityTypeContext.WORKSPACE_LOGO: {"workspace_id": entity_id},
        FileAsset.EntityTypeContext.PROJECT_COVER: {"project_id": entity_id},
        FileAsset.EntityTypeContext.USER_AVATAR: {"user_id": entity_id},
        FileAsset.EntityTypeContext.USER_COVER: {"user_id": entity_id},
        FileAsset.EntityTypeContext.ISSUE_ATTACHMENT: {"issue_id": entity_id},
        FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: {"issue_id": entity_id},
        FileAsset.EntityTypeContext.PAGE_DESCRIPTION: {"page_id": entity_id},
        FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: {"comment_id": entity_id},
    }
    return entity_field_mapping.get(entity_type, {})
🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 727-727: Too many return statements (7/6)

(R0911)


764-764: 🛠️ Refactor suggestion

Add validation for entity_type when provided.

The entity_type parameter is accepted but not validated against allowed values. This could lead to invalid entity types being stored in the database.

Add validation similar to other endpoints in this file:

+ if entity_type and entity_type not in FileAsset.EntityTypeContext.values:
+     return Response(
+         {"error": "Invalid entity type.", "status": False},
+         status=status.HTTP_400_BAD_REQUEST,
+     )

775-777: ⚠️ Potential issue

Add validation for asset existence and upload status.

The code fetches the original asset but doesn't validate that the requested asset_id exists or that the asset is uploaded. This could lead to silent failures or attempting to copy non-existent storage objects.

Add validation after fetching the asset:

original_asset = FileAsset.objects.filter(
    workspace=workspace, id=asset_id
).first()
+ 
+ # Validate asset exists
+ if not original_asset:
+     return Response(
+         {"error": "Asset not found"},
+         status=status.HTTP_404_NOT_FOUND,
+     )
+ 
+ # Validate asset is uploaded
+ if not original_asset.is_uploaded:
+     return Response(
+         {"error": "Asset not uploaded"},
+         status=status.HTTP_400_BAD_REQUEST,
+     )

795-795: ⚠️ Potential issue

Add error handling for storage copy operation.

The storage.copy_object() call can fail if the source object doesn't exist or due to storage service issues, but there's no error handling. This could leave the database in an inconsistent state with asset records that don't have corresponding storage objects.

Add error handling around the storage operation:

- storage.copy_object(original_asset.asset, destination_key)
+ try:
+     storage.copy_object(original_asset.asset, destination_key)
+ except Exception as e:
+     # Clean up the created asset record if storage copy fails
+     duplicated_asset.delete()
+     return Response(
+         {"error": f"Failed to copy storage object: {str(e)}"},
+         status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+     )
🧹 Nitpick comments (1)
apiserver/plane/app/views/asset/v2.py (1)

779-779: Fix line length violation.

The line exceeds the maximum length of 88 characters.

- destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
+ destination_key = (
+     f"{workspace.id}/{uuid.uuid4().hex}-"
+     f"{original_asset.attributes.get('name')}"
+ )
🧰 Tools
🪛 Ruff (0.11.9)

779-779: Line too long (102 > 88)

(E501)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9704f21 and cdd6a7b.

📒 Files selected for processing (4)
  • apiserver/plane/app/throttles/asset.py (1 hunks)
  • apiserver/plane/app/urls/asset.py (2 hunks)
  • apiserver/plane/app/views/asset/v2.py (2 hunks)
  • apiserver/plane/settings/common.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apiserver/plane/app/urls/asset.py
  • apiserver/plane/settings/common.py
🧰 Additional context used
🪛 Pylint (3.3.7)
apiserver/plane/app/throttles/asset.py

[refactor] 4-4: Too few public methods (1/2)

(R0903)

apiserver/plane/app/views/asset/v2.py

[refactor] 727-727: Too many return statements (7/6)

(R0911)

🪛 Ruff (0.11.9)
apiserver/plane/app/views/asset/v2.py

779-779: Line too long (102 > 88)

(E501)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apiserver/plane/app/throttles/asset.py (1)

1-11: LGTM! Clean throttling implementation.

The AssetRateThrottle class correctly implements rate limiting based on asset_id. The implementation follows Django REST framework patterns and properly handles cases where asset_id might not be present.

🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 4-4: Too few public methods (1/2)

(R0903)

apiserver/plane/app/views/asset/v2.py (2)

22-22: LGTM! Proper import of throttling class.

The import correctly adds the AssetRateThrottle class needed for the new endpoint.


723-726: LGTM! Throttling properly configured.

The endpoint correctly applies the AssetRateThrottle to implement rate limiting based on asset ID as requested in previous reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
apiserver/plane/app/views/asset/v2.py (2)

795-797: storage.copy_object still lacks error handling

A previous review pointed this out. Failure here leaves a DB row without a real object.
Wrap in try/except, roll back the new FileAsset, and return a 500.


727-758: 🛠️ Refactor suggestion

get_entity_id_field is still duplicated – extract or centralise to stay DRY

This helper is now present in three endpoint classes with identical logic. The earlier review already requested moving it into a shared location (e.g., a mix-in or BaseAPIView) and replacing the cascade of if statements with a lookup dict.
Keeping three copies will inevitably drift.

🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 727-727: Too many return statements (7/6)

(R0911)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cdd6a7b and 3cb427f.

📒 Files selected for processing (3)
  • apiserver/plane/app/urls/asset.py (2 hunks)
  • apiserver/plane/app/views/__init__.py (1 hunks)
  • apiserver/plane/app/views/asset/v2.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apiserver/plane/app/views/init.py
  • apiserver/plane/app/urls/asset.py
🧰 Additional context used
🪛 Ruff (0.11.9)
apiserver/plane/app/views/asset/v2.py

779-779: Line too long (102 > 88)

(E501)

🪛 Pylint (3.3.7)
apiserver/plane/app/views/asset/v2.py

[refactor] 727-727: Too many return statements (7/6)

(R0911)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Analyze (javascript)

# Update the is_uploaded field for all newly created assets
FileAsset.objects.filter(id=duplicated_asset.id).update(is_uploaded=True)

return Response(duplicated_asset.id, status=status.HTTP_200_OK)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

API should return JSON, not bare UUID

The rest of the API surface consistently wraps IDs in a JSON object. Returning a raw UUID string breaks client expectations and makes the response non-extensible.

-        return Response(duplicated_asset.id, status=status.HTTP_200_OK)
+        return Response(
+            {"asset_id": str(duplicated_asset.id)},
+            status=status.HTTP_201_CREATED,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return Response(duplicated_asset.id, status=status.HTTP_200_OK)
return Response(
{"asset_id": str(duplicated_asset.id)},
status=status.HTTP_201_CREATED,
)
🤖 Prompt for AI Agents
In apiserver/plane/app/views/asset/v2.py at line 799, the API currently returns
a bare UUID string which breaks client expectations and consistency. Modify the
return statement to wrap the duplicated_asset.id inside a JSON object, for
example returning a dictionary with a key like "id" mapped to the UUID, and then
pass that dictionary to the Response. This ensures the response is JSON
formatted and extensible.

Comment on lines +760 to +765
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, asset_id):
project_id = request.data.get("project_id", None)
entity_id = request.data.get("entity_id", None)
entity_type = request.data.get("entity_type", None)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate request payload before touching the DB

entity_type, entity_id and (optionally) project_id are accepted unchecked.
If entity_type is missing or invalid, FileAsset will be created with an invalid enum value and raise at save-time.
Add upfront validation consistent with other endpoints.

+        if entity_type not in FileAsset.EntityTypeContext.values:
+            return Response(
+                {"error": "Invalid entity type", "status": False},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+        if not entity_id:
+            return Response(
+                {"error": "Missing entity_id", "status": False},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, asset_id):
project_id = request.data.get("project_id", None)
entity_id = request.data.get("entity_id", None)
entity_type = request.data.get("entity_type", None)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug, asset_id):
project_id = request.data.get("project_id", None)
entity_id = request.data.get("entity_id", None)
entity_type = request.data.get("entity_type", None)
if entity_type not in FileAsset.EntityTypeContext.values:
return Response(
{"error": "Invalid entity type", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
if not entity_id:
return Response(
{"error": "Missing entity_id", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# …rest of the existing implementation…
🤖 Prompt for AI Agents
In apiserver/plane/app/views/asset/v2.py around lines 760 to 765, the request
payload fields entity_type, entity_id, and optionally project_id are used
without validation, which can cause errors when saving FileAsset with invalid
enum values. Add validation logic before any database operations to check that
entity_type is present and valid, entity_id is provided, and project_id if given
is valid. This validation should follow the pattern used in other endpoints to
ensure consistent and early error handling.

Comment on lines +766 to +779
workspace = Workspace.objects.get(slug=slug)
if project_id:
# check if project exists in the workspace
if not Project.objects.filter(id=project_id, workspace=workspace).exists():
return Response(
{"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND
)

storage = S3Storage()
original_asset = FileAsset.objects.filter(
workspace=workspace, id=asset_id
).first()
# for original_asset in original_assets:
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against missing or not-uploaded source asset

original_asset = … .first() may return None, or the asset may still be uploading.
Accessing .attributes on None (or copying an incomplete asset) will raise 500s.

-        original_asset = FileAsset.objects.filter(
-            workspace=workspace, id=asset_id
-        ).first()
+        original_asset = FileAsset.objects.filter(
+            workspace=workspace,
+            id=asset_id,
+            is_uploaded=True,
+        ).first()
+
+        if not original_asset:
+            return Response(
+                {"error": "Source asset not found or not uploaded"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
workspace = Workspace.objects.get(slug=slug)
if project_id:
# check if project exists in the workspace
if not Project.objects.filter(id=project_id, workspace=workspace).exists():
return Response(
{"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND
)
storage = S3Storage()
original_asset = FileAsset.objects.filter(
workspace=workspace, id=asset_id
).first()
# for original_asset in original_assets:
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
workspace = Workspace.objects.get(slug=slug)
if project_id:
# check if project exists in the workspace
if not Project.objects.filter(id=project_id, workspace=workspace).exists():
return Response(
{"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND
)
storage = S3Storage()
- original_asset = FileAsset.objects.filter(
- workspace=workspace, id=asset_id
- ).first()
+ original_asset = FileAsset.objects.filter(
+ workspace=workspace,
+ id=asset_id,
+ is_uploaded=True,
+ ).first()
+
+ if not original_asset:
+ return Response(
+ {"error": "Source asset not found or not uploaded"},
+ status=status.HTTP_404_NOT_FOUND,
+ )
# for original_asset in original_assets:
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
🧰 Tools
🪛 Ruff (0.11.9)

779-779: Line too long (102 > 88)

(E501)

🤖 Prompt for AI Agents
In apiserver/plane/app/views/asset/v2.py around lines 766 to 779, the code
assigns original_asset using .first() which can return None if no matching asset
is found. Accessing original_asset.attributes without checking for None can
cause a server error. Add a guard to check if original_asset is None or if it is
still uploading before proceeding. If either condition is true, return an
appropriate error response to prevent 500 errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants