From bc4c1415ebbd7079a61e3097464c7efbe86e8e6c Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Fri, 19 Apr 2024 10:49:09 +0200
Subject: [PATCH] BIS-741 : AFS uses DSS store - AFS changes

---
 .../java/ch/ethz/sis/shared/io/IOUtils.java   |  53 +++++++++
 server-data-store/build.gradle                |   4 +-
 .../AtomicFileSystemServerParameter.java      |   1 +
 .../sis/afsserver/worker/WorkerFactory.java   |   2 +-
 .../OpenBISAuthorizationInfoProvider.java     |  59 +++++-----
 .../afsserver/worker/proxy/ExecutorProxy.java | 109 +++++++++++++++---
 .../sis/afsserver/worker/proxy/ProxyUtil.java | 109 ++++++++++++++++++
 7 files changed, 290 insertions(+), 47 deletions(-)
 create mode 100644 server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ProxyUtil.java

diff --git a/lib-transactional-file-system/src/main/java/ch/ethz/sis/shared/io/IOUtils.java b/lib-transactional-file-system/src/main/java/ch/ethz/sis/shared/io/IOUtils.java
index 627a0c3edad..e2fd2439149 100644
--- a/lib-transactional-file-system/src/main/java/ch/ethz/sis/shared/io/IOUtils.java
+++ b/lib-transactional-file-system/src/main/java/ch/ethz/sis/shared/io/IOUtils.java
@@ -20,6 +20,7 @@ import ch.ethz.sis.afs.api.dto.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.AtomicMoveNotSupportedException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileStore;
@@ -47,6 +48,7 @@ import java.security.MessageDigest;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.util.*;
+import java.util.stream.Collectors;
 
 public class IOUtils {
 
@@ -630,6 +632,57 @@ public class IOUtils {
         }
     }
 
+    public static String[] getShares(String folder){
+        try
+        {
+            return Files.list(Paths.get(folder)).filter(file ->
+            {
+                if (!Files.isDirectory(file))
+                {
+                    return false;
+                }
+                try
+                {
+                    Integer.parseInt(file.getFileName().toString());
+                    return true;
+                } catch (NumberFormatException e)
+                {
+                    return false;
+                }
+            }).map(file -> file.getFileName().toString()).toArray(String[]::new);
+        } catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static String[] getShards(String str) {
+        byte[] md5 = getMD5(str.getBytes(StandardCharsets.UTF_8));
+        String hex = asHex(md5);
+        return new String[]{
+            hex.substring(0, 2),
+            hex.substring(2,4),
+            hex.substring(4,6)
+        };
+    }
+
+    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+
+    public static String asHex(byte[] bytes) {
+        char[] hex = new char[bytes.length * 2];
+
+        int bytesIndex = 0;
+        int hexIndex = 0;
+
+        while(bytesIndex < bytes.length) {
+            hex[hexIndex++] = HEX_ARRAY[bytes[bytesIndex] >>> 4 & 0x0F];
+            hex[hexIndex++] = HEX_ARRAY[bytes[bytesIndex] & 0x0F];
+            bytesIndex++;
+        }
+
+        return new String(hex);
+    }
+
     public static String encodeBase64(byte[] input) {
         return Base64.getEncoder().encodeToString(input);
     }
diff --git a/server-data-store/build.gradle b/server-data-store/build.gradle
index 45908299720..47b932d13dd 100644
--- a/server-data-store/build.gradle
+++ b/server-data-store/build.gradle
@@ -23,12 +23,12 @@ dependencies {
     annotationProcessor 'lombok:lombok:1.18.22'
     implementation project(':lib-transactional-file-system'),
             project(':api-data-store-server-java'),
+            project(':api-openbis-java'),
             project(':lib-json'),
             'lombok:lombok:1.18.22',
             'io.netty:netty-all:4.1.68.Final',
             'log4j:log4j-api:2.10.0',
-            'log4j:log4j-core:2.10.0',
-            'openbis:openbis-v3-api-batteries-included:20.10.5';
+            'log4j:log4j-core:2.10.0'
     testImplementation 'junit:junit:4.10',
             'hamcrest:hamcrest-core:1.3'
 }
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/startup/AtomicFileSystemServerParameter.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/startup/AtomicFileSystemServerParameter.java
index 4c85918d48d..7e540481a25 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/startup/AtomicFileSystemServerParameter.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/startup/AtomicFileSystemServerParameter.java
@@ -27,6 +27,7 @@ public enum AtomicFileSystemServerParameter {
     jsonObjectMapperClass,
     writeAheadLogRoot,
     storageRoot,
+    storageUuid,
     //
     // Parameters for the HTTP server
     //
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/WorkerFactory.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/WorkerFactory.java
index 007f4511187..4fc1f94960f 100755
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/WorkerFactory.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/WorkerFactory.java
@@ -29,7 +29,7 @@ public class WorkerFactory extends AbstractFactory<Configuration, Configuration,
     public Worker create(Configuration configuration) throws Exception {
 
         // 5. Execute the operation
-        AuditorProxy executorProxy = new AuditorProxy(new ExecutorProxy());
+        AuditorProxy executorProxy = new AuditorProxy(new ExecutorProxy(configuration));
 
         // 4. Check that the user have rights to do the operation
         AuthorizationInfoProvider authorizationInfoProvider = configuration.getInstance(AtomicFileSystemServerParameter.authorizationInfoProviderClass);
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/OpenBISAuthorizationInfoProvider.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/OpenBISAuthorizationInfoProvider.java
index 1f92d5a503c..e5abec89a88 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/OpenBISAuthorizationInfoProvider.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/OpenBISAuthorizationInfoProvider.java
@@ -15,61 +15,64 @@
  */
 package ch.ethz.sis.afsserver.worker.providers.impl;
 
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
 import ch.ethz.sis.afsserver.worker.providers.AuthorizationInfoProvider;
+import ch.ethz.sis.afsserver.worker.proxy.ProxyUtil;
 import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IPermIdHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.rights.Right;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.rights.Rights;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.rights.fetchoptions.RightsFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.ISampleId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
 import ch.ethz.sis.shared.io.FilePermission;
 import ch.ethz.sis.shared.startup.Configuration;
 import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
 
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class OpenBISAuthorizationInfoProvider implements AuthorizationInfoProvider {
+public class OpenBISAuthorizationInfoProvider implements AuthorizationInfoProvider
+{
 
     private IApplicationServerApi v3 = null;
 
     @Override
-    public void init(Configuration initParameter) throws Exception {
+    public void init(Configuration initParameter) throws Exception
+    {
         String openBISUrl = initParameter.getStringProperty(AtomicFileSystemServerParameter.openBISUrl);
         int openBISTimeout = initParameter.getIntegerProperty(AtomicFileSystemServerParameter.openBISTimeout);
         v3 = HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, openBISUrl, openBISTimeout);
     }
 
     @Override
-    public boolean doesSessionHaveRights(String sessionToken, String owner, Set<FilePermission> permissions) {
-        Set<FilePermission> found = new HashSet<>();
+    public boolean doesSessionHaveRights(String sessionToken, String owner, Set<FilePermission> permissions)
+    {
+        IPermIdHolder foundOwner = ProxyUtil.findOwner(v3, sessionToken, owner);
 
-        ISampleId identifier = null;
-        if (owner.contains("/")) { // Is Identifier
-            identifier = new SampleIdentifier(owner);
-        } else { // Is permId
-            identifier = new SamplePermId(owner);
-        }
-        Map<ISampleId, Sample> samples = v3.getSamples(sessionToken, List.of(identifier), new SampleFetchOptions());
-        if (!samples.isEmpty()) {
-            found.add(FilePermission.Read);
+        if (foundOwner == null)
+        {
+            return false;
         }
-        Rights rights = v3.getRights(sessionToken, List.of(identifier), new RightsFetchOptions()).get(identifier);
-        if (rights.getRights().contains(Right.UPDATE)) {
-            found.add(FilePermission.Write);
+
+        Set<FilePermission> foundPermissions = new HashSet<>();
+        foundPermissions.add(FilePermission.Read);
+
+        Rights rights = v3.getRights(sessionToken, List.of(foundOwner.getPermId()), new RightsFetchOptions()).get(foundOwner.getPermId());
+
+        if (rights.getRights().contains(Right.UPDATE))
+        {
+            foundPermissions.add(FilePermission.Write);
         }
 
-        for (FilePermission permission:permissions) {
-            if (!found.contains(permission)) {
+        for (FilePermission permission : permissions)
+        {
+            if (!foundPermissions.contains(permission))
+            {
                 return false;
             }
         }
+
         return true;
     }
+
 }
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ExecutorProxy.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ExecutorProxy.java
index 2b4b5e71ecf..21e208255da 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ExecutorProxy.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ExecutorProxy.java
@@ -15,20 +15,42 @@
  */
 package ch.ethz.sis.afsserver.worker.proxy;
 
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
+import ch.ethz.sis.afs.exception.AFSExceptions;
 import ch.ethz.sis.afsapi.dto.File;
 import ch.ethz.sis.afsapi.dto.FreeSpace;
+import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
 import ch.ethz.sis.afsserver.worker.AbstractProxy;
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IPermIdHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
 import ch.ethz.sis.shared.io.IOUtils;
+import ch.ethz.sis.shared.startup.Configuration;
+import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
 import lombok.NonNull;
 
-public class ExecutorProxy extends AbstractProxy {
+public class ExecutorProxy extends AbstractProxy
+{
 
-    public ExecutorProxy() {
+    private final IApplicationServerApi v3;
+
+    private final String storageRoot;
+
+    private final String storageUuid;
+
+    public ExecutorProxy(final Configuration configuration)
+    {
         super(null);
+        String openBISUrl = configuration.getStringProperty(AtomicFileSystemServerParameter.openBISUrl);
+        int openBISTimeout = configuration.getIntegerProperty(AtomicFileSystemServerParameter.openBISTimeout);
+        v3 = HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, openBISUrl, openBISTimeout);
+        storageRoot = configuration.getStringProperty(AtomicFileSystemServerParameter.storageRoot);
+        storageUuid = configuration.getStringProperty(AtomicFileSystemServerParameter.storageUuid);
     }
 
     //
@@ -36,28 +58,33 @@ public class ExecutorProxy extends AbstractProxy {
     //
 
     @Override
-    public void begin(UUID transactionId) throws Exception {
+    public void begin(UUID transactionId) throws Exception
+    {
         workerContext.setTransactionId(transactionId);
         workerContext.getConnection().begin(transactionId);
     }
 
     @Override
-    public Boolean prepare() throws Exception {
+    public Boolean prepare() throws Exception
+    {
         return workerContext.getConnection().prepare();
     }
 
     @Override
-    public void commit() throws Exception {
+    public void commit() throws Exception
+    {
         workerContext.getConnection().commit();
     }
 
     @Override
-    public void rollback() throws Exception {
+    public void rollback() throws Exception
+    {
         workerContext.getConnection().rollback();
     }
 
     @Override
-    public List<UUID> recover() throws Exception {
+    public List<UUID> recover() throws Exception
+    {
         return workerContext.getConnection().recover();
     }
 
@@ -65,45 +92,95 @@ public class ExecutorProxy extends AbstractProxy {
     // File System Operations
     //
 
-    public String getPath(String owner, String source) {
-        return String.join(""+IOUtils.PATH_SEPARATOR, "", owner.toString(), source);
+    public String getPath(String owner, String source)
+    {
+        if (storageUuid == null)
+        {
+            // AFS does not reuse DSS store folder
+            return String.join("" + IOUtils.PATH_SEPARATOR, "", owner.toString(), source);
+        } else
+        {
+            // AFS reuses DSS store folder
+            IPermIdHolder foundOwner = ProxyUtil.findOwner(v3, workerContext.getSessionToken(), owner);
+
+            if (foundOwner == null)
+            {
+                throw AFSExceptions.NotAPath.getInstance(owner);
+            }
+
+            String foundOwnerPath = null;
+
+            if (foundOwner instanceof DataSet)
+            {
+                DataSet foundDataSet = (DataSet) foundOwner;
+                foundOwnerPath = foundDataSet.getPhysicalData().getShareId() + "/" + foundDataSet.getPhysicalData().getLocation();
+            } else
+            {
+                String[] shares = IOUtils.getShares(storageRoot);
+                String[] shards = IOUtils.getShards(owner);
+
+                for (String share : shares)
+                {
+                    String potentialOwnerPath = share + "/" + storageUuid + String.join("/", shards) + "/" + foundOwner.getPermId().toString();
+                    if (Files.exists(Paths.get(potentialOwnerPath)))
+                    {
+                        foundOwnerPath = potentialOwnerPath;
+                        break;
+                    }
+                }
+            }
+
+            if (foundOwnerPath == null)
+            {
+                throw AFSExceptions.NotAPath.getInstance(owner);
+            }
+
+            return String.join("" + IOUtils.PATH_SEPARATOR, "", foundOwnerPath, source);
+        }
     }
 
     @Override
-    public List<File> list(String owner, String source, Boolean recursively) throws Exception {
+    public List<File> list(String owner, String source, Boolean recursively) throws Exception
+    {
         return workerContext.getConnection().list(getPath(owner, source), recursively)
                 .stream()
                 .map(file -> convertToFile(owner, file))
                 .collect(Collectors.toList());
     }
 
-    private File convertToFile(String owner, ch.ethz.sis.afs.api.dto.File file) {
+    private File convertToFile(String owner, ch.ethz.sis.afs.api.dto.File file)
+    {
         return new File(owner, file.getPath().substring(owner.length() + 1), file.getName(), file.getDirectory(), file.getSize(),
                 file.getLastModifiedTime(), file.getCreationTime(), file.getLastAccessTime());
     }
 
     @Override
-    public byte[] read(String owner, String source, Long offset, Integer limit) throws Exception {
+    public byte[] read(String owner, String source, Long offset, Integer limit) throws Exception
+    {
         return workerContext.getConnection().read(getPath(owner, source), offset, limit);
     }
 
     @Override
-    public Boolean write(String owner, String source, Long offset, byte[] data, byte[] md5Hash) throws Exception {
+    public Boolean write(String owner, String source, Long offset, byte[] data, byte[] md5Hash) throws Exception
+    {
         return workerContext.getConnection().write(getPath(owner, source), offset, data, md5Hash);
     }
 
     @Override
-    public Boolean delete(String owner, String source) throws Exception {
+    public Boolean delete(String owner, String source) throws Exception
+    {
         return workerContext.getConnection().delete(getPath(owner, source));
     }
 
     @Override
-    public Boolean copy(String sourceOwner, String source, String targetOwner, String target) throws Exception {
+    public Boolean copy(String sourceOwner, String source, String targetOwner, String target) throws Exception
+    {
         return workerContext.getConnection().copy(getPath(sourceOwner, source), getPath(targetOwner, target));
     }
 
     @Override
-    public Boolean move(String sourceOwner, String source, String targetOwner, String target) throws Exception {
+    public Boolean move(String sourceOwner, String source, String targetOwner, String target) throws Exception
+    {
         return workerContext.getConnection().move(getPath(sourceOwner, source), getPath(targetOwner, target));
     }
 
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ProxyUtil.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ProxyUtil.java
new file mode 100644
index 00000000000..07c7dbdbf86
--- /dev/null
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/worker/proxy/ProxyUtil.java
@@ -0,0 +1,109 @@
+package ch.ethz.sis.afsserver.worker.proxy;
+
+import java.util.List;
+import java.util.Map;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IPermIdHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.IDataSetId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.IExperimentId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.ISampleId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
+
+public class ProxyUtil
+{
+
+    public static IPermIdHolder findOwner(IApplicationServerApi v3, String sessionToken, String owner)
+    {
+        Experiment foundExperiment = findExperiment(v3, sessionToken, owner);
+
+        if (foundExperiment != null)
+        {
+            return foundExperiment;
+        }
+
+        Sample foundSample = ProxyUtil.findSample(v3, sessionToken, owner);
+
+        if (foundSample != null)
+        {
+            return foundSample;
+        }
+
+        return ProxyUtil.findDataSet(v3, sessionToken, owner);
+    }
+
+    public static Experiment findExperiment(IApplicationServerApi v3, String sessionToken, String experimentPermIdOrIdentifier)
+    {
+        IExperimentId experimentId;
+
+        if (experimentPermIdOrIdentifier.contains("/"))
+        { // Is Identifier
+            experimentId = new ExperimentIdentifier(experimentPermIdOrIdentifier);
+        } else
+        { // Is permId
+            experimentId = new ExperimentPermId(experimentPermIdOrIdentifier);
+        }
+
+        Map<IExperimentId, Experiment> experiments = v3.getExperiments(sessionToken, List.of(experimentId), new ExperimentFetchOptions());
+
+        if (!experiments.isEmpty())
+        {
+            return experiments.get(experimentId);
+        } else
+        {
+            return null;
+        }
+    }
+
+    public static Sample findSample(IApplicationServerApi v3, String sessionToken, String samplePermIdOrIdentifier)
+    {
+        ISampleId sampleId;
+
+        if (samplePermIdOrIdentifier.contains("/"))
+        { // Is Identifier
+            sampleId = new SampleIdentifier(samplePermIdOrIdentifier);
+        } else
+        { // Is permId
+            sampleId = new SamplePermId(samplePermIdOrIdentifier);
+        }
+
+        Map<ISampleId, Sample> samples = v3.getSamples(sessionToken, List.of(sampleId), new SampleFetchOptions());
+
+        if (!samples.isEmpty())
+        {
+            return samples.get(sampleId);
+        } else
+        {
+            return null;
+        }
+    }
+
+    public static DataSet findDataSet(IApplicationServerApi v3, String sessionToken, String dataSetPermId)
+    {
+        IDataSetId dataSetId = new DataSetPermId(dataSetPermId);
+
+        DataSetFetchOptions fo = new DataSetFetchOptions();
+        fo.withPhysicalData();
+
+        Map<IDataSetId, DataSet> dataSets = v3.getDataSets(sessionToken, List.of(dataSetId), fo);
+
+        if (!dataSets.isEmpty())
+        {
+            return dataSets.get(dataSetId);
+        } else
+        {
+            return null;
+        }
+    }
+
+}
-- 
GitLab