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