This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new 9d7a3e1fe feat(portal): de-Thrift the file upload/write path to gRPC 
(Track D, D4.2) (#187)
9d7a3e1fe is described below

commit 9d7a3e1fe7a6db13930afa116fc1ae72e7e68565
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 01:34:48 2026 -0400

    feat(portal): de-Thrift the file upload/write path to gRPC (Track D, D4.2) 
(#187)
    
    Repoint the upload/write data-product views from the legacy
    airavata_django_portal_sdk user_storage helpers to the gRPC storage + 
research
    facades:
    
    - upload_input_file / tus_upload_finish: new _storage_upload_and_register 
helper
      uploads bytes via storage.upload_file (full ~/-prefixed path under the 
input
      staging dir 'tmp') then registers the data product via
      research.register_data_product, returning the registered product adapted 
to
      the DataProductSerializer shape. Replaces user_storage.save_input_file.
    - DataProductView.put (fileContentText): overwrite the file in place via
      storage.upload_file at the replica's path. Replaces
      user_storage.update_data_product_content.
    - grpc_requests.data_product_for_upload builds the proto DataProductModel
      (FILE type, single GATEWAY_DATA_STORE/TRANSIENT replica, mime-type 
metadata,
      product size) the way the legacy SDK's _create_data_product did.
    
    The REST/JSON contract to the Vue frontend is unchanged (same
    DataProductSerializer output). Listing/delete in UserStoragePathView /
    ExperimentStoragePathView and the create_user_storage_dir signal remain on
    user_storage and migrate in D4.3.
    
    Depends on apache/airavata#651 (data-product register/read round-trip).
    
    Validation: manage.py check clean; live end-to-end against the tilt backend 
—
    upload via the view helper registers a product with its replica, the
    DataProductSerializer renders productUri/downloadURL/isInputFileUpload=True/
    filesize/userHasWriteAccess, and DataProductView.put overwrites the bytes
    (download returns the new content).
---
 .../django_airavata/apps/api/grpc_requests.py      | 30 +++++++++++
 .../django_airavata/apps/api/views.py              | 62 +++++++++++++++++++---
 2 files changed, 84 insertions(+), 8 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/api/grpc_requests.py 
b/airavata-django-portal/django_airavata/apps/api/grpc_requests.py
index 3bc156d4c..3a5704d0c 100644
--- a/airavata-django-portal/django_airavata/apps/api/grpc_requests.py
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_requests.py
@@ -555,3 +555,33 @@ def group(t):
         members=list(t.members or []),
         admins=list(t.admins or []),
     )
+
+
+def data_product_for_upload(*, gateway_id, owner_name, product_name, file_path,
+                            storage_resource_id, content_type=None, 
product_size=0):
+    """Build a proto ``DataProductModel`` to register for a freshly uploaded 
file.
+
+    The gRPC ``storage.upload_file`` only transfers the bytes and returns a
+    minimal ``DataProductModel``; the portal registers the full data product 
via
+    ``research.register_data_product`` so the file gets a canonical product 
URI.
+    Mirrors the legacy ``user_storage._create_data_product`` shape: a single
+    GATEWAY_DATA_STORE / TRANSIENT replica pointing at ``file_path``, with the
+    content type recorded under ``mime-type`` metadata.
+    """
+    rc = _pb2("data.replica.replica_catalog_pb2")
+    product_metadata = {"mime-type": content_type} if content_type else {}
+    return rc.DataProductModel(
+        gateway_id=gateway_id,
+        owner_name=owner_name,
+        product_name=product_name,
+        data_product_type=rc.DataProductType.FILE,
+        product_size=product_size or 0,
+        product_metadata=product_metadata,
+        replica_locations=[rc.DataReplicaLocationModel(
+            replica_name="{} gateway data store copy".format(product_name),
+            
replica_location_category=rc.ReplicaLocationCategory.GATEWAY_DATA_STORE,
+            replica_persistent_type=rc.ReplicaPersistentType.TRANSIENT,
+            storage_resource_id=storage_resource_id or '',
+            file_path=file_path,
+        )],
+    )
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py 
b/airavata-django-portal/django_airavata/apps/api/views.py
index c08c15beb..fd49b2a54 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -80,9 +80,49 @@ from . import (
 
 READ_PERMISSION_TYPE = '{}:READ'
 
+# Input files uploaded for an experiment are staged under this directory in the
+# user's storage (mirrors the legacy SDK's TMP_INPUT_FILE_UPLOAD_DIR).
+TMP_INPUT_FILE_UPLOAD_DIR = "tmp"
+
 log = logging.getLogger(__name__)
 
 
+def _storage_upload_and_register(request, dir_path, uploaded_file, name=None,
+                                 content_type=None):
+    """Upload a file to user storage and register a data product for it (gRPC).
+
+    Writes the bytes via the ``storage`` facade (the path is the full file 
path,
+    ``~/``-prefixed so the backend resolves it against the storage root), then
+    registers a data product via the ``research`` facade so the file has a
+    canonical product URI. Returns the registered data product adapted to the
+    ``DataProductSerializer`` shape. Replaces the legacy
+    ``user_storage.save``/``save_input_file`` (which transferred bytes and
+    registered the data product in one call).
+    """
+    storage = request.airavata.storage
+    name = name or os.path.basename(getattr(uploaded_file, 'name', '') or '')
+    # Full file path, ~/-prefixed so resolvePath expands it to the storage 
root.
+    upload_path = "~/" + os.path.join(dir_path, name).lstrip("/")
+    content = uploaded_file.read()
+    storage.upload_file(
+        path=upload_path, content=content, name=name,
+        content_type=content_type or '')
+    # The upload response is minimal; resolve the absolute path the backend 
wrote
+    # to and register the full data product.
+    metadata = storage.get_file_metadata(upload_path)
+    product_uri = request.airavata.research.register_data_product(
+        grpc_requests.data_product_for_upload(
+            gateway_id=settings.GATEWAY_ID,
+            owner_name=request.user.username,
+            product_name=name,
+            file_path=metadata.path,
+            storage_resource_id=storage.get_default_storage_resource_id(),
+            content_type=content_type,
+            product_size=metadata.size))
+    return grpc_adapters.data_product(
+        request.airavata.research.get_data_product(product_uri))
+
+
 class GroupViewSet(APIBackedViewSet):
     serializer_class = serializers.GroupSerializer
     lookup_field = 'group_id'
@@ -875,10 +915,14 @@ class DataProductView(APIView):
         data_product = grpc_adapters.data_product(
             request.airavata.research.get_data_product(data_product_uri))
         if request.data and "fileContentText" in request.data:
-            user_storage.update_data_product_content(
-                request=request,
-                data_product=data_product,
-                fileContentText=request.data["fileContentText"])
+            file_path = grpc_adapters.data_product_file_path(data_product)
+            if file_path is None:
+                return Response(status=status.HTTP_400_BAD_REQUEST)
+            # Overwrite the file content in place at the replica's path.
+            request.airavata.storage.upload_file(
+                path=file_path,
+                content=request.data["fileContentText"].encode("utf-8"),
+                name=data_product.productName or os.path.basename(file_path))
             return self.get(request=request, format=format)
         else:
             return Response(status=status.HTTP_400_BAD_REQUEST)
@@ -888,8 +932,9 @@ class DataProductView(APIView):
 def upload_input_file(request):
     try:
         input_file = request.FILES['file']
-        data_product = user_storage.save_input_file(
-            request, input_file, content_type=input_file.content_type)
+        data_product = _storage_upload_and_register(
+            request, TMP_INPUT_FILE_UPLOAD_DIR, input_file,
+            content_type=input_file.content_type)
         serializer = serializers.DataProductSerializer(
             data_product, context={'request': request})
         return JsonResponse({'uploaded': True,
@@ -907,8 +952,9 @@ def tus_upload_finish(request):
 
     def save_upload(file_path, file_name, file_type):
         with open(file_path, 'rb') as uploaded_file:
-            return user_storage.save_input_file(request, uploaded_file,
-                                                name=file_name, 
content_type=file_type)
+            return _storage_upload_and_register(
+                request, TMP_INPUT_FILE_UPLOAD_DIR, uploaded_file,
+                name=file_name, content_type=file_type)
     try:
         data_product = tus.save_tus_upload(uploadURL, save_upload)
         serializer = serializers.DataProductSerializer(

Reply via email to