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

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


The following commit(s) were added to refs/heads/master by this push:
     new e23618c9fc feat(storage-service): surface per-file data-product URI in 
listings (#653)
e23618c9fc is described below

commit e23618c9fc67bc18b9e18d5b65601ec08187dded
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 02:12:19 2026 -0400

    feat(storage-service): surface per-file data-product URI in listings (#653)
    
    File listings (list_dir / get_file_metadata / list_experiment_dir) now 
carry a
    stable data product URI per file, so clients can reference each stored file 
by
    data product (downloads, deletes, selection) without a separate registry 
table.
    
    - DataProductInterface/DataProductRepository: 
getDataProductByReplicaFilePath
      (JPQL join on the replica file path + gateway) finds the data product 
whose
      gateway-data-store replica is a given path.
    - StorageProvider/StorageProviderImpl: getOrCreateDataProductByPath returns 
that
      product's URI, registering a new FILE data product (single 
GATEWAY_DATA_STORE
      replica) if none exists. This replaces the legacy 
file-path-to-data-product
      mapping that the portal kept in its own ORM table.
    - UserStorageGrpcService populates FileMetadataResponse.data_product_uri 
for each
      file (best-effort: directories and lookup failures yield an empty URI, 
never a
      listing failure).
    
    Validated live: an uploaded file's get_file_metadata/list_dir return a
    data_product_uri that is stable across calls (found, not re-registered) and
    resolves via the data-product service to the file's replica. 
storage-service +
    research-service tests pass.
---
 .../research/repository/DataProductRepository.java | 11 +++++
 .../java/org/apache/airavata/db/DBConstants.java   |  1 +
 .../org/apache/airavata/db/QueryConstants.java     |  4 ++
 .../airavata/interfaces/DataProductInterface.java  |  7 +++
 .../airavata/interfaces/StorageProvider.java       | 18 ++++++++
 .../airavata/storage/StorageProviderImpl.java      | 28 ++++++++++++
 .../storage/grpc/UserStorageGrpcService.java       | 50 +++++++++++++++++++---
 7 files changed, 114 insertions(+), 5 deletions(-)

diff --git 
a/airavata-api/research-service/src/main/java/org/apache/airavata/research/repository/DataProductRepository.java
 
b/airavata-api/research-service/src/main/java/org/apache/airavata/research/repository/DataProductRepository.java
index 514fb47b41..4663c7b4ec 100644
--- 
a/airavata-api/research-service/src/main/java/org/apache/airavata/research/repository/DataProductRepository.java
+++ 
b/airavata-api/research-service/src/main/java/org/apache/airavata/research/repository/DataProductRepository.java
@@ -195,6 +195,17 @@ public class DataProductRepository extends 
AbstractRepository<DataProductModel,
         return dataProductModelList;
     }
 
+    @Override
+    public DataProductModel getDataProductByReplicaFilePath(String gatewayId, 
String filePath)
+            throws ReplicaCatalogException {
+        Map<String, Object> queryParameters = new HashMap<>();
+        queryParameters.put(DBConstants.DataProduct.GATEWAY_ID, gatewayId);
+        queryParameters.put(DBConstants.DataProduct.FILE_PATH, filePath);
+        List<DataProductModel> matches =
+                select(QueryConstants.FIND_DATA_PRODUCT_BY_REPLICA_FILE_PATH, 
1, 0, queryParameters);
+        return matches.isEmpty() ? null : matches.get(0);
+    }
+
     @Override
     public boolean isDataProductExists(String productUri) throws 
ReplicaCatalogException {
         return isExists(productUri);
diff --git a/airavata-api/src/main/java/org/apache/airavata/db/DBConstants.java 
b/airavata-api/src/main/java/org/apache/airavata/db/DBConstants.java
index d6cccb4b60..606c76cd82 100644
--- a/airavata-api/src/main/java/org/apache/airavata/db/DBConstants.java
+++ b/airavata-api/src/main/java/org/apache/airavata/db/DBConstants.java
@@ -169,6 +169,7 @@ public class DBConstants {
         public static final String OWNER_NAME = "ownerName";
         public static final String PRODUCT_NAME = "productName";
         public static final String PARENT_PRODUCT_URI = "parentProductUri";
+        public static final String FILE_PATH = "filePath";
     }
 
     public static class Workflow {
diff --git 
a/airavata-api/src/main/java/org/apache/airavata/db/QueryConstants.java 
b/airavata-api/src/main/java/org/apache/airavata/db/QueryConstants.java
index 52d1d8e405..8feff209d7 100644
--- a/airavata-api/src/main/java/org/apache/airavata/db/QueryConstants.java
+++ b/airavata-api/src/main/java/org/apache/airavata/db/QueryConstants.java
@@ -208,6 +208,10 @@ public interface QueryConstants {
             + DBConstants.DataProduct.GATEWAY_ID + " AND DP.ownerName LIKE :" 
+ DBConstants.DataProduct.OWNER_NAME
             + " AND DP.productName LIKE :" + 
DBConstants.DataProduct.PRODUCT_NAME;
 
+    String FIND_DATA_PRODUCT_BY_REPLICA_FILE_PATH = "SELECT DP FROM " + 
"DataProductEntity" + " DP "
+            + "JOIN DP.replicaLocations R WHERE DP.gatewayId = :"
+            + DBConstants.DataProduct.GATEWAY_ID + " AND R.filePath = :" + 
DBConstants.DataProduct.FILE_PATH;
+
     String GET_WORKFLOW_FOR_EXPERIMENT_ID = "SELECT W FROM " + 
AIRAVATA_WORKFLOW_ENTITY + " W "
             + "WHERE W.experimentId LIKE :" + 
DBConstants.Workflow.EXPERIMENT_ID;
 
diff --git 
a/airavata-api/src/main/java/org/apache/airavata/interfaces/DataProductInterface.java
 
b/airavata-api/src/main/java/org/apache/airavata/interfaces/DataProductInterface.java
index 8c0b62d61c..ef63de1f9a 100644
--- 
a/airavata-api/src/main/java/org/apache/airavata/interfaces/DataProductInterface.java
+++ 
b/airavata-api/src/main/java/org/apache/airavata/interfaces/DataProductInterface.java
@@ -39,6 +39,13 @@ public interface DataProductInterface {
     List<DataProductModel> searchDataProductsByName(
             String gatewayId, String userId, String productName, int limit, 
int offset) throws ReplicaCatalogException;
 
+    /**
+     * Return the data product in the gateway that has a replica at the given 
file path, or
+     * {@code null} if none exists.
+     */
+    DataProductModel getDataProductByReplicaFilePath(String gatewayId, String 
filePath)
+            throws ReplicaCatalogException;
+
     boolean isDataProductExists(String productUri) throws 
ReplicaCatalogException;
 
     boolean removeDataProduct(String productUri) throws 
ReplicaCatalogException;
diff --git 
a/airavata-api/src/main/java/org/apache/airavata/interfaces/StorageProvider.java
 
b/airavata-api/src/main/java/org/apache/airavata/interfaces/StorageProvider.java
index 51b43fa9f7..9484aede7f 100644
--- 
a/airavata-api/src/main/java/org/apache/airavata/interfaces/StorageProvider.java
+++ 
b/airavata-api/src/main/java/org/apache/airavata/interfaces/StorageProvider.java
@@ -203,4 +203,22 @@ public interface StorageProvider {
      * @throws Exception if a data access error occurs
      */
     boolean removeReplicaLocation(String replicaId) throws Exception;
+
+    /**
+     * Return the product URI of the data product whose gateway-data-store 
replica is the given
+     * file path, registering a new {@code FILE} data product for it if none 
exists. This is the
+     * server-side equivalent of the legacy per-file file-path-to-data-product 
mapping, so callers
+     * (e.g. file listings) can surface a stable data product URI for each 
stored file.
+     *
+     * @param gatewayId the gateway id
+     * @param ownerName the file owner's user id
+     * @param fileName the file name (used as the product name when creating)
+     * @param filePath the absolute file path on the storage resource
+     * @param storageResourceId the storage resource id holding the file
+     * @return the existing or newly registered data product URI
+     * @throws Exception if a data access error occurs
+     */
+    String getOrCreateDataProductByPath(
+            String gatewayId, String ownerName, String fileName, String 
filePath, String storageResourceId)
+            throws Exception;
 }
diff --git 
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/StorageProviderImpl.java
 
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/StorageProviderImpl.java
index ac9b14d13d..e133574079 100644
--- 
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/StorageProviderImpl.java
+++ 
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/StorageProviderImpl.java
@@ -27,7 +27,10 @@ import org.apache.airavata.interfaces.StorageProvider;
 import 
org.apache.airavata.model.appcatalog.storageresource.proto.StorageResourceDescription;
 import org.apache.airavata.model.data.movement.proto.DataMovementInterface;
 import org.apache.airavata.model.data.replica.proto.DataProductModel;
+import org.apache.airavata.model.data.replica.proto.DataProductType;
 import org.apache.airavata.model.data.replica.proto.DataReplicaLocationModel;
+import org.apache.airavata.model.data.replica.proto.ReplicaLocationCategory;
+import org.apache.airavata.model.data.replica.proto.ReplicaPersistentType;
 import org.apache.airavata.storage.repository.StorageResourceRepository;
 import org.springframework.stereotype.Service;
 
@@ -135,4 +138,29 @@ public class StorageProviderImpl implements 
StorageProvider {
     public boolean removeReplicaLocation(String replicaId) throws Exception {
         return dataReplicaLocationRepository.removeReplicaLocation(replicaId);
     }
+
+    @Override
+    public String getOrCreateDataProductByPath(
+            String gatewayId, String ownerName, String fileName, String 
filePath, String storageResourceId)
+            throws Exception {
+        DataProductModel existing = 
dataProductRepository.getDataProductByReplicaFilePath(gatewayId, filePath);
+        if (existing != null) {
+            return existing.getProductUri();
+        }
+        DataReplicaLocationModel replica = 
DataReplicaLocationModel.newBuilder()
+                .setReplicaName(fileName + " gateway data store copy")
+                
.setReplicaLocationCategory(ReplicaLocationCategory.GATEWAY_DATA_STORE)
+                .setReplicaPersistentType(ReplicaPersistentType.TRANSIENT)
+                .setStorageResourceId(storageResourceId != null ? 
storageResourceId : "")
+                .setFilePath(filePath)
+                .build();
+        DataProductModel product = DataProductModel.newBuilder()
+                .setGatewayId(gatewayId)
+                .setOwnerName(ownerName != null ? ownerName : "")
+                .setProductName(fileName)
+                .setDataProductType(DataProductType.FILE)
+                .addReplicaLocations(replica)
+                .build();
+        return dataProductRepository.registerDataProduct(product);
+    }
 }
diff --git 
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
 
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
index 79d121e9c0..d2d06486c5 100644
--- 
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
+++ 
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
@@ -33,6 +33,7 @@ import org.apache.airavata.grpc.GrpcStatusMapper;
 import org.apache.airavata.interfaces.ExperimentRegistry;
 import org.apache.airavata.interfaces.FileMetadata;
 import org.apache.airavata.interfaces.GatewayStoragePreferenceProvider;
+import org.apache.airavata.interfaces.StorageProvider;
 import org.apache.airavata.interfaces.StorageResourceAdaptor;
 import 
org.apache.airavata.model.appcatalog.gatewayprofile.proto.StoragePreference;
 import org.apache.airavata.model.data.movement.proto.DataMovementProtocol;
@@ -52,14 +53,37 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
     private final AdaptorSupport adaptorSupport;
     private final ExperimentRegistry experimentRegistry;
     private final GatewayStoragePreferenceProvider 
gatewayStoragePreferenceProvider;
+    private final StorageProvider storageProvider;
 
     public UserStorageGrpcService(
             AdaptorSupport adaptorSupport,
             ExperimentRegistry experimentRegistry,
-            GatewayStoragePreferenceProvider gatewayStoragePreferenceProvider) 
{
+            GatewayStoragePreferenceProvider gatewayStoragePreferenceProvider,
+            StorageProvider storageProvider) {
         this.adaptorSupport = adaptorSupport;
         this.experimentRegistry = experimentRegistry;
         this.gatewayStoragePreferenceProvider = 
gatewayStoragePreferenceProvider;
+        this.storageProvider = storageProvider;
+    }
+
+    /**
+     * Resolve (registering if necessary) the data product URI for a stored 
file, so listings and
+     * metadata can expose a stable URI per file. Returns "" for directories 
or on any failure
+     * (best-effort: a missing data product URI must not fail the listing).
+     */
+    private String resolveDataProductUri(FileMetadata meta, String path, 
String storageResourceId) {
+        if (meta.isDirectory()) {
+            return "";
+        }
+        try {
+            RequestContext ctx = GrpcRequestContext.current();
+            String uri = storageProvider.getOrCreateDataProductByPath(
+                    ctx.getGatewayId(), ctx.getUserId(), meta.getName(), path, 
storageResourceId);
+            return uri != null ? uri : "";
+        } catch (Exception e) {
+            logger.warn("Could not resolve data product URI for {}", path, e);
+            return "";
+        }
     }
 
     private StoragePreference resolveStoragePreference(String 
storageResourceId) throws Exception {
@@ -79,6 +103,15 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
         return prefs.get(0);
     }
 
+    /** Resolve the effective storage resource id: the request's, else the 
gateway default. */
+    private String resolveStorageResourceId(String storageResourceId) throws 
Exception {
+        if (storageResourceId != null && !storageResourceId.isEmpty()) {
+            return storageResourceId;
+        }
+        StoragePreference pref = resolveStoragePreference(storageResourceId);
+        return pref != null ? pref.getStorageResourceId() : "";
+    }
+
     private StorageResourceAdaptor getStorageAdaptor(String storageResourceId) 
throws Exception {
         RequestContext ctx = GrpcRequestContext.current();
         String resolvedId = storageResourceId;
@@ -214,6 +247,7 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
         try {
             StorageResourceAdaptor adaptor = 
getStorageAdaptor(request.getStorageResourceId());
             String path = resolvePath(request.getPath(), 
request.getStorageResourceId());
+            String storageResourceId = 
resolveStorageResourceId(request.getStorageResourceId());
 
             List<String> entries = adaptor.listDirectory(path);
             ListDirResponse.Builder responseBuilder = 
ListDirResponse.newBuilder();
@@ -221,7 +255,8 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
             for (String entry : entries) {
                 String fullPath = path.endsWith("/") ? path + entry : path + 
"/" + entry;
                 FileMetadata meta = adaptor.getFileMetadata(fullPath);
-                FileMetadataResponse fileMeta = toFileMetadataResponse(meta, 
fullPath);
+                FileMetadataResponse fileMeta = toFileMetadataResponse(
+                        meta, fullPath, resolveDataProductUri(meta, fullPath, 
storageResourceId));
                 if (meta.isDirectory()) {
                     responseBuilder.addDirectories(fileMeta);
                 } else {
@@ -316,8 +351,10 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
         try {
             StorageResourceAdaptor adaptor = 
getStorageAdaptor(request.getStorageResourceId());
             String path = resolvePath(request.getPath(), 
request.getStorageResourceId());
+            String storageResourceId = 
resolveStorageResourceId(request.getStorageResourceId());
             FileMetadata meta = adaptor.getFileMetadata(path);
-            observer.onNext(toFileMetadataResponse(meta, path));
+            observer.onNext(toFileMetadataResponse(
+                    meta, path, resolveDataProductUri(meta, path, 
storageResourceId)));
             observer.onCompleted();
         } catch (Exception e) {
             observer.onError(GrpcStatusMapper.toStatusException(e));
@@ -375,7 +412,8 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
                 String fullPath =
                         experimentDataDir.endsWith("/") ? experimentDataDir + 
entry : experimentDataDir + "/" + entry;
                 FileMetadata meta = adaptor.getFileMetadata(fullPath);
-                FileMetadataResponse fileMeta = toFileMetadataResponse(meta, 
fullPath);
+                FileMetadataResponse fileMeta = toFileMetadataResponse(
+                        meta, fullPath, resolveDataProductUri(meta, fullPath, 
storageResourceId));
                 if (meta.isDirectory()) {
                     responseBuilder.addDirectories(fileMeta);
                 } else {
@@ -416,7 +454,8 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
         }
     }
 
-    private static FileMetadataResponse toFileMetadataResponse(FileMetadata 
meta, String path) {
+    private static FileMetadataResponse toFileMetadataResponse(
+            FileMetadata meta, String path, String dataProductUri) {
         return FileMetadataResponse.newBuilder()
                 .setName(meta.getName() != null ? meta.getName() : "")
                 .setPath(path)
@@ -424,6 +463,7 @@ public class UserStorageGrpcService extends 
UserStorageServiceGrpc.UserStorageSe
                 .setIsDirectory(meta.isDirectory())
                 .setModifiedTime(meta.getModifiedTime())
                 .setContentType(meta.getContentType() != null ? 
meta.getContentType() : "")
+                .setDataProductUri(dataProductUri != null ? dataProductUri : 
"")
                 .build();
     }
 }

Reply via email to