diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3f0616 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +### Maven ### +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +.idea/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6538ca1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,156 @@ +# Contribution guide + +First, thank you for contributing! We love and encourage pull requests from +everyone. Please follow the guidelines: + +- Check the open [issues](https://git.frostfs.info/TrueCloudLab/frostfs-sdk-java/issues) and + [pull requests](https://git.frostfs.info/TrueCloudLab/frostfs-sdk-java/pulls) for existing + discussions. + +- Open an issue first, to discuss a new feature or enhancement. + +- Write tests and make sure the test suite passes locally and on CI. + +- Open a pull request and reference the relevant issue(s). + +- Make sure your commits are logically separated and have good comments + explaining the details of your change. + +- After receiving a feedback, amend your commits or add new ones as + appropriate. + +- **Have fun!** + +## Development Workflow + +Start by forking the `frostfs-sdk-java` repository, make changes in a branch and then +send a pull request. We encourage pull requests to discuss code changes. Here +are the steps in details: + +### Set up your git repository +Fork [FrostFS S3 Gateway +upstream](https://git.frostfs.info/repo/fork/346) source repository +to your own personal repository. Copy the URL of your fork (you will need it for +the `git clone` command below). + +```sh +$ git clone https://git.frostfs.info//frostfs-sdk-java.git +``` + +### Set up git remote as ``upstream`` +```sh +$ cd frostfs-sdk-java +$ git remote add upstream https://git.frostfs.info/TrueCloudLab/frostfs-sdk-java.git +$ git fetch upstream +$ git merge upstream/master +... +``` + +### Create your feature branch +Before making code changes, make sure you create a separate branch for these +changes. Maybe you will find it convenient to name a branch in +`/-` format. + +``` +$ git checkout -b feature/123-something_awesome +``` + +### Test your changes +After your code changes, make sure + +- To add test cases for the new code. +- To run `mvn clean verify` +- To squash your commits into a single commit or a series of logically separated + commits with `git rebase -i`. It's okay to force update your pull request. +- To run `mvn clean package` successfully. + +### Commit changes +After verification, commit your changes. There is a [great +post](https://chris.beams.io/posts/git-commit/) on how to write useful commit +messages. Try following this template: + +``` +[#Issue] Summary + +Description + + + + +``` + +``` +$ git commit -ams '[#123] Add some feature' +``` + +### Push to the branch +Push your locally committed changes to the remote origin (your fork) +``` +$ git push origin feature/123-something_awesome +``` + +### Create a Pull Request +Pull requests can be created via Forgejo. Refer to [this +document](https://docs.codeberg.org/collaborating/pull-requests-and-git-flow/) for +detailed steps on how to create a pull request. After a Pull Request gets peer +reviewed and approved, it will be merged. + +## DCO Sign off + +All authors to the project retain copyright to their work. However, to ensure +that they are only submitting work that they have rights to, we require +everyone to acknowledge this by signing their work. + +Any copyright notices in this repository should specify the authors as "the +contributors". + +To sign your work, just add a line like this at the end of your commit message: + +``` +Signed-off-by: Samii Sakisaka +``` + +This can be easily done with the `--signoff` option to `git commit`. + +By doing this you state that you can certify the following (from [The Developer +Certificate of Origin](https://developercertificate.org/)): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` \ No newline at end of file diff --git a/README.md b/README.md index 9f37b2a..fe3c04f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,97 @@ # frostfs-sdk-java Java implementation of FrostFS SDK + +## Prerequisites + +### Get the key for your wallet + +1. Get the address +```bash +cat | jq .accounts[0].address | tr -d '"' +``` + +2. Get the key +```bash +neo-go wallet export -w -d +``` + +## Example usage + +### Container operations + +```java +import info.frostfs.sdk.dto.container.Container; +import info.frostfs.sdk.dto.netmap.PlacementPolicy; +import info.frostfs.sdk.dto.netmap.Replica; +import info.frostfs.sdk.enums.BasicAcl; +import info.frostfs.sdk.jdo.ClientSettings; +import info.frostfs.sdk.FrostFSClient; + +public class ContainerExample { + + public void example() { + ClientSettings clientSettings = new ClientSettings(, ); + FrostFSClient frostFSClient = new FrostFSClient(clientSettings); + + // Create container + var placementPolicy = new PlacementPolicy(true, new Replica[]{new Replica(1)}); + var containerId = frostFSClient.createContainer(new Container(BasicAcl.PUBLIC_RW, placementPolicy)); + + // Get container + var container = frostFSClient.getContainer(containerId); + + // List containers + var containerIds = frostFSClient.listContainers(); + + // Delete container + frostFSClient.deleteContainer(containerId); + } +} +``` + +### Object operations + +```java +import info.frostfs.sdk.enums.ObjectType; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.dto.object.ObjectAttribute; +import info.frostfs.sdk.dto.object.ObjectFilter; +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.dto.object.ObjectId; +import info.frostfs.sdk.jdo.PutObjectParameters; +import info.frostfs.sdk.FrostFSClient; + +import java.io.FileInputStream; +import java.io.IOException; + +public class ObjectExample { + + public void example() { + ClientSettings clientSettings = new ClientSettings(, ); + FrostFSClient frostFSClient = new FrostFSClient(clientSettings); + + // Put object + ObjectId objectId; + try (FileInputStream fis = new FileInputStream("cat.jpg")) { + var cat = new ObjectHeader( + containerId, ObjectType.REGULAR, new ObjectAttribute[]{new ObjectAttribute("Filename", "cat.jpg")} + ); + + var params = new PutObjectParameters(cat, fis); + objectId = frostFSClient.putObject(params); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Get object + var obj = frostFSClient.getObject(containerId, objectId); + + // Get object header + var objectHeader = frostFSClient.getObjectHead(containerId, objectId); + + // Search regular objects + var objectIds = frostFSClient.searchObjects(containerId, ObjectFilter.RootFilter()); + } +} +``` \ No newline at end of file diff --git a/client/pom.xml b/client/pom.xml new file mode 100644 index 0000000..bb808ce --- /dev/null +++ b/client/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + info.frostfs.sdk + frostfs-sdk-java + 0.1.0 + + + client + + + 11 + 11 + UTF-8 + + + + + info.frostfs.sdk + cryptography + 0.1.0 + + + info.frostfs.sdk + models + 0.1.0 + + + org.apache.commons + commons-collections4 + 4.4 + + + commons-codec + commons-codec + 1.17.0 + + + + \ No newline at end of file diff --git a/client/src/main/java/info/frostfs/sdk/FrostFSClient.java b/client/src/main/java/info/frostfs/sdk/FrostFSClient.java new file mode 100644 index 0000000..48fdcc5 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/FrostFSClient.java @@ -0,0 +1,139 @@ +package info.frostfs.sdk; + +import frostfs.session.Types; +import info.frostfs.sdk.dto.SessionToken; +import info.frostfs.sdk.dto.Version; +import info.frostfs.sdk.dto.container.Container; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.dto.netmap.NetmapSnapshot; +import info.frostfs.sdk.dto.netmap.NodeInfo; +import info.frostfs.sdk.dto.object.ObjectFilter; +import info.frostfs.sdk.dto.object.ObjectFrostFS; +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.dto.object.ObjectId; +import info.frostfs.sdk.jdo.ClientEnvironment; +import info.frostfs.sdk.jdo.ClientSettings; +import info.frostfs.sdk.jdo.NetworkSettings; +import info.frostfs.sdk.jdo.PutObjectParameters; +import info.frostfs.sdk.services.*; +import info.frostfs.sdk.services.impl.*; +import io.grpc.Channel; + +import java.util.List; + +import static info.frostfs.sdk.tools.GrpcClient.initGrpcChannel; +import static java.util.Objects.isNull; + +public class FrostFSClient implements ContainerClient, ObjectClient, NetmapClient, SessionClient, ToolsClient { + private static final String ERROR_CLIENT_OPTIONS_INIT = "Options must be initialized."; + private static final String ERROR_VERSION_SUPPORT_TEMPLATE = "FrostFS %s is not supported."; + + private final ContainerClientImpl containerClientImpl; + private final NetmapClientImpl netmapClientImpl; + private final ObjectClientImpl objectClientImpl; + private final SessionClientImpl sessionClientImpl; + private final ObjectToolsImpl objectToolsImpl; + + public FrostFSClient(ClientSettings clientSettings) { + if (isNull(clientSettings)) { + throw new IllegalArgumentException(ERROR_CLIENT_OPTIONS_INIT); + } + + clientSettings.validate(); + + Channel channel = initGrpcChannel(clientSettings.getHost(), clientSettings.getCreds()); + + ClientEnvironment clientEnvironment = + new ClientEnvironment(clientSettings.getKey(), channel, new Version(), this); + + this.containerClientImpl = new ContainerClientImpl(clientEnvironment); + this.netmapClientImpl = new NetmapClientImpl(clientEnvironment); + this.sessionClientImpl = new SessionClientImpl(clientEnvironment); + this.objectClientImpl = new ObjectClientImpl(clientEnvironment); + this.objectToolsImpl = new ObjectToolsImpl(clientEnvironment); + checkFrostFsVersionSupport(clientEnvironment.getVersion()); + } + + private void checkFrostFsVersionSupport(Version version) { + var localNodeInfo = netmapClientImpl.getLocalNodeInfo(); + if (!localNodeInfo.getVersion().isSupported(version)) { + throw new IllegalArgumentException( + String.format(ERROR_VERSION_SUPPORT_TEMPLATE, localNodeInfo.getVersion()) + ); + } + } + + @Override + public Container getContainer(ContainerId cid) { + return containerClientImpl.getContainer(cid); + } + + @Override + public List listContainers() { + return containerClientImpl.listContainers(); + } + + @Override + public ContainerId createContainer(Container container) { + return containerClientImpl.createContainer(container); + } + + @Override + public void deleteContainer(ContainerId cid) { + containerClientImpl.deleteContainer(cid); + } + + @Override + public ObjectHeader getObjectHead(ContainerId containerId, ObjectId objectId) { + return objectClientImpl.getObjectHead(containerId, objectId); + } + + @Override + public ObjectFrostFS getObject(ContainerId containerId, ObjectId objectId) { + return objectClientImpl.getObject(containerId, objectId); + } + + @Override + public ObjectId putObject(PutObjectParameters parameters) { + return objectClientImpl.putObject(parameters); + } + + @Override + public void deleteObject(ContainerId containerId, ObjectId objectId) { + objectClientImpl.deleteObject(containerId, objectId); + } + + @Override + public Iterable searchObjects(ContainerId cid, ObjectFilter... filters) { + return objectClientImpl.searchObjects(cid, filters); + } + + @Override + public NetmapSnapshot getNetmapSnapshot() { + return netmapClientImpl.getNetmapSnapshot(); + } + + @Override + public NodeInfo getLocalNodeInfo() { + return netmapClientImpl.getLocalNodeInfo(); + } + + @Override + public NetworkSettings getNetworkSettings() { + return netmapClientImpl.getNetworkSettings(); + } + + @Override + public SessionToken createSession(long expiration) { + return sessionClientImpl.createSession(expiration); + } + + public Types.SessionToken createSessionInternal(long expiration) { + return sessionClientImpl.createSessionInternal(expiration); + } + + @Override + public ObjectId calculateObjectId(ObjectHeader header) { + return objectToolsImpl.calculateObjectId(header); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/constants/CryptoConst.java b/client/src/main/java/info/frostfs/sdk/constants/CryptoConst.java new file mode 100644 index 0000000..4e28686 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/constants/CryptoConst.java @@ -0,0 +1,8 @@ +package info.frostfs.sdk.constants; + +public class CryptoConst { + public static final String SIGNATURE_ALGORITHM = "NONEwithECDSAinP1363Format"; + + private CryptoConst() { + } +} diff --git a/client/src/main/java/info/frostfs/sdk/jdo/ClientEnvironment.java b/client/src/main/java/info/frostfs/sdk/jdo/ClientEnvironment.java new file mode 100644 index 0000000..6b140d3 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/jdo/ClientEnvironment.java @@ -0,0 +1,58 @@ +package info.frostfs.sdk.jdo; + +import info.frostfs.sdk.dto.OwnerId; +import info.frostfs.sdk.dto.Version; +import info.frostfs.sdk.FrostFSClient; +import io.grpc.Channel; +import org.apache.commons.lang3.StringUtils; + +import static java.util.Objects.isNull; + +public class ClientEnvironment { + private final OwnerId ownerId; + private final Version version; + private final ECDsa key; + private final Channel channel; + private final FrostFSClient frostFSClient; + private NetworkSettings networkSettings; + + public ClientEnvironment(String wif, Channel channel, Version version, FrostFSClient frostFSClient) { + if (StringUtils.isEmpty(wif) || isNull(channel) || isNull(version) || isNull(frostFSClient)) { + throw new IllegalArgumentException("One of the input attributes is missing"); + } + + this.key = new ECDsa(wif); + this.ownerId = new OwnerId(key.getPublicKeyByte()); + this.version = version; + this.channel = channel; + this.frostFSClient = frostFSClient; + } + + public Channel getChannel() { + return channel; + } + + public NetworkSettings getNetworkSettings() { + return networkSettings; + } + + public void setNetworkSettings(NetworkSettings networkSettings) { + this.networkSettings = networkSettings; + } + + public FrostFSClient getFrostFSClient() { + return frostFSClient; + } + + public OwnerId getOwnerId() { + return ownerId; + } + + public Version getVersion() { + return version; + } + + public ECDsa getKey() { + return key; + } +} diff --git a/client/src/main/java/info/frostfs/sdk/jdo/ClientSettings.java b/client/src/main/java/info/frostfs/sdk/jdo/ClientSettings.java new file mode 100644 index 0000000..5f7bd32 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/jdo/ClientSettings.java @@ -0,0 +1,65 @@ +package info.frostfs.sdk.jdo; + +import io.grpc.ChannelCredentials; +import org.apache.commons.lang3.StringUtils; + +public class ClientSettings { + private static final String ERROR_TEMPLATE = "%s is required parameter."; + + public String key; + public String host; + public ChannelCredentials creds; + + public ClientSettings(String key, String host) { + this.key = key; + this.host = host; + validate(); + } + + public ClientSettings(String key, String host, ChannelCredentials creds) { + this.key = key; + this.host = host; + this.creds = creds; + validate(); + } + + public ChannelCredentials getCreds() { + return creds; + } + + public void setCreds(ChannelCredentials creds) { + this.creds = creds; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public void validate() { + StringBuilder errorMessage = new StringBuilder(); + + if (StringUtils.isEmpty(key)) { + errorMessage.append(String.format(ERROR_TEMPLATE, "Key")).append(System.lineSeparator()); + } + + if (StringUtils.isEmpty(host)) { + errorMessage.append(String.format(ERROR_TEMPLATE, "Host")).append(System.lineSeparator()); + } + + if (errorMessage.length() != 0) { + throw new IllegalArgumentException(errorMessage.toString()); + } + } +} diff --git a/client/src/main/java/info/frostfs/sdk/jdo/ECDsa.java b/client/src/main/java/info/frostfs/sdk/jdo/ECDsa.java new file mode 100644 index 0000000..d328c10 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/jdo/ECDsa.java @@ -0,0 +1,35 @@ +package info.frostfs.sdk.jdo; + +import org.apache.commons.lang3.StringUtils; + +import java.security.PrivateKey; + +import static info.frostfs.sdk.KeyExtension.*; + +public class ECDsa { + private final byte[] publicKeyByte; + private final byte[] privateKeyByte; + private final PrivateKey privateKey; + + public ECDsa(String wif) { + if (StringUtils.isEmpty(wif)) { + throw new IllegalArgumentException("Wif is invalid"); + } + + this.privateKeyByte = getPrivateKeyFromWIF(wif); + this.publicKeyByte = loadPublicKey(privateKeyByte); + this.privateKey = loadPrivateKey(privateKeyByte); + } + + public byte[] getPublicKeyByte() { + return publicKeyByte; + } + + public byte[] getPrivateKeyByte() { + return privateKeyByte; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } +} diff --git a/client/src/main/java/info/frostfs/sdk/jdo/NetworkSettings.java b/client/src/main/java/info/frostfs/sdk/jdo/NetworkSettings.java new file mode 100644 index 0000000..1ed01a6 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/jdo/NetworkSettings.java @@ -0,0 +1,143 @@ +package info.frostfs.sdk.jdo; + +import java.util.HashMap; +import java.util.Map; + +public class NetworkSettings { + + public Long auditFee; + public Long basicIncomeRate; + public Long containerFee; + public Long containerAliasFee; + public Long innerRingCandidateFee; + public Long withdrawFee; + public Long epochDuration; + public Long iRCandidateFee; + public Long maxObjectSize; + public Long maxECDataCount; + public Long maxECParityCount; + public Long withdrawalFee; + public Boolean homomorphicHashingDisabled; + public Boolean maintenanceModeAllowed; + public Map unnamedSettings = new HashMap<>(); + + public Long getAuditFee() { + return auditFee; + } + + public void setAuditFee(Long auditFee) { + this.auditFee = auditFee; + } + + public Long getBasicIncomeRate() { + return basicIncomeRate; + } + + public void setBasicIncomeRate(Long basicIncomeRate) { + this.basicIncomeRate = basicIncomeRate; + } + + public long getContainerFee() { + return containerFee; + } + + public void setContainerFee(long containerFee) { + this.containerFee = containerFee; + } + + public long getContainerAliasFee() { + return containerAliasFee; + } + + public void setContainerAliasFee(long containerAliasFee) { + this.containerAliasFee = containerAliasFee; + } + + public long getInnerRingCandidateFee() { + return innerRingCandidateFee; + } + + public void setInnerRingCandidateFee(long innerRingCandidateFee) { + this.innerRingCandidateFee = innerRingCandidateFee; + } + + public long getWithdrawFee() { + return withdrawFee; + } + + public void setWithdrawFee(long withdrawFee) { + this.withdrawFee = withdrawFee; + } + + public long getEpochDuration() { + return epochDuration; + } + + public void setEpochDuration(long epochDuration) { + this.epochDuration = epochDuration; + } + + public long getiRCandidateFee() { + return iRCandidateFee; + } + + public void setiRCandidateFee(long iRCandidateFee) { + this.iRCandidateFee = iRCandidateFee; + } + + public long getMaxObjectSize() { + return maxObjectSize; + } + + public void setMaxObjectSize(long maxObjectSize) { + this.maxObjectSize = maxObjectSize; + } + + public long getMaxECDataCount() { + return maxECDataCount; + } + + public void setMaxECDataCount(long maxECDataCount) { + this.maxECDataCount = maxECDataCount; + } + + public long getMaxECParityCount() { + return maxECParityCount; + } + + public void setMaxECParityCount(long maxECParityCount) { + this.maxECParityCount = maxECParityCount; + } + + public long getWithdrawalFee() { + return withdrawalFee; + } + + public void setWithdrawalFee(long withdrawalFee) { + this.withdrawalFee = withdrawalFee; + } + + public boolean isHomomorphicHashingDisabled() { + return homomorphicHashingDisabled; + } + + public void setHomomorphicHashingDisabled(boolean homomorphicHashingDisabled) { + this.homomorphicHashingDisabled = homomorphicHashingDisabled; + } + + public boolean isMaintenanceModeAllowed() { + return maintenanceModeAllowed; + } + + public void setMaintenanceModeAllowed(boolean maintenanceModeAllowed) { + this.maintenanceModeAllowed = maintenanceModeAllowed; + } + + public Map getUnnamedSettings() { + return unnamedSettings; + } + + public void setUnnamedSettings(Map unnamedSettings) { + this.unnamedSettings = unnamedSettings; + } +} diff --git a/client/src/main/java/info/frostfs/sdk/jdo/PutObjectParameters.java b/client/src/main/java/info/frostfs/sdk/jdo/PutObjectParameters.java new file mode 100644 index 0000000..3c4ecdd --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/jdo/PutObjectParameters.java @@ -0,0 +1,80 @@ +package info.frostfs.sdk.jdo; + +import info.frostfs.sdk.dto.object.ObjectHeader; + +import java.io.FileInputStream; + +import static java.util.Objects.isNull; + +public class PutObjectParameters { + private static final String ERROR_TEMPLATE = "%s value cannot be null."; + + public ObjectHeader header; + public FileInputStream payload; + public boolean clientCut; + public int bufferMaxSize; + + public PutObjectParameters(ObjectHeader header, FileInputStream payload, boolean clientCut, int bufferMaxSize) { + this.header = header; + this.payload = payload; + this.clientCut = clientCut; + this.bufferMaxSize = bufferMaxSize; + + validate(); + } + + public PutObjectParameters(ObjectHeader header, FileInputStream payload) { + this.header = header; + this.payload = payload; + + validate(); + } + + public ObjectHeader getHeader() { + return header; + } + + public void setHeader(ObjectHeader header) { + this.header = header; + } + + public FileInputStream getPayload() { + return payload; + } + + public void setPayload(FileInputStream payload) { + this.payload = payload; + } + + public boolean isClientCut() { + return clientCut; + } + + public void setClientCut(boolean clientCut) { + this.clientCut = clientCut; + } + + public int getBufferMaxSize() { + return bufferMaxSize; + } + + public void setBufferMaxSize(int bufferMaxSize) { + this.bufferMaxSize = bufferMaxSize; + } + + public void validate() { + StringBuilder errorMessage = new StringBuilder(); + + if (isNull(header)) { + errorMessage.append(String.format(ERROR_TEMPLATE, "Header")).append(System.lineSeparator()); + } + + if (isNull(payload)) { + errorMessage.append(String.format(ERROR_TEMPLATE, "Payload")).append(System.lineSeparator()); + } + + if (errorMessage.length() != 0) { + throw new IllegalArgumentException(errorMessage.toString()); + } + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/ContainerClient.java b/client/src/main/java/info/frostfs/sdk/services/ContainerClient.java new file mode 100644 index 0000000..d3f7f9c --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/ContainerClient.java @@ -0,0 +1,16 @@ +package info.frostfs.sdk.services; + +import info.frostfs.sdk.dto.container.Container; +import info.frostfs.sdk.dto.container.ContainerId; + +import java.util.List; + +public interface ContainerClient { + Container getContainer(ContainerId cid); + + List listContainers(); + + ContainerId createContainer(Container container); + + void deleteContainer(ContainerId cid); +} diff --git a/client/src/main/java/info/frostfs/sdk/services/ContextAccessor.java b/client/src/main/java/info/frostfs/sdk/services/ContextAccessor.java new file mode 100644 index 0000000..72c47dd --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/ContextAccessor.java @@ -0,0 +1,15 @@ +package info.frostfs.sdk.services; + +import info.frostfs.sdk.jdo.ClientEnvironment; + +public class ContextAccessor { + private final ClientEnvironment context; + + public ContextAccessor(ClientEnvironment context) { + this.context = context; + } + + public ClientEnvironment getContext() { + return context; + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/NetmapClient.java b/client/src/main/java/info/frostfs/sdk/services/NetmapClient.java new file mode 100644 index 0000000..bc34978 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/NetmapClient.java @@ -0,0 +1,13 @@ +package info.frostfs.sdk.services; + +import info.frostfs.sdk.dto.netmap.NetmapSnapshot; +import info.frostfs.sdk.dto.netmap.NodeInfo; +import info.frostfs.sdk.jdo.NetworkSettings; + +public interface NetmapClient { + NetmapSnapshot getNetmapSnapshot(); + + NodeInfo getLocalNodeInfo(); + + NetworkSettings getNetworkSettings(); +} diff --git a/client/src/main/java/info/frostfs/sdk/services/ObjectClient.java b/client/src/main/java/info/frostfs/sdk/services/ObjectClient.java new file mode 100644 index 0000000..423e938 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/ObjectClient.java @@ -0,0 +1,20 @@ +package info.frostfs.sdk.services; + +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.dto.object.ObjectFilter; +import info.frostfs.sdk.dto.object.ObjectFrostFS; +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.dto.object.ObjectId; +import info.frostfs.sdk.jdo.PutObjectParameters; + +public interface ObjectClient { + ObjectHeader getObjectHead(ContainerId containerId, ObjectId objectId); + + ObjectFrostFS getObject(ContainerId containerId, ObjectId objectId); + + ObjectId putObject(PutObjectParameters parameters); + + void deleteObject(ContainerId containerId, ObjectId objectId); + + Iterable searchObjects(ContainerId cid, ObjectFilter... filters); +} diff --git a/client/src/main/java/info/frostfs/sdk/services/SessionClient.java b/client/src/main/java/info/frostfs/sdk/services/SessionClient.java new file mode 100644 index 0000000..16e0da0 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/SessionClient.java @@ -0,0 +1,7 @@ +package info.frostfs.sdk.services; + +import info.frostfs.sdk.dto.SessionToken; + +public interface SessionClient { + SessionToken createSession(long expiration); +} diff --git a/client/src/main/java/info/frostfs/sdk/services/ToolsClient.java b/client/src/main/java/info/frostfs/sdk/services/ToolsClient.java new file mode 100644 index 0000000..e8ce342 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/ToolsClient.java @@ -0,0 +1,8 @@ +package info.frostfs.sdk.services; + +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.dto.object.ObjectId; + +public interface ToolsClient { + ObjectId calculateObjectId(ObjectHeader header); +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/ContainerClientImpl.java b/client/src/main/java/info/frostfs/sdk/services/impl/ContainerClientImpl.java new file mode 100644 index 0000000..5c0e7af --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/ContainerClientImpl.java @@ -0,0 +1,122 @@ +package info.frostfs.sdk.services.impl; + +import frostfs.container.ContainerServiceGrpc; +import frostfs.container.Service; +import info.frostfs.sdk.dto.container.Container; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.jdo.ClientEnvironment; +import info.frostfs.sdk.mappers.OwnerIdMapper; +import info.frostfs.sdk.mappers.VersionMapper; +import info.frostfs.sdk.mappers.container.ContainerIdMapper; +import info.frostfs.sdk.mappers.container.ContainerMapper; +import info.frostfs.sdk.services.ContainerClient; +import info.frostfs.sdk.services.ContextAccessor; +import info.frostfs.sdk.tools.RequestConstructor; +import info.frostfs.sdk.tools.RequestSigner; +import info.frostfs.sdk.tools.Verifier; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; + +public class ContainerClientImpl extends ContextAccessor implements ContainerClient { + private final ContainerServiceGrpc.ContainerServiceBlockingStub serviceBlockingStub; + + public ContainerClientImpl(ClientEnvironment clientEnvironment) { + super(clientEnvironment); + this.serviceBlockingStub = ContainerServiceGrpc.newBlockingStub(clientEnvironment.getChannel()); + } + + @Override + public Container getContainer(ContainerId cid) { + if (isNull(cid)) { + throw new IllegalArgumentException("ContainerId is not present"); + } + + var body = Service.GetRequest.Body.newBuilder() + .setContainerId(ContainerIdMapper.toGrpcMessage(cid)) + .build(); + var request = Service.GetRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + RequestSigner.sign(request, getContext().getKey()); + + var response = serviceBlockingStub.get(request.build()); + + Verifier.checkResponse(response); + return ContainerMapper.toModel(response.getBody().getContainer()); + } + + @Override + public List listContainers() { + var body = Service.ListRequest.Body.newBuilder() + .setOwnerId(OwnerIdMapper.toGrpcMessage(getContext().getOwnerId())) + .build(); + var request = Service.ListRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + RequestSigner.sign(request, getContext().getKey()); + + var response = serviceBlockingStub.list(request.build()); + + Verifier.checkResponse(response); + + return response.getBody().getContainerIdsList().stream() + .map(cid -> new ContainerId(cid.getValue().toByteArray())) + .collect(Collectors.toList()); + } + + @Override + public ContainerId createContainer(Container container) { + if (isNull(container)) { + throw new IllegalArgumentException("Container is not present"); + } + + var grpcContainer = ContainerMapper.toGrpcMessage(container); + grpcContainer = grpcContainer.toBuilder() + .setOwnerId(OwnerIdMapper.toGrpcMessage(getContext().getOwnerId())) + .setVersion(VersionMapper.toGrpcMessage(getContext().getVersion())) + .build(); + + var body = Service.PutRequest.Body.newBuilder() + .setContainer(grpcContainer) + .setSignature(RequestSigner.signRFC6979(getContext().getKey(), grpcContainer)) + .build(); + var request = Service.PutRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + RequestSigner.sign(request, getContext().getKey()); + + var response = serviceBlockingStub.put(request.build()); + + Verifier.checkResponse(response); + return new ContainerId(response.getBody().getContainerId().getValue().toByteArray()); + } + + @Override + public void deleteContainer(ContainerId cid) { + if (isNull(cid)) { + throw new IllegalArgumentException("ContainerId is not present"); + } + + var grpcContainerId = ContainerIdMapper.toGrpcMessage(cid); + + var body = Service.DeleteRequest.Body.newBuilder() + .setContainerId(grpcContainerId) + .setSignature(RequestSigner.signRFC6979(getContext().getKey(), grpcContainerId.getValue())) + .build(); + var request = Service.DeleteRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + RequestSigner.sign(request, getContext().getKey()); + + var response = serviceBlockingStub.delete(request.build()); + + Verifier.checkResponse(response); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/NetmapClientImpl.java b/client/src/main/java/info/frostfs/sdk/services/impl/NetmapClientImpl.java new file mode 100644 index 0000000..b38624c --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/NetmapClientImpl.java @@ -0,0 +1,157 @@ +package info.frostfs.sdk.services.impl; + +import frostfs.netmap.NetmapServiceGrpc; +import frostfs.netmap.Service; +import frostfs.netmap.Types; +import info.frostfs.sdk.dto.netmap.NetmapSnapshot; +import info.frostfs.sdk.dto.netmap.NodeInfo; +import info.frostfs.sdk.jdo.ClientEnvironment; +import info.frostfs.sdk.jdo.NetworkSettings; +import info.frostfs.sdk.mappers.netmap.NetmapSnapshotMapper; +import info.frostfs.sdk.mappers.netmap.NodeInfoMapper; +import info.frostfs.sdk.services.ContextAccessor; +import info.frostfs.sdk.services.NetmapClient; +import info.frostfs.sdk.tools.RequestConstructor; +import info.frostfs.sdk.tools.Verifier; + +import java.nio.charset.StandardCharsets; + +import static info.frostfs.sdk.tools.RequestSigner.sign; +import static java.util.Objects.nonNull; + +public class NetmapClientImpl extends ContextAccessor implements NetmapClient { + private final NetmapServiceGrpc.NetmapServiceBlockingStub netmapServiceClient; + + public NetmapClientImpl(ClientEnvironment clientEnvironment) { + super(clientEnvironment); + this.netmapServiceClient = NetmapServiceGrpc.newBlockingStub(getContext().getChannel()); + } + + private static boolean getBoolValue(byte[] bytes) { + for (var byteValue : bytes) { + if (byteValue != 0) { + return true; + } + } + + return false; + } + + private static long getLongValue(byte[] bytes) { + long val = 0; + for (var i = bytes.length - 1; i >= 0; i--) { + val = (val << 8) + bytes[i]; + } + + return val; + } + + private static void setNetworksParam(Types.NetworkConfig.Parameter param, NetworkSettings settings) { + var key = new String(param.getKey().toByteArray(), StandardCharsets.UTF_8); + + var valueBytes = param.getValue().toByteArray(); + switch (key) { + case "AuditFee": + settings.setAuditFee(getLongValue(valueBytes)); + break; + case "BasicIncomeRate": + settings.setBasicIncomeRate(getLongValue(valueBytes)); + break; + case "ContainerFee": + settings.setContainerFee(getLongValue(valueBytes)); + break; + case "ContainerAliasFee": + settings.setContainerAliasFee(getLongValue(valueBytes)); + break; + case "EpochDuration": + settings.setEpochDuration(getLongValue(valueBytes)); + break; + case "InnerRingCandidateFee": + settings.setiRCandidateFee(getLongValue(valueBytes)); + break; + case "MaxECDataCount": + settings.setMaxECDataCount(getLongValue(valueBytes)); + break; + case "MaxECParityCount": + settings.setMaxECParityCount(getLongValue(valueBytes)); + break; + case "MaxObjectSize": + settings.setMaxObjectSize(getLongValue(valueBytes)); + break; + case "WithdrawFee": + settings.setWithdrawalFee(getLongValue(valueBytes)); + break; + case "HomomorphicHashingDisabled": + settings.setHomomorphicHashingDisabled(getBoolValue(valueBytes)); + break; + case "MaintenanceModeAllowed": + settings.setMaintenanceModeAllowed(getBoolValue(valueBytes)); + break; + default: + settings.getUnnamedSettings().put(key, valueBytes); + break; + } + } + + @Override + public NetworkSettings getNetworkSettings() { + if (nonNull(getContext().getNetworkSettings())) { + return getContext().getNetworkSettings(); + } + + var info = getNetworkInfo(); + + var settings = new NetworkSettings(); + + for (var param : info.getBody().getNetworkInfo().getNetworkConfig().getParametersList()) { + setNetworksParam(param, settings); + } + + getContext().setNetworkSettings(settings); + + return settings; + } + + @Override + public NodeInfo getLocalNodeInfo() { + var request = Service.LocalNodeInfoRequest.newBuilder() + .setBody(Service.LocalNodeInfoRequest.Body.newBuilder().build()); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + var response = netmapServiceClient.localNodeInfo(request.build()); + Verifier.checkResponse(response); + + return NodeInfoMapper.toModel(response.getBody()); + } + + public Service.NetworkInfoResponse getNetworkInfo() { + var request = Service.NetworkInfoRequest.newBuilder() + .setBody(Service.NetworkInfoRequest.Body.newBuilder().build()); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + var response = netmapServiceClient.networkInfo(request.build()); + + Verifier.checkResponse(response); + + return response; + } + + @Override + public NetmapSnapshot getNetmapSnapshot() { + var request = Service.NetmapSnapshotRequest.newBuilder() + .setBody(Service.NetmapSnapshotRequest.Body.newBuilder().build()); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + var response = netmapServiceClient.netmapSnapshot(request.build()); + + Verifier.checkResponse(response); + + return NetmapSnapshotMapper.toModel(response); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/ObjectClientImpl.java b/client/src/main/java/info/frostfs/sdk/services/impl/ObjectClientImpl.java new file mode 100644 index 0000000..69046d6 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/ObjectClientImpl.java @@ -0,0 +1,367 @@ +package info.frostfs.sdk.services.impl; + +import com.google.common.collect.Iterables; +import com.google.protobuf.ByteString; +import frostfs.object.ObjectServiceGrpc; +import frostfs.object.Service; +import frostfs.refs.Types; +import info.frostfs.sdk.constants.AppConst; +import info.frostfs.sdk.dto.Split; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.dto.object.*; +import info.frostfs.sdk.jdo.ClientEnvironment; +import info.frostfs.sdk.jdo.PutObjectParameters; +import info.frostfs.sdk.mappers.OwnerIdMapper; +import info.frostfs.sdk.mappers.VersionMapper; +import info.frostfs.sdk.mappers.container.ContainerIdMapper; +import info.frostfs.sdk.mappers.object.ObjectFilterMapper; +import info.frostfs.sdk.mappers.object.ObjectFrostFSMapper; +import info.frostfs.sdk.mappers.object.ObjectHeaderMapper; +import info.frostfs.sdk.mappers.object.ObjectIdMapper; +import info.frostfs.sdk.services.ContextAccessor; +import info.frostfs.sdk.services.ObjectClient; +import info.frostfs.sdk.services.impl.rwhelper.ObjectReader; +import info.frostfs.sdk.services.impl.rwhelper.ObjectWriter; +import info.frostfs.sdk.services.impl.rwhelper.SearchReader; +import info.frostfs.sdk.tools.RequestConstructor; +import info.frostfs.sdk.tools.Verifier; +import org.apache.commons.collections4.CollectionUtils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static info.frostfs.sdk.Helper.getSha256; +import static info.frostfs.sdk.tools.RequestConstructor.addObjectSessionToken; +import static info.frostfs.sdk.tools.RequestSigner.sign; +import static java.util.Objects.nonNull; + +public class ObjectClientImpl extends ContextAccessor implements ObjectClient { + private static final String ERROR_PAYLOAD = "PayloadLength must be specified"; + + private final ObjectServiceGrpc.ObjectServiceBlockingStub objectServiceBlockingClient; + private final ObjectServiceGrpc.ObjectServiceStub objectServiceClient; + private final ObjectToolsImpl objectToolsImpl; + + public ObjectClientImpl(ClientEnvironment clientEnvironment) { + super(clientEnvironment); + this.objectServiceBlockingClient = ObjectServiceGrpc.newBlockingStub(getContext().getChannel()); + this.objectServiceClient = ObjectServiceGrpc.newStub(getContext().getChannel()); + this.objectToolsImpl = new ObjectToolsImpl(clientEnvironment); + } + + @Override + public ObjectHeader getObjectHead(ContainerId cid, ObjectId oid) { + var address = Types.Address.newBuilder() + .setContainerId(ContainerIdMapper.toGrpcMessage(cid)) + .setObjectId(ObjectIdMapper.toGrpcMessage(oid)) + .build(); + var body = Service.HeadRequest.Body.newBuilder() + .setAddress(address) + .build(); + var request = Service.HeadRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + var response = objectServiceBlockingClient.head(request.build()); + Verifier.checkResponse(response); + + return ObjectHeaderMapper.toModel(response.getBody().getHeader().getHeader()); + } + + @Override + public ObjectFrostFS getObject(ContainerId cid, ObjectId oid) { + var sessionToken = getContext().getFrostFSClient().createSessionInternal(-1); + + var address = Types.Address.newBuilder() + .setContainerId(ContainerIdMapper.toGrpcMessage(cid)) + .setObjectId(ObjectIdMapper.toGrpcMessage(oid)) + .build(); + var body = Service.GetRequest.Body.newBuilder() + .setAddress(address) + .build(); + var request = Service.GetRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + addObjectSessionToken( + request, sessionToken, ContainerIdMapper.toGrpcMessage(cid), ObjectIdMapper.toGrpcMessage(oid), + frostfs.session.Types.ObjectSessionContext.Verb.GET, getContext().getKey() + ); + sign(request, getContext().getKey()); + + var obj = getObject(request.build()); + return ObjectFrostFSMapper.toModel(obj); + } + + + @Override + public void deleteObject(ContainerId cid, ObjectId oid) { + var address = Types.Address.newBuilder() + .setContainerId(ContainerIdMapper.toGrpcMessage(cid)) + .setObjectId(ObjectIdMapper.toGrpcMessage(oid)) + .build(); + var body = Service.DeleteRequest.Body.newBuilder() + .setAddress(address) + .build(); + var request = Service.DeleteRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + var response = objectServiceBlockingClient.delete(request.build()); + Verifier.checkResponse(response); + } + + @Override + public Iterable searchObjects(ContainerId cid, ObjectFilter... filters) { + var body = Service.SearchRequest.Body.newBuilder() + .setContainerId(ContainerIdMapper.toGrpcMessage(cid)) + .setVersion(1);// TODO: clarify this param + + for (ObjectFilter filter : filters) { + body.addFilters(ObjectFilterMapper.toGrpcMessage(filter)); + } + + var request = Service.SearchRequest.newBuilder() + .setBody(body.build()); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + var objectsIds = searchObjects(request.build()); + + return Iterables.transform(objectsIds, input -> new ObjectId(input.getValue().toByteArray())); + } + + @Override + public ObjectId putObject(PutObjectParameters parameters) { + parameters.validate(); + + return parameters.clientCut ? putClientCutObject(parameters) : putStreamObject(parameters); + } + + public ObjectId putSingleObject(ObjectFrostFS modelObject) { + var sessionToken = getContext().getFrostFSClient().createSessionInternal(-1); + + var grpcObject = objectToolsImpl.createObject(modelObject); + + var request = Service.PutSingleRequest.newBuilder() + .setBody(Service.PutSingleRequest.Body.newBuilder().setObject(grpcObject).build()); + + + RequestConstructor.addDefaultMetaHeader(request); + addObjectSessionToken( + request, sessionToken, grpcObject.getHeader().getContainerId(), grpcObject.getObjectId(), + frostfs.session.Types.ObjectSessionContext.Verb.PUT, getContext().getKey() + ); + sign(request, getContext().getKey()); + + var response = objectServiceBlockingClient.putSingle(request.build()); + + Verifier.checkResponse(response); + + return new ObjectId(grpcObject.getObjectId().getValue().toByteArray()); + } + + private frostfs.object.Types.Object getObject(Service.GetRequest request) { + var iterator = getObjectInit(request); + var obj = iterator.readHeader(); + var payload = new byte[Math.toIntExact(obj.getHeader().getPayloadLength())]; + var offset = 0; + var chunk = iterator.readChunk(); + + while (nonNull(chunk)) { + System.arraycopy(chunk, 0, payload, offset, chunk.length); + offset += chunk.length; + chunk = iterator.readChunk(); + } + + return obj.toBuilder().setPayload(ByteString.copyFrom(payload)).build(); + } + + private ObjectReader getObjectInit(Service.GetRequest initRequest) { + if (initRequest.getSerializedSize() == 0) { + throw new IllegalArgumentException(initRequest.getClass().getName()); + } + + return new ObjectReader(objectServiceBlockingClient.get(initRequest)); + } + + private ObjectId putStreamObject(PutObjectParameters parameters) { + var header = parameters.getHeader(); + + var sessionToken = getContext().getFrostFSClient().createSessionInternal(-1); + + var hdr = ObjectHeaderMapper.toGrpcMessage(header); + hdr = hdr.toBuilder() + .setOwnerId(OwnerIdMapper.toGrpcMessage(getContext().getOwnerId())) + .setVersion(VersionMapper.toGrpcMessage(getContext().getVersion())) + .build(); + + var oid = Types.ObjectID.newBuilder().setValue(getSha256(hdr)).build(); + + var initRequest = Service.PutRequest.newBuilder() + .setBody( + Service.PutRequest.Body.newBuilder() + .setInit( + Service.PutRequest.Body.Init.newBuilder().setHeader(hdr).build() + ).build() + ); + + RequestConstructor.addDefaultMetaHeader(initRequest); + addObjectSessionToken( + initRequest, sessionToken, hdr.getContainerId(), oid, + frostfs.session.Types.ObjectSessionContext.Verb.PUT, getContext().getKey() + ); + sign(initRequest, getContext().getKey()); + + + var writer = putObjectInit(initRequest.build()); + + var bufferSize = parameters.getBufferMaxSize() > 0 ? parameters.getBufferMaxSize() : AppConst.OBJECT_CHUNK_SIZE; + bufferSize = (int) Math.min(getStreamSize(parameters.getPayload()), bufferSize); + bufferSize = header.getPayloadLength() > 0 ? (int) Math.min(header.getPayloadLength(), bufferSize) : bufferSize; + + var buffer = new byte[bufferSize]; + while (true) { + var bytesCount = readNBytes(parameters.getPayload(), buffer, bufferSize); + if (bytesCount <= 0) { + break; + } + + var chunkRequest = Service.PutRequest.newBuilder(initRequest.build()) + .setBody( + Service.PutRequest.Body.newBuilder() + .setChunk(ByteString.copyFrom(Arrays.copyOfRange(buffer, 0, bytesCount))) + .build() + ) + .clearVerifyHeader(); + sign(chunkRequest, getContext().getKey()); + writer.write(chunkRequest.build()); + } + + var response = writer.complete(); + Verifier.checkResponse(response); + + return new ObjectId(response.getBody().getObjectId().getValue().toByteArray()); + } + + private ObjectId putClientCutObject(PutObjectParameters parameters) { + var header = parameters.getHeader(); + + var networkSettings = getContext().getFrostFSClient().getNetworkSettings(); + var payloadSize = getStreamSize(parameters.getPayload()); + var objectSize = (int) Math.min(payloadSize, networkSettings.getMaxObjectSize()); + var fullLength = header.getPayloadLength() == 0 ? payloadSize : header.getPayloadLength(); + if (fullLength == 0) { + throw new IllegalArgumentException(ERROR_PAYLOAD); + } + + var buffer = new byte[objectSize]; + var largeObject = new LargeObject(header.getContainerId()); + var split = new Split(); + + ObjectId objectId; + List sentObjectIds = new ArrayList<>(); + ObjectFrostFS currentObject; + + while (true) { + var bytesCount = readNBytes(parameters.getPayload(), buffer, objectSize); + if (CollectionUtils.isNotEmpty(sentObjectIds)) { + split.setPrevious(sentObjectIds.get(sentObjectIds.size() - 1)); + } + + largeObject.appendBlock(buffer, bytesCount); + + currentObject = new ObjectFrostFS( + header.getContainerId(), + bytesCount < objectSize ? Arrays.copyOfRange(buffer, 0, bytesCount) : buffer + ); + currentObject.setSplit(split); + + if (largeObject.getPayloadLength() == fullLength) { + break; + } + + objectId = putSingleObject(currentObject); + + sentObjectIds.add(objectId); + } + + if (CollectionUtils.isEmpty(sentObjectIds)) { + currentObject.addAttributes(parameters.getHeader().getAttributes()); + return putSingleObject(currentObject); + } + + largeObject.addAttributes(parameters.getHeader().getAttributes()); + largeObject.calculateHash(); + + currentObject.setParent(largeObject); + + objectId = putSingleObject(currentObject); + + sentObjectIds.add(objectId); + + var linkObject = new LinkObject(header.getContainerId(), split.getSplitId(), largeObject); + linkObject.addChildren(sentObjectIds); + + linkObject.getHeader().getAttributes().clear(); + + putSingleObject(linkObject); + + return objectToolsImpl.calculateObjectId(largeObject.getHeader()); + } + + private ObjectWriter putObjectInit(Service.PutRequest initRequest) { + if (initRequest.getSerializedSize() == 0) { + throw new IllegalArgumentException(initRequest.getClass().getName()); + } + + ObjectWriter writer = new ObjectWriter(objectServiceClient); + writer.write(initRequest); + return writer; + } + + private Iterable searchObjects(Service.SearchRequest request) { + var reader = getSearchReader(request); + var ids = reader.read(); + List result = new ArrayList<>(); + + while (nonNull(ids) && !ids.isEmpty()) { + result.addAll(ids); + ids = reader.read(); + } + + return result; + } + + private SearchReader getSearchReader(Service.SearchRequest initRequest) { + if (initRequest.getSerializedSize() == 0) { + throw new IllegalArgumentException(initRequest.getClass().getName()); + } + + return new SearchReader(objectServiceBlockingClient.search(initRequest)); + } + + private int readNBytes(FileInputStream fileInputStream, byte[] buffer, int size) { + try { + return fileInputStream.readNBytes(buffer, 0, size); + } catch (IOException exp) { + throw new IllegalArgumentException(exp.getMessage()); + } + } + + private long getStreamSize(FileInputStream fileInputStream) { + try { + return fileInputStream.getChannel().size(); + } catch (IOException exp) { + throw new IllegalArgumentException(exp.getMessage()); + } + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/ObjectToolsImpl.java b/client/src/main/java/info/frostfs/sdk/services/impl/ObjectToolsImpl.java new file mode 100644 index 0000000..b68a26e --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/ObjectToolsImpl.java @@ -0,0 +1,104 @@ +package info.frostfs.sdk.services.impl; + +import com.google.protobuf.ByteString; +import frostfs.object.Types; +import info.frostfs.sdk.dto.object.ObjectFrostFS; +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.dto.object.ObjectId; +import info.frostfs.sdk.jdo.ClientEnvironment; +import info.frostfs.sdk.mappers.OwnerIdMapper; +import info.frostfs.sdk.mappers.VersionMapper; +import info.frostfs.sdk.mappers.object.ObjectHeaderMapper; +import info.frostfs.sdk.mappers.object.ObjectIdMapper; +import info.frostfs.sdk.services.ContextAccessor; +import info.frostfs.sdk.services.ToolsClient; +import org.apache.commons.collections4.ListUtils; + +import static info.frostfs.sdk.Helper.getSha256; +import static info.frostfs.sdk.tools.RequestSigner.signData; +import static java.util.Objects.nonNull; + +public class ObjectToolsImpl extends ContextAccessor implements ToolsClient { + public ObjectToolsImpl(ClientEnvironment context) { + super(context); + } + + private static frostfs.refs.Types.Checksum sha256Checksum(byte[] data) { + return frostfs.refs.Types.Checksum.newBuilder() + .setType(frostfs.refs.Types.ChecksumType.SHA256) + .setSum(ByteString.copyFrom(getSha256(data))) + .build(); + } + + @Override + public ObjectId calculateObjectId(ObjectHeader header) { + var grpcHeader = createHeader(header, new byte[]{}); + + return ObjectIdMapper.toModel( + frostfs.refs.Types.ObjectID.newBuilder().setValue(getSha256(grpcHeader)).build() + ); + } + + public Types.Object createObject(ObjectFrostFS objectFrostFs) { + var grpcHeaderBuilder = ObjectHeaderMapper.toGrpcMessage(objectFrostFs.getHeader()).toBuilder() + .setOwnerId(OwnerIdMapper.toGrpcMessage(getContext().getOwnerId())) + .setVersion(VersionMapper.toGrpcMessage(getContext().getVersion())) + .setPayloadLength(objectFrostFs.getPayload().length) + .setPayloadHash(sha256Checksum(objectFrostFs.getPayload())); + + var split = objectFrostFs.getHeader().getSplit(); + if (nonNull(split)) { + var splitGrpc = Types.Header.Split.newBuilder() + .setSplitId(nonNull(split.getSplitId()) ? ByteString.copyFrom(split.getSplitId().toBinary()) : null); + + ListUtils.emptyIfNull(split.getChildren()).stream() + .map(ObjectIdMapper::toGrpcMessage) + .forEach(splitGrpc::addChildren); + + + if (nonNull(split.getParentHeader())) { + var grpcParentHeader = createHeader(split.getParentHeader(), new byte[]{}); + var parent = frostfs.refs.Types.ObjectID.newBuilder().setValue(getSha256(grpcParentHeader)).build(); + var parentSig = frostfs.refs.Types.Signature.newBuilder() + .setKey(ByteString.copyFrom(getContext().getKey().getPublicKeyByte())) + .setSign(ByteString.copyFrom(signData(getContext().getKey(), parent.toByteArray()))); + + splitGrpc.setParent(parent) + .setParentHeader(grpcParentHeader) + .setParentSignature(parentSig); + + split.setParent(ObjectIdMapper.toModel(parent)); + } + if (nonNull(split.getPrevious())) { + splitGrpc.setPrevious(ObjectIdMapper.toGrpcMessage(split.getPrevious())); + } + grpcHeaderBuilder.setSplit(splitGrpc); + } + + var grpcHeader = grpcHeaderBuilder.build(); + var objectId = frostfs.refs.Types.ObjectID.newBuilder().setValue(getSha256(grpcHeader)).build(); + var sig = frostfs.refs.Types.Signature.newBuilder() + .setKey(ByteString.copyFrom(getContext().getKey().getPublicKeyByte())) + .setSign(ByteString.copyFrom(signData(getContext().getKey(), objectId.toByteArray()))); + return Types.Object.newBuilder() + .setHeader(grpcHeader) + .setObjectId(objectId) + .setPayload(ByteString.copyFrom(objectFrostFs.getPayload())) + .setSignature(sig) + .build(); + } + + private Types.Header createHeader(ObjectHeader header, byte[] payload) { + var grpcHeader = ObjectHeaderMapper.toGrpcMessage(header).toBuilder() + .setOwnerId(OwnerIdMapper.toGrpcMessage(getContext().getOwnerId())) + .setVersion(VersionMapper.toGrpcMessage(getContext().getVersion())); + + if (header.getPayloadCheckSum() != null) { + grpcHeader.setPayloadHash(sha256Checksum(header.getPayloadCheckSum())); + } else if (payload != null) { + grpcHeader.setPayloadHash(sha256Checksum(payload)); + } + + return grpcHeader.build(); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/SessionClientImpl.java b/client/src/main/java/info/frostfs/sdk/services/impl/SessionClientImpl.java new file mode 100644 index 0000000..137c178 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/SessionClientImpl.java @@ -0,0 +1,65 @@ +package info.frostfs.sdk.services.impl; + +import frostfs.session.Service; +import frostfs.session.SessionServiceGrpc; +import frostfs.session.Types; +import info.frostfs.sdk.dto.SessionToken; +import info.frostfs.sdk.jdo.ClientEnvironment; +import info.frostfs.sdk.mappers.OwnerIdMapper; +import info.frostfs.sdk.mappers.SessionMapper; +import info.frostfs.sdk.services.ContextAccessor; +import info.frostfs.sdk.services.SessionClient; +import info.frostfs.sdk.tools.RequestConstructor; + +import static info.frostfs.sdk.tools.RequestSigner.sign; + +public class SessionClientImpl extends ContextAccessor implements SessionClient { + private final SessionServiceGrpc.SessionServiceBlockingStub serviceBlockingStub; + + public SessionClientImpl(ClientEnvironment clientEnvironment) { + super(clientEnvironment); + this.serviceBlockingStub = SessionServiceGrpc.newBlockingStub(getContext().getChannel()); + } + + @Override + public SessionToken createSession(long expiration) { + var sessionToken = createSessionInternal(expiration); + var token = SessionMapper.serialize(sessionToken); + return new SessionToken(new byte[]{}, token); + } + + public Types.SessionToken createSessionInternal(long expiration) { + var body = Service.CreateRequest.Body.newBuilder() + .setOwnerId(OwnerIdMapper.toGrpcMessage(getContext().getOwnerId())) + .setExpiration(expiration) + .build(); + var request = Service.CreateRequest.newBuilder() + .setBody(body); + + RequestConstructor.addDefaultMetaHeader(request); + sign(request, getContext().getKey()); + + return createSession(request.build()); + } + + private Types.SessionToken createSession(Service.CreateRequest request) { + var resp = serviceBlockingStub.create(request); + + var lifetime = Types.SessionToken.Body.TokenLifetime.newBuilder() + .setExp(request.getBody().getExpiration()) + .setIat(resp.getMetaHeader().getEpoch()) + .setNbf(resp.getMetaHeader().getEpoch()) + .build(); + + var body = Types.SessionToken.Body.newBuilder() + .setId(resp.getBody().getId()) + .setSessionKey(resp.getBody().getSessionKey()) + .setOwnerId(request.getBody().getOwnerId()) + .setLifetime(lifetime) + .build(); + + return Types.SessionToken.newBuilder() + .setBody(body) + .build(); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/ObjectReader.java b/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/ObjectReader.java new file mode 100644 index 0000000..d686921 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/ObjectReader.java @@ -0,0 +1,51 @@ +package info.frostfs.sdk.services.impl.rwhelper; + +import frostfs.object.Service; +import frostfs.object.Types; +import info.frostfs.sdk.tools.Verifier; + +import java.util.Iterator; + +public class ObjectReader { + public static final String ERROR_UNEXPECTED_STREAM = "unexpected end of stream"; + public static final String ERROR_UNEXPECTED_MESSAGE_TYPE = "unexpected message type"; + + public Iterator call; + + public ObjectReader(Iterator call) { + this.call = call; + } + + public Types.Object readHeader() { + if (!call.hasNext()) { + throw new IllegalArgumentException(ERROR_UNEXPECTED_STREAM); + } + + var response = call.next(); + Verifier.checkResponse(response); + + if (response.getBody().getObjectPartCase().getNumber() != Service.GetResponse.Body.INIT_FIELD_NUMBER) { + throw new IllegalArgumentException(ERROR_UNEXPECTED_MESSAGE_TYPE); + } + + return Types.Object.newBuilder() + .setObjectId(response.getBody().getInit().getObjectId()) + .setHeader(response.getBody().getInit().getHeader()) + .build(); + } + + public byte[] readChunk() { + if (!call.hasNext()) { + return null; + } + + var response = call.next(); + Verifier.checkResponse(response); + + if (response.getBody().getObjectPartCase().getNumber() != Service.GetResponse.Body.CHUNK_FIELD_NUMBER) { + throw new IllegalArgumentException(ERROR_UNEXPECTED_MESSAGE_TYPE); + } + + return response.getBody().getChunk().toByteArray(); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/ObjectWriter.java b/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/ObjectWriter.java new file mode 100644 index 0000000..7d6271b --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/ObjectWriter.java @@ -0,0 +1,59 @@ +package info.frostfs.sdk.services.impl.rwhelper; + +import frostfs.object.ObjectServiceGrpc; +import frostfs.object.Service; +import io.grpc.stub.StreamObserver; + +import static java.util.Objects.isNull; + +public class ObjectWriter { + private final StreamObserver requestObserver; + private final PutResponseCallback responseObserver; + + public ObjectWriter(ObjectServiceGrpc.ObjectServiceStub objectServiceStub) { + PutResponseCallback responseObserver = new PutResponseCallback(); + + this.responseObserver = responseObserver; + this.requestObserver = objectServiceStub.put(responseObserver); + } + + public void write(Service.PutRequest request) { + if (isNull(request)) { + throw new IllegalArgumentException(); + } + + requestObserver.onNext(request); + } + + public Service.PutResponse complete() { + requestObserver.onCompleted(); + + while (isNull(responseObserver.getResponse())) { + System.out.println("Waiting response"); + } + + return responseObserver.getResponse(); + } + + private static class PutResponseCallback implements StreamObserver { + private Service.PutResponse response; + + @Override + public void onNext(Service.PutResponse putResponse) { + this.response = putResponse; + } + + @Override + public void onError(Throwable throwable) { + throw new RuntimeException(throwable); + } + + @Override + public void onCompleted() { + } + + public Service.PutResponse getResponse() { + return response; + } + } +} diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/SearchReader.java b/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/SearchReader.java new file mode 100644 index 0000000..ec42c05 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/services/impl/rwhelper/SearchReader.java @@ -0,0 +1,26 @@ +package info.frostfs.sdk.services.impl.rwhelper; + +import frostfs.object.Service; +import info.frostfs.sdk.tools.Verifier; + +import java.util.Iterator; +import java.util.List; + +public class SearchReader { + public Iterator call; + + public SearchReader(Iterator call) { + this.call = call; + } + + public List read() { + if (!call.hasNext()) { + return null; + } + + var response = call.next(); + Verifier.checkResponse(response); + + return response.getBody().getIdListList(); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/GrpcClient.java b/client/src/main/java/info/frostfs/sdk/tools/GrpcClient.java new file mode 100644 index 0000000..96ef9b2 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/GrpcClient.java @@ -0,0 +1,29 @@ +package info.frostfs.sdk.tools; + +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.netty.NettyChannelBuilder; + +import java.net.URI; +import java.net.URISyntaxException; + +import static java.util.Objects.isNull; + +public class GrpcClient { + private static final String ERROR_INVALID_HOST_TEMPLATE = "Host %s has invalid format. Error: %s"; + + private GrpcClient() { + } + + public static Channel initGrpcChannel(String host, ChannelCredentials creds) { + try { + URI uri = new URI(host); + var channelBuilder = isNull(creds) ? NettyChannelBuilder.forAddress(uri.getHost(), uri.getPort()) + : NettyChannelBuilder.forAddress(uri.getHost(), uri.getPort(), creds); + + return channelBuilder.usePlaintext().build(); + } catch (URISyntaxException exp) { + throw new IllegalArgumentException(String.format(ERROR_INVALID_HOST_TEMPLATE, host, exp.getMessage())); + } + } +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/MessageHelper.java b/client/src/main/java/info/frostfs/sdk/tools/MessageHelper.java new file mode 100644 index 0000000..cb0005f --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/MessageHelper.java @@ -0,0 +1,18 @@ +package info.frostfs.sdk.tools; + +import com.google.protobuf.Message; +import com.google.protobuf.MessageOrBuilder; + +public class MessageHelper { + + private MessageHelper() { + } + + public static Message getField(MessageOrBuilder messageOrBuilder, String fieldName) { + return (Message) messageOrBuilder.getField(messageOrBuilder.getDescriptorForType().findFieldByName(fieldName)); + } + + public static void setField(Message.Builder builder, String fieldName, Object value) { + builder.setField(builder.getDescriptorForType().findFieldByName(fieldName), value); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/RequestConstructor.java b/client/src/main/java/info/frostfs/sdk/tools/RequestConstructor.java new file mode 100644 index 0000000..047d146 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/RequestConstructor.java @@ -0,0 +1,63 @@ +package info.frostfs.sdk.tools; + +import com.google.protobuf.Message; +import frostfs.session.Types; +import info.frostfs.sdk.dto.MetaHeader; +import info.frostfs.sdk.jdo.ECDsa; +import info.frostfs.sdk.mappers.MetaHeaderMapper; + +import static info.frostfs.sdk.constants.FieldConst.META_HEADER_FIELD_NAME; +import static info.frostfs.sdk.tools.MessageHelper.getField; +import static info.frostfs.sdk.tools.MessageHelper.setField; +import static info.frostfs.sdk.tools.RequestSigner.signMessagePart; +import static java.util.Objects.isNull; + +public class RequestConstructor { + + private RequestConstructor() { + } + + public static void addDefaultMetaHeader(Message.Builder request) { + addMetaHeader(request, null); + } + + public static void addMetaHeader(Message.Builder request, Types.RequestMetaHeader metaHeader) { + if (isNull(request)) { + return; + } + + if (isNull(metaHeader) || metaHeader.getSerializedSize() == 0) { + metaHeader = MetaHeaderMapper.toGrpcMessage(new MetaHeader()); + setField(request, META_HEADER_FIELD_NAME, metaHeader); + } + } + + public static void addObjectSessionToken(Message.Builder request, + Types.SessionToken sessionToken, + frostfs.refs.Types.ContainerID cid, + frostfs.refs.Types.ObjectID oid, + Types.ObjectSessionContext.Verb verb, + ECDsa key) { + if (isNull(request) || isNull(sessionToken)) { + return; + } + + var header = (Types.RequestMetaHeader) getField(request, META_HEADER_FIELD_NAME); + if (header.getSessionToken().getSerializedSize() > 0) { + return; + } + + var ctx = Types.ObjectSessionContext.newBuilder() + .setTarget(Types.ObjectSessionContext.Target.newBuilder().setContainer(cid).addObjects(oid).build()) + .setVerb(verb) + .build(); + + var body = sessionToken.getBody().toBuilder().setObject(ctx).build(); + sessionToken = sessionToken.toBuilder() + .setSignature(signMessagePart(key, body)) + .setBody(body) + .build(); + + setField(request, META_HEADER_FIELD_NAME, header.toBuilder().setSessionToken(sessionToken).build()); + } +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/RequestSigner.java b/client/src/main/java/info/frostfs/sdk/tools/RequestSigner.java new file mode 100644 index 0000000..ea7d828 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/RequestSigner.java @@ -0,0 +1,121 @@ +package info.frostfs.sdk.tools; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import frostfs.session.Types; +import info.frostfs.sdk.constants.CryptoConst; +import info.frostfs.sdk.jdo.ECDsa; +import org.apache.commons.codec.digest.DigestUtils; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.sec.SECObjectIdentifiers; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; + +import java.math.BigInteger; +import java.security.Signature; + +import static info.frostfs.sdk.constants.FieldConst.*; +import static org.bouncycastle.crypto.util.DigestFactory.createSHA256; +import static org.bouncycastle.util.BigIntegers.asUnsignedByteArray; + +public class RequestSigner { + public static final String ERROR_UNSUPPORTED_TYPE_TEMPLATE = "Unsupported message type: %s"; + public static final int RFC6979_SIGNATURE_SIZE = 64; + + private RequestSigner() { + } + + public static byte[] signData(ECDsa key, byte[] data) { + var hash = new byte[65]; + hash[0] = 0x04; + try { + Signature signature = Signature.getInstance(CryptoConst.SIGNATURE_ALGORITHM); + signature.initSign(key.getPrivateKey()); + signature.update(DigestUtils.sha512(data)); + byte[] sig = signature.sign(); + System.arraycopy(sig, 0, hash, 1, sig.length); + } catch (Exception exp) { + throw new RuntimeException(exp); + } + + return hash; + } + + public static byte[] signRFC6979(ECDsa key, byte[] data) { + var digest = createSHA256(); + var secp256R1 = SECNamedCurves.getByOID(SECObjectIdentifiers.secp256r1); + + var ecParameters = new ECDomainParameters(secp256R1.getCurve(), secp256R1.getG(), secp256R1.getN()); + var ecPrivateKey = new ECPrivateKeyParameters(new BigInteger(1, key.getPrivateKeyByte()), ecParameters); + var signer = new ECDSASigner(new HMacDSAKCalculator(digest)); + var hash = new byte[digest.getDigestSize()]; + + digest.update(data, 0, data.length); + digest.doFinal(hash, 0); + signer.init(true, ecPrivateKey); + + var rs = signer.generateSignature(hash); + var rBytes = asUnsignedByteArray(rs[0]); + var sBytes = asUnsignedByteArray(rs[1]); + + var signature = new byte[RFC6979_SIGNATURE_SIZE]; + var index = RFC6979_SIGNATURE_SIZE / 2 - rBytes.length; + System.arraycopy(rBytes, 0, signature, index, rBytes.length); + index = RFC6979_SIGNATURE_SIZE - sBytes.length; + System.arraycopy(sBytes, 0, signature, index, sBytes.length); + return signature; + } + + public static frostfs.refs.Types.SignatureRFC6979 signRFC6979(ECDsa key, Message message) { + return frostfs.refs.Types.SignatureRFC6979.newBuilder() + .setKey(ByteString.copyFrom(key.getPublicKeyByte())) + .setSign(ByteString.copyFrom(signRFC6979(key, message.toByteArray()))) + .build(); + } + + public static frostfs.refs.Types.SignatureRFC6979 signRFC6979(ECDsa key, ByteString data) { + return frostfs.refs.Types.SignatureRFC6979.newBuilder() + .setKey(ByteString.copyFrom(key.getPublicKeyByte())) + .setSign(ByteString.copyFrom(signRFC6979(key, data.toByteArray()))) + .build(); + } + + public static frostfs.refs.Types.Signature signMessagePart(ECDsa key, Message data) { + var data2Sign = data.getSerializedSize() == 0 ? new byte[]{} : data.toByteArray(); + return frostfs.refs.Types.Signature.newBuilder() + .setKey(ByteString.copyFrom(key.getPublicKeyByte())) + .setSign(ByteString.copyFrom(signData(key, data2Sign))) + .build(); + } + + public static void sign(Message.Builder request, ECDsa key) { + var meta = MessageHelper.getField(request, META_HEADER_FIELD_NAME); + var body = MessageHelper.getField(request, BODY_FIELD_NAME); + var verify = MessageHelper.getField(request, VERIFY_HEADER_FIELD_NAME); + var verifyOrigin = MessageHelper.getField(verify, ORIGIN_FIELD_NAME); + + Message.Builder verifyBuilder; + if (verify instanceof Types.RequestVerificationHeader) { + verifyBuilder = Types.RequestVerificationHeader.newBuilder(); + } else if (verify instanceof Types.ResponseVerificationHeader) { + verifyBuilder = Types.ResponseVerificationHeader.newBuilder(); + } else { + throw new IllegalArgumentException( + String.format(ERROR_UNSUPPORTED_TYPE_TEMPLATE, verify.getClass().getName()) + ); + } + + if (verifyOrigin.getSerializedSize() == 0) { + MessageHelper.setField(verifyBuilder, BODY_SIGNATURE_FIELD_NAME, signMessagePart(key, body)); + } else { + MessageHelper.setField(verifyBuilder, ORIGIN_FIELD_NAME, verifyOrigin); + } + + MessageHelper.setField(verifyBuilder, META_SIGNATURE_FIELD_NAME, signMessagePart(key, meta)); + MessageHelper.setField(verifyBuilder, ORIGIN_SIGNATURE_FIELD_NAME, signMessagePart(key, verifyOrigin)); + MessageHelper.setField(request, VERIFY_HEADER_FIELD_NAME, verifyBuilder.build()); + } + +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/Verifier.java b/client/src/main/java/info/frostfs/sdk/tools/Verifier.java new file mode 100644 index 0000000..48a2afc --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/Verifier.java @@ -0,0 +1,130 @@ +package info.frostfs.sdk.tools; + +import com.google.protobuf.Message; +import frostfs.session.Types; +import info.frostfs.sdk.constants.CryptoConst; +import info.frostfs.sdk.mappers.StatusMapper; +import org.apache.commons.codec.digest.DigestUtils; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.sec.SECObjectIdentifiers; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; + +import java.math.BigInteger; +import java.security.PublicKey; +import java.security.Signature; +import java.util.Arrays; + +import static info.frostfs.sdk.KeyExtension.getPublicKeyFromBytes; +import static info.frostfs.sdk.constants.FieldConst.*; +import static java.util.Objects.isNull; +import static org.bouncycastle.crypto.util.DigestFactory.createSHA256; +import static org.bouncycastle.util.BigIntegers.fromUnsignedByteArray; + +public class Verifier { + public static final String ERROR_WRONG_SIG_SIZE_TEMPLATE = "Wrong signature size. Expected length=%s, actual=%s"; + public static final String ERROR_INVALID_RESPONSE = "Invalid response"; + public static final int RFC6979_SIG_SIZE = 64; + + private Verifier() { + } + + public static boolean verifyRFC6979(frostfs.refs.Types.SignatureRFC6979 signature, Message data) { + return verifyRFC6979(signature.getKey().toByteArray(), data.toByteArray(), signature.getSign().toByteArray()); + } + + public static boolean verifyRFC6979(byte[] publicKey, byte[] data, byte[] sig) { + if (isNull(publicKey) || isNull(data) || isNull(sig)) { + return false; + } + + var rs = decodeSignature(sig); + var digest = createSHA256(); + var signer = new ECDSASigner(new HMacDSAKCalculator(digest)); + var secp256R1 = SECNamedCurves.getByOID(SECObjectIdentifiers.secp256r1); + var ecParameters = new ECDomainParameters(secp256R1.getCurve(), secp256R1.getG(), secp256R1.getN()); + var ecPublicKey = new ECPublicKeyParameters(secp256R1.getCurve().decodePoint(publicKey), ecParameters); + var hash = new byte[digest.getDigestSize()]; + digest.update(data, 0, data.length); + digest.doFinal(hash, 0); + signer.init(false, ecPublicKey); + return signer.verifySignature(hash, rs[0], rs[1]); + } + + private static BigInteger[] decodeSignature(byte[] sig) { + if (sig.length != RFC6979_SIG_SIZE) { + throw new IllegalArgumentException( + String.format(ERROR_WRONG_SIG_SIZE_TEMPLATE, RFC6979_SIG_SIZE, sig.length) + ); + } + + var rs = new BigInteger[2]; + + rs[0] = fromUnsignedByteArray(Arrays.copyOfRange(sig, 0, (RFC6979_SIG_SIZE / 2) - 1)); + rs[1] = fromUnsignedByteArray(Arrays.copyOfRange(sig, RFC6979_SIG_SIZE / 2, RFC6979_SIG_SIZE - 1)); + return rs; + } + + public static void checkResponse(Message response) { + if (!verify(response)) { + throw new IllegalArgumentException(ERROR_INVALID_RESPONSE); + } + + var metaHeader = (Types.ResponseMetaHeader) MessageHelper.getField(response, META_HEADER_FIELD_NAME); + var status = StatusMapper.toModel(metaHeader.getStatus()); + if (!status.isSuccess()) { + throw new IllegalArgumentException(status.toString()); + } + } + + public static boolean verify(Message response) { + var body = MessageHelper.getField(response, BODY_FIELD_NAME); + var metaHeader = (Types.ResponseMetaHeader) MessageHelper.getField(response, META_HEADER_FIELD_NAME); + var verifyHeader = (Types.ResponseVerificationHeader) MessageHelper.getField(response, VERIFY_HEADER_FIELD_NAME); + + return verifyMatryoshkaLevel(body, metaHeader, verifyHeader); + } + + public static boolean verifyMatryoshkaLevel(Message data, + frostfs.session.Types.ResponseMetaHeader meta, + frostfs.session.Types.ResponseVerificationHeader verification) { + if (!verifyMessagePart(verification.getMetaSignature(), meta)) { + return false; + } + + var origin = verification.getOrigin(); + if (!verifyMessagePart(verification.getOriginSignature(), origin)) { + return false; + } + + if (origin.getSerializedSize() == 0) { + return verifyMessagePart(verification.getBodySignature(), data); + } + return verification.getBodySignature().getSerializedSize() == 0 + && verifyMatryoshkaLevel(data, meta.getOrigin(), origin); + } + + public static boolean verifyMessagePart(frostfs.refs.Types.Signature sig, Message data) { + if (sig.getSerializedSize() == 0 || sig.getKey().isEmpty() || sig.getSign().isEmpty()) { + return false; + } + + var publicKey = getPublicKeyFromBytes(sig.getKey().toByteArray()); + + var data2Verify = data.getSerializedSize() == 0 ? new byte[]{} : data.toByteArray(); + return verifyData(publicKey, data2Verify, sig.getSign().toByteArray()); + } + + public static boolean verifyData(PublicKey publicKey, byte[] data, byte[] sig) { + try { + Signature signature = Signature.getInstance(CryptoConst.SIGNATURE_ALGORITHM); + signature.initVerify(publicKey); + signature.update(DigestUtils.sha512(data)); + return signature.verify(Arrays.copyOfRange(sig, 1, sig.length)); + } catch (Exception exp) { + throw new RuntimeException(exp); + } + } +} diff --git a/cryptography/pom.xml b/cryptography/pom.xml new file mode 100644 index 0000000..385a810 --- /dev/null +++ b/cryptography/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + info.frostfs.sdk + frostfs-sdk-java + 0.1.0 + + + cryptography + + + 11 + 11 + UTF-8 + 3.23.0 + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + org.apache.commons + commons-lang3 + 3.14.0 + + + + \ No newline at end of file diff --git a/cryptography/src/main/java/info/frostfs/sdk/ArrayHelper.java b/cryptography/src/main/java/info/frostfs/sdk/ArrayHelper.java new file mode 100644 index 0000000..915f202 --- /dev/null +++ b/cryptography/src/main/java/info/frostfs/sdk/ArrayHelper.java @@ -0,0 +1,14 @@ +package info.frostfs.sdk; + +public class ArrayHelper { + private ArrayHelper() { + } + + public static byte[] concat(byte[] startArray, byte[] endArray) { + byte[] result = new byte[startArray.length + endArray.length]; + + System.arraycopy(startArray, 0, result, 0, startArray.length); + System.arraycopy(endArray, 0, result, startArray.length, endArray.length); + return result; + } +} diff --git a/cryptography/src/main/java/info/frostfs/sdk/Base58.java b/cryptography/src/main/java/info/frostfs/sdk/Base58.java new file mode 100644 index 0000000..6321d58 --- /dev/null +++ b/cryptography/src/main/java/info/frostfs/sdk/Base58.java @@ -0,0 +1,135 @@ +package info.frostfs.sdk; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; + +import static info.frostfs.sdk.ArrayHelper.concat; +import static info.frostfs.sdk.Helper.getSha256; +import static java.util.Objects.isNull; + +public class Base58 { + public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + private static final char ENCODED_ZERO = ALPHABET[0]; + private static final int[] INDEXES = new int[128]; + + static { + Arrays.fill(INDEXES, -1); + for (int i = 0; i < ALPHABET.length; i++) { + INDEXES[ALPHABET[i]] = i; + } + } + + private Base58() { + } + + public static byte[] base58CheckDecode(String input) { + if (StringUtils.isEmpty(input)) { + throw new IllegalArgumentException("Input value is missing"); + } + + byte[] buffer = decode(input); + if (buffer.length < 4) { + throw new IllegalArgumentException(); + } + + byte[] decode = Arrays.copyOfRange(buffer, 0, buffer.length - 4); + byte[] checksum = getSha256(getSha256(decode)); + var bufferEnd = Arrays.copyOfRange(buffer, buffer.length - 4, buffer.length); + var checksumStart = Arrays.copyOfRange(checksum, 0, 4); + if (!Arrays.equals(bufferEnd, checksumStart)) { + throw new IllegalArgumentException(); + } + + return decode; + } + + public static String base58CheckEncode(byte[] data) { + if (isNull(data)) { + throw new IllegalArgumentException("Input value is missing"); + } + + byte[] checksum = getSha256(getSha256(data)); + var buffer = concat(data, Arrays.copyOfRange(checksum, 0, 4)); + var ret = encode(buffer); + return ret; + } + + public static String encode(byte[] input) { + if (input.length == 0) { + return ""; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input.length && input[zeros] == 0) { + ++zeros; + } + // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) + input = Arrays.copyOf(input, input.length); // since we modify it in-place + char[] encoded = new char[input.length * 2]; // upper bound + int outputStart = encoded.length; + for (int inputStart = zeros; inputStart < input.length; ) { + encoded[--outputStart] = ALPHABET[divmod(input, inputStart, 256, 58)]; + if (input[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. + while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) { + ++outputStart; + } + while (--zeros >= 0) { + encoded[--outputStart] = ENCODED_ZERO; + } + // Return encoded string (including encoded leading zeros). + return new String(encoded, outputStart, encoded.length - outputStart); + } + + public static byte[] decode(String input) { + if (input.isEmpty()) { + return new byte[0]; + } + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + byte[] input58 = new byte[input.length()]; + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + int digit = c < 128 ? INDEXES[c] : -1; + if (digit < 0) { + throw new IllegalArgumentException(String.format("Invalid character in Base58: 0x%04x", (int) c)); + } + input58[i] = (byte) digit; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + ++zeros; + } + // Convert base-58 digits to base-256 digits. + byte[] decoded = new byte[input.length()]; + int outputStart = decoded.length; + for (int inputStart = zeros; inputStart < input58.length; ) { + decoded[--outputStart] = divmod(input58, inputStart, 58, 256); + if (input58[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.length && decoded[outputStart] == 0) { + ++outputStart; + } + // Return decoded data (including original number of leading zeros). + return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); + } + + private static byte divmod(byte[] number, int firstDigit, int base, int divisor) { + // this is just long division which accounts for the base of the input digits + int remainder = 0; + for (int i = firstDigit; i < number.length; i++) { + int digit = (int) number[i] & 0xFF; + int temp = remainder * base + digit; + number[i] = (byte) (temp / divisor); + remainder = temp % divisor; + } + return (byte) remainder; + } +} diff --git a/cryptography/src/main/java/info/frostfs/sdk/Helper.java b/cryptography/src/main/java/info/frostfs/sdk/Helper.java new file mode 100644 index 0000000..58b8c1e --- /dev/null +++ b/cryptography/src/main/java/info/frostfs/sdk/Helper.java @@ -0,0 +1,60 @@ +package info.frostfs.sdk; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import org.bouncycastle.crypto.digests.RIPEMD160Digest; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static java.util.Objects.isNull; + +public class Helper { + private Helper() { + } + + public static byte[] getRIPEMD160(byte[] value) { + if (isNull(value)) { + throw new IllegalArgumentException("Input value is missing"); + } + + var hash = new byte[20]; + var digest = new RIPEMD160Digest(); + digest.update(value, 0, value.length); + digest.doFinal(hash, 0); + return hash; + } + + public static MessageDigest getSha256Instance() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static byte[] getSha256(byte[] value) { + if (isNull(value)) { + throw new IllegalArgumentException("Input value is missing"); + } + + return getSha256Instance().digest(value); + } + + public static ByteString getSha256(Message value) { + if (isNull(value)) { + throw new IllegalArgumentException("Input value is missing"); + } + + return ByteString.copyFrom(getSha256(value.toByteArray())); + } + + public static String getHexString(byte[] value) { + if (isNull(value)) { + throw new IllegalArgumentException("Input value is missing"); + } + + return String.format("%0" + (value.length << 1) + "x", new BigInteger(1, value)); + } +} diff --git a/cryptography/src/main/java/info/frostfs/sdk/KeyExtension.java b/cryptography/src/main/java/info/frostfs/sdk/KeyExtension.java new file mode 100644 index 0000000..40affe5 --- /dev/null +++ b/cryptography/src/main/java/info/frostfs/sdk/KeyExtension.java @@ -0,0 +1,192 @@ +package info.frostfs.sdk; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.sec.SECObjectIdentifiers; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +import static info.frostfs.sdk.Helper.getRIPEMD160; +import static info.frostfs.sdk.Helper.getSha256; +import static java.util.Objects.isNull; +import static org.bouncycastle.util.BigIntegers.fromUnsignedByteArray; + +public class KeyExtension { + public static final byte NEO_ADDRESS_VERSION = 0x35; + private static final int DECODE_ADDRESS_LENGTH = 21; + private static final int COMPRESSED_PUBLIC_KEY_LENGTH = 33; + private static final int UNCOMPRESSED_PUBLIC_KEY_LENGTH = 65; + private static final int CHECK_SIG_DESCRIPTOR = ByteBuffer + .wrap(getSha256("System.Crypto.CheckSig".getBytes(StandardCharsets.US_ASCII))) + .order(ByteOrder.LITTLE_ENDIAN).getInt(); + + private KeyExtension() { + } + + public static byte[] compress(byte[] publicKey) { + checkInputValue(publicKey); + if (publicKey.length != UNCOMPRESSED_PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + String.format("Compress argument isn't uncompressed public key. Expected length=%s, actual=%s", + UNCOMPRESSED_PUBLIC_KEY_LENGTH, publicKey.length) + ); + } + + var secp256R1 = SECNamedCurves.getByOID(SECObjectIdentifiers.secp256r1); + var point = secp256R1.getCurve().decodePoint(publicKey); + return point.getEncoded(true); + } + + public static byte[] getPrivateKeyFromWIF(String wif) { + if (StringUtils.isEmpty(wif)) { + throw new IllegalArgumentException("Input value is missing"); + } + + var data = Base58.base58CheckDecode(wif); + return Arrays.copyOfRange(data, 1, data.length - 1); + } + + public static byte[] loadPublicKey(byte[] privateKey) { + checkInputValue(privateKey); + + X9ECParameters params = SECNamedCurves.getByOID(SECObjectIdentifiers.secp256r1); + ECDomainParameters domain = new ECDomainParameters( + params.getCurve(), params.getG(), params.getN(), params.getH() + ); + ECPoint q = domain.getG().multiply(new BigInteger(1, privateKey)); + ECPublicKeyParameters publicParams = new ECPublicKeyParameters(q, domain); + return publicParams.getQ().getEncoded(true); + } + + public static PrivateKey loadPrivateKey(byte[] privateKey) { + checkInputValue(privateKey); + + X9ECParameters params = SECNamedCurves.getByOID(SECObjectIdentifiers.secp256r1); + ECDomainParameters domain = new ECDomainParameters( + params.getCurve(), params.getG(), params.getN(), params.getH() + ); + ECPrivateKeyParameters ecParams = new ECPrivateKeyParameters(fromUnsignedByteArray(privateKey), domain); + + ECParameterSpec ecParameterSpec = new ECNamedCurveSpec( + "secp256r1", params.getCurve(), params.getG(), params.getN(), params.getH() + ); + ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(ecParams.getD(), ecParameterSpec); + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePrivate(privateKeySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); + } + } + + public static PublicKey getPublicKeyFromBytes(byte[] publicKey) { + checkInputValue(publicKey); + + if (publicKey.length != COMPRESSED_PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + String.format("Decompress argument isn't compressed public key. Expected length=%s, actual=%s", + COMPRESSED_PUBLIC_KEY_LENGTH, publicKey.length) + ); + } + + X9ECParameters secp256R1 = SECNamedCurves.getByOID(SECObjectIdentifiers.secp256r1); + ECDomainParameters domain = new ECDomainParameters( + secp256R1.getCurve(), secp256R1.getG(), secp256R1.getN(), secp256R1.getH() + ); + var ecPoint = secp256R1.getCurve().decodePoint(publicKey); + var publicParams = new ECPublicKeyParameters(ecPoint, domain); + java.security.spec.ECPoint point = new java.security.spec.ECPoint( + publicParams.getQ().getRawXCoord().toBigInteger(), publicParams.getQ().getRawYCoord().toBigInteger() + ); + ECParameterSpec ecParameterSpec = new ECNamedCurveSpec( + "secp256r1", secp256R1.getCurve(), secp256R1.getG(), secp256R1.getN(), + secp256R1.getH(), secp256R1.getSeed() + ); + ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(point, ecParameterSpec); + + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(publicKeySpec); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static byte[] getScriptHash(byte[] publicKey) { + checkInputValue(publicKey); + + var script = createSignatureRedeemScript(publicKey); + return getRIPEMD160(getSha256(script)); + } + + public static String publicKeyToAddress(byte[] publicKey) { + checkInputValue(publicKey); + if (publicKey.length != COMPRESSED_PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + String.format("PublicKey isn't encoded compressed public key. Expected length=%s, actual=%s", + COMPRESSED_PUBLIC_KEY_LENGTH, publicKey.length) + ); + } + + return toAddress(getScriptHash(publicKey), NEO_ADDRESS_VERSION); + } + + private static String toAddress(byte[] scriptHash, byte version) { + checkInputValue(scriptHash); + byte[] data = new byte[DECODE_ADDRESS_LENGTH]; + data[0] = version; + System.arraycopy(scriptHash, 0, data, 1, scriptHash.length); + return Base58.base58CheckEncode(data); + } + + private static byte[] getBytes(int value) { + byte[] buffer = new byte[4]; + + for (int i = 0; i < buffer.length; i++) { + buffer[i] = (byte) (value >> i * 8); + } + + return buffer; + } + + private static byte[] createSignatureRedeemScript(byte[] publicKey) { + checkInputValue(publicKey); + if (publicKey.length != COMPRESSED_PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + String.format("PublicKey isn't encoded compressed public key. Expected length=%s, actual=%s", + COMPRESSED_PUBLIC_KEY_LENGTH, publicKey.length) + ); + } + + var script = new byte[]{0x0c, COMPRESSED_PUBLIC_KEY_LENGTH}; //PUSHDATA1 33 + + script = ArrayHelper.concat(script, publicKey); + script = ArrayHelper.concat(script, new byte[]{0x41}); //SYSCALL + script = ArrayHelper.concat(script, getBytes(CHECK_SIG_DESCRIPTOR)); //Neo_Crypto_CheckSig + return script; + } + + private static void checkInputValue(byte[] data) { + if (isNull(data)) { + throw new IllegalArgumentException("Input value is missing"); + } + } +} diff --git a/models/pom.xml b/models/pom.xml new file mode 100644 index 0000000..edc1c26 --- /dev/null +++ b/models/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + info.frostfs.sdk + frostfs-sdk-java + 0.1.0 + + + models + + + 11 + 11 + UTF-8 + + + + + info.frostfs.sdk + cryptography + 0.1.0 + + + info.frostfs.sdk + protos + 0.1.0 + + + org.apache.commons + commons-lang3 + 3.14.0 + + + org.apache.commons + commons-collections4 + 4.4 + + + + \ No newline at end of file diff --git a/models/src/main/java/info/frostfs/sdk/UUIDExtension.java b/models/src/main/java/info/frostfs/sdk/UUIDExtension.java new file mode 100644 index 0000000..730a1a5 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/UUIDExtension.java @@ -0,0 +1,35 @@ +package info.frostfs.sdk; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import static java.util.Objects.isNull; + +public class UUIDExtension { + private static final int UUID_BYTE_ARRAY_LENGTH = 16; + + private UUIDExtension() { + } + + public static UUID asUuid(byte[] bytes) { + if (isNull(bytes) || bytes.length != UUID_BYTE_ARRAY_LENGTH) { + throw new IllegalArgumentException("Uuid byte array length must be " + UUID_BYTE_ARRAY_LENGTH); + } + + ByteBuffer bb = ByteBuffer.wrap(bytes); + long firstLong = bb.getLong(); + long secondLong = bb.getLong(); + return new UUID(firstLong, secondLong); + } + + public static byte[] asBytes(UUID uuid) { + if (isNull(uuid)) { + throw new IllegalArgumentException("Uuid is not present"); + } + + ByteBuffer bb = ByteBuffer.allocate(16); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/constants/AppConst.java b/models/src/main/java/info/frostfs/sdk/constants/AppConst.java new file mode 100644 index 0000000..a823466 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/constants/AppConst.java @@ -0,0 +1,11 @@ +package info.frostfs.sdk.constants; + +public class AppConst { + public static final int DEFAULT_MAJOR_VERSION = 2; + public static final int DEFAULT_MINOR_VERSION = 13; + public static final int OBJECT_CHUNK_SIZE = 3 * (1 << 20); + public static final int SHA256_HASH_LENGTH = 32; + + private AppConst() { + } +} diff --git a/models/src/main/java/info/frostfs/sdk/constants/FieldConst.java b/models/src/main/java/info/frostfs/sdk/constants/FieldConst.java new file mode 100644 index 0000000..636df86 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/constants/FieldConst.java @@ -0,0 +1,15 @@ +package info.frostfs.sdk.constants; + +public class FieldConst { + public static final String META_HEADER_FIELD_NAME = "meta_header"; + public static final String META_SIGNATURE_FIELD_NAME = "meta_signature"; + public static final String BODY_FIELD_NAME = "body"; + public static final String BODY_SIGNATURE_FIELD_NAME = "body_signature"; + public static final String ORIGIN_FIELD_NAME = "origin"; + public static final String ORIGIN_SIGNATURE_FIELD_NAME = "origin_signature"; + public static final String VERIFY_HEADER_FIELD_NAME = "verify_header"; + public static final String EMPTY_STRING = ""; + + private FieldConst() { + } +} diff --git a/models/src/main/java/info/frostfs/sdk/constants/XHeaderConst.java b/models/src/main/java/info/frostfs/sdk/constants/XHeaderConst.java new file mode 100644 index 0000000..e8b2c72 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/constants/XHeaderConst.java @@ -0,0 +1,10 @@ +package info.frostfs.sdk.constants; + +public class XHeaderConst { + public static final String RESERVED_XHEADER_PREFIX = "__SYSTEM__"; + public static final String XHEADER_NETMAP_EPOCH = RESERVED_XHEADER_PREFIX + "NETMAP_EPOCH"; + public static final String XHEADER_NETMAP_LOOKUP_DEPTH = RESERVED_XHEADER_PREFIX + "NETMAP_LOOKUP_DEPTH"; + + private XHeaderConst() { + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/MetaHeader.java b/models/src/main/java/info/frostfs/sdk/dto/MetaHeader.java new file mode 100644 index 0000000..9d6735e --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/MetaHeader.java @@ -0,0 +1,63 @@ +package info.frostfs.sdk.dto; + +import static info.frostfs.sdk.constants.AppConst.DEFAULT_MAJOR_VERSION; +import static info.frostfs.sdk.constants.AppConst.DEFAULT_MINOR_VERSION; +import static java.util.Objects.isNull; + +public class MetaHeader { + private Version version; + private int epoch; + private int ttl; + + public MetaHeader(Version version, int epoch, int ttl) { + if (isNull(version) || epoch < 0 || ttl <= 0) { + throw new IllegalArgumentException("One of the input attributes is invalid or missing"); + } + + this.version = version; + this.epoch = epoch; + this.ttl = ttl; + } + + public MetaHeader() { + this.version = new Version(DEFAULT_MAJOR_VERSION, DEFAULT_MINOR_VERSION); + this.epoch = 0; + this.ttl = 2; + } + + public Version getVersion() { + return version; + } + + public void setVersion(Version version) { + if (isNull(version)) { + throw new IllegalArgumentException("Version is missing."); + } + + this.version = version; + } + + public int getEpoch() { + return epoch; + } + + public void setEpoch(int epoch) { + if (epoch < 0) { + throw new IllegalArgumentException("The epoch must be greater than or equal to zero."); + } + + this.epoch = epoch; + } + + public int getTtl() { + return ttl; + } + + public void setTtl(int ttl) { + if (ttl <= 0) { + throw new IllegalArgumentException("The ttl must be greater than zero."); + } + + this.ttl = ttl; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/OwnerId.java b/models/src/main/java/info/frostfs/sdk/dto/OwnerId.java new file mode 100644 index 0000000..1603e21 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/OwnerId.java @@ -0,0 +1,30 @@ +package info.frostfs.sdk.dto; + +import info.frostfs.sdk.Base58; + +import static info.frostfs.sdk.KeyExtension.publicKeyToAddress; +import static java.util.Objects.isNull; + +public class OwnerId { + private final String value; + + public OwnerId(String value) { + this.value = value; + } + + public OwnerId(byte[] publicKey) { + if (isNull(publicKey) || publicKey.length == 0) { + throw new IllegalArgumentException("PublicKey is invalid"); + } + + this.value = publicKeyToAddress(publicKey); + } + + public String getValue() { + return value; + } + + public byte[] toHash() { + return Base58.decode(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/SessionToken.java b/models/src/main/java/info/frostfs/sdk/dto/SessionToken.java new file mode 100644 index 0000000..2f4055b --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/SessionToken.java @@ -0,0 +1,19 @@ +package info.frostfs.sdk.dto; + +public class SessionToken { + private final byte[] id; + private final byte[] sessionKey; + + public SessionToken(byte[] id, byte[] sessionKey) { + this.id = id; + this.sessionKey = sessionKey; + } + + public byte[] getId() { + return id; + } + + public byte[] getSessionKey() { + return sessionKey; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/Signature.java b/models/src/main/java/info/frostfs/sdk/dto/Signature.java new file mode 100644 index 0000000..5b2b85d --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/Signature.java @@ -0,0 +1,33 @@ +package info.frostfs.sdk.dto; + +import info.frostfs.sdk.enums.SignatureScheme; + +public class Signature { + private byte[] key; + private byte[] sign; + private SignatureScheme scheme; + + public byte[] getKey() { + return key; + } + + public void setKey(byte[] key) { + this.key = key; + } + + public byte[] getSign() { + return sign; + } + + public void setSign(byte[] sign) { + this.sign = sign; + } + + public SignatureScheme getScheme() { + return scheme; + } + + public void setScheme(SignatureScheme scheme) { + this.scheme = scheme; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/Split.java b/models/src/main/java/info/frostfs/sdk/dto/Split.java new file mode 100644 index 0000000..642ed58 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/Split.java @@ -0,0 +1,71 @@ +package info.frostfs.sdk.dto; + +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.dto.object.ObjectId; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.isNull; + +public class Split { + private final List children; + private final SplitId splitId; + private ObjectId parent; + private ObjectId previous; + private Signature parentSignature; + private ObjectHeader parentHeader; + + public Split() { + this(new SplitId()); + } + + public Split(SplitId splitId) { + if (isNull(splitId)) { + throw new IllegalArgumentException("SplitId is not present"); + } + + this.splitId = splitId; + this.children = new ArrayList<>(); + } + + public SplitId getSplitId() { + return splitId; + } + + public ObjectId getParent() { + return parent; + } + + public void setParent(ObjectId parent) { + this.parent = parent; + } + + public ObjectId getPrevious() { + return previous; + } + + public void setPrevious(ObjectId previous) { + this.previous = previous; + } + + public Signature getParentSignature() { + return parentSignature; + } + + public void setParentSignature(Signature parentSignature) { + this.parentSignature = parentSignature; + } + + public ObjectHeader getParentHeader() { + return parentHeader; + } + + public void setParentHeader(ObjectHeader parentHeader) { + this.parentHeader = parentHeader; + } + + public List getChildren() { + return children; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/SplitId.java b/models/src/main/java/info/frostfs/sdk/dto/SplitId.java new file mode 100644 index 0000000..3ced690 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/SplitId.java @@ -0,0 +1,36 @@ +package info.frostfs.sdk.dto; + +import java.util.UUID; + +import static info.frostfs.sdk.UUIDExtension.asBytes; +import static info.frostfs.sdk.UUIDExtension.asUuid; +import static java.util.Objects.isNull; + +public class SplitId { + private final UUID id; + + public SplitId() { + this.id = UUID.randomUUID(); + } + + public SplitId(UUID uuid) { + this.id = uuid; + } + + public SplitId(byte[] binary) { + this.id = asUuid(binary); + } + + public SplitId(String str) { + this.id = UUID.fromString(str); + } + + @Override + public String toString() { + return id.toString(); + } + + public byte[] toBinary() { + return isNull(id) ? null : asBytes(id); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/Status.java b/models/src/main/java/info/frostfs/sdk/dto/Status.java new file mode 100644 index 0000000..e1075b5 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/Status.java @@ -0,0 +1,46 @@ +package info.frostfs.sdk.dto; + +import info.frostfs.sdk.enums.StatusCode; + +import static info.frostfs.sdk.constants.FieldConst.EMPTY_STRING; +import static java.util.Objects.isNull; + +public class Status { + private StatusCode code; + private String message; + + public Status(StatusCode code, String message) { + this.code = code; + this.message = isNull(message) ? EMPTY_STRING : message; + } + + public Status(StatusCode code) { + this.code = code; + this.message = EMPTY_STRING; + } + + public StatusCode getCode() { + return code; + } + + public void setCode(StatusCode code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return String.format("Response status: %s. Message: %s.", code, message); + } + + public boolean isSuccess() { + return StatusCode.SUCCESS.equals(code); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/Version.java b/models/src/main/java/info/frostfs/sdk/dto/Version.java new file mode 100644 index 0000000..0222922 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/Version.java @@ -0,0 +1,41 @@ +package info.frostfs.sdk.dto; + +import static info.frostfs.sdk.constants.AppConst.DEFAULT_MAJOR_VERSION; +import static info.frostfs.sdk.constants.AppConst.DEFAULT_MINOR_VERSION; +import static java.util.Objects.isNull; + +public class Version { + private final int major; + private final int minor; + + public Version(int major, int minor) { + this.major = major; + this.minor = minor; + } + + public Version() { + this.major = DEFAULT_MAJOR_VERSION; + this.minor = DEFAULT_MINOR_VERSION; + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + @Override + public String toString() { + return "v" + major + "." + minor; + } + + public boolean isSupported(Version version) { + if (isNull(version)) { + return false; + } + + return major == version.getMajor(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/container/Container.java b/models/src/main/java/info/frostfs/sdk/dto/container/Container.java new file mode 100644 index 0000000..e07c28a --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/container/Container.java @@ -0,0 +1,52 @@ +package info.frostfs.sdk.dto.container; + +import info.frostfs.sdk.dto.Version; +import info.frostfs.sdk.dto.netmap.PlacementPolicy; +import info.frostfs.sdk.enums.BasicAcl; + +import java.util.UUID; + +public class Container { + private UUID nonce; + private BasicAcl basicAcl; + private PlacementPolicy placementPolicy; + private Version version; + + public Container(BasicAcl basicAcl, PlacementPolicy placementPolicy) { + this.nonce = UUID.randomUUID(); + this.basicAcl = basicAcl; + this.placementPolicy = placementPolicy; + } + + public UUID getNonce() { + return nonce; + } + + public void setNonce(UUID nonce) { + this.nonce = nonce; + } + + public BasicAcl getBasicAcl() { + return basicAcl; + } + + public void setBasicAcl(BasicAcl basicAcl) { + this.basicAcl = basicAcl; + } + + public PlacementPolicy getPlacementPolicy() { + return placementPolicy; + } + + public void setPlacementPolicy(PlacementPolicy placementPolicy) { + this.placementPolicy = placementPolicy; + } + + public Version getVersion() { + return version; + } + + public void setVersion(Version version) { + this.version = version; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/container/ContainerId.java b/models/src/main/java/info/frostfs/sdk/dto/container/ContainerId.java new file mode 100644 index 0000000..1cd39b2 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/container/ContainerId.java @@ -0,0 +1,40 @@ +package info.frostfs.sdk.dto.container; + +import info.frostfs.sdk.Base58; +import info.frostfs.sdk.constants.AppConst; +import org.apache.commons.lang3.StringUtils; + +import static java.util.Objects.isNull; + +public class ContainerId { + private final String value; + + public ContainerId(String value) { + if (StringUtils.isEmpty(value)) { + throw new IllegalArgumentException("ContainerId value is missing"); + } + + this.value = value; + } + + public ContainerId(byte[] hash) { + if (isNull(hash) || hash.length != AppConst.SHA256_HASH_LENGTH) { + throw new IllegalArgumentException("ContainerId must be a sha256 hash."); + } + + this.value = Base58.encode(hash); + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + public byte[] toHash() { + return Base58.decode(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/netmap/NetmapSnapshot.java b/models/src/main/java/info/frostfs/sdk/dto/netmap/NetmapSnapshot.java new file mode 100644 index 0000000..9c2ef2c --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/netmap/NetmapSnapshot.java @@ -0,0 +1,21 @@ +package info.frostfs.sdk.dto.netmap; + +import java.util.List; + +public class NetmapSnapshot { + private final Long epoch; + private final List nodeInfoCollection; + + public NetmapSnapshot(Long epoch, List nodeInfoCollection) { + this.epoch = epoch; + this.nodeInfoCollection = nodeInfoCollection; + } + + public Long getEpoch() { + return epoch; + } + + public List getNodeInfoCollection() { + return nodeInfoCollection; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/netmap/NodeInfo.java b/models/src/main/java/info/frostfs/sdk/dto/netmap/NodeInfo.java new file mode 100644 index 0000000..2be1b1b --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/netmap/NodeInfo.java @@ -0,0 +1,44 @@ +package info.frostfs.sdk.dto.netmap; + +import info.frostfs.sdk.dto.Version; +import info.frostfs.sdk.enums.NodeState; + +import java.util.List; +import java.util.Map; + +public class NodeInfo { + private final NodeState state; + private final Version version; + private final List addresses; + private final Map attributes; + private final byte[] publicKey; + + public NodeInfo(NodeState state, Version version, List addresses, + Map attributes, byte[] publicKey) { + this.state = state; + this.version = version; + this.addresses = addresses; + this.attributes = attributes; + this.publicKey = publicKey; + } + + public NodeState getState() { + return state; + } + + public Version getVersion() { + return version; + } + + public byte[] getPublicKey() { + return publicKey; + } + + public Map getAttributes() { + return attributes; + } + + public List getAddresses() { + return addresses; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/netmap/PlacementPolicy.java b/models/src/main/java/info/frostfs/sdk/dto/netmap/PlacementPolicy.java new file mode 100644 index 0000000..74a1c72 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/netmap/PlacementPolicy.java @@ -0,0 +1,19 @@ +package info.frostfs.sdk.dto.netmap; + +public class PlacementPolicy { + private final Replica[] replicas; + private final boolean unique; + + public PlacementPolicy(boolean unique, Replica[] replicas) { + this.replicas = replicas; + this.unique = unique; + } + + public Replica[] getReplicas() { + return replicas; + } + + public boolean isUnique() { + return unique; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/netmap/Replica.java b/models/src/main/java/info/frostfs/sdk/dto/netmap/Replica.java new file mode 100644 index 0000000..74e3886 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/netmap/Replica.java @@ -0,0 +1,36 @@ +package info.frostfs.sdk.dto.netmap; + +import org.apache.commons.lang3.StringUtils; + +import static info.frostfs.sdk.constants.FieldConst.EMPTY_STRING; + +public class Replica { + private final int count; + private final String selector; + + public Replica(int count, String selector) { + if (count <= 0) { + throw new IllegalArgumentException("Replica count must be positive"); + } + + this.count = count; + this.selector = StringUtils.isEmpty(selector) ? EMPTY_STRING : selector; + } + + public Replica(int count) { + if (count <= 0) { + throw new IllegalArgumentException("Replica count must be positive"); + } + + this.count = count; + this.selector = EMPTY_STRING; + } + + public int getCount() { + return count; + } + + public String getSelector() { + return selector; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/LargeObject.java b/models/src/main/java/info/frostfs/sdk/dto/object/LargeObject.java new file mode 100644 index 0000000..915b745 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/LargeObject.java @@ -0,0 +1,34 @@ +package info.frostfs.sdk.dto.object; + +import info.frostfs.sdk.dto.container.ContainerId; + +import java.security.MessageDigest; + +import static info.frostfs.sdk.Helper.getSha256Instance; +import static java.util.Objects.isNull; + +public class LargeObject extends ObjectFrostFS { + private final MessageDigest payloadHash; + + public LargeObject(ContainerId cid) { + super(cid, new byte[]{}); + this.payloadHash = getSha256Instance(); + } + + public void appendBlock(byte[] bytes, int count) { + if (count == 0 || isNull(bytes) || bytes.length == 0) { + return; + } + + this.getHeader().increasePayloadLength(count); + this.payloadHash.update(bytes, 0, count); + } + + public void calculateHash() { + this.getHeader().setPayloadCheckSum(this.payloadHash.digest()); + } + + public long getPayloadLength() { + return getHeader().getPayloadLength(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/LinkObject.java b/models/src/main/java/info/frostfs/sdk/dto/object/LinkObject.java new file mode 100644 index 0000000..d7a7091 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/LinkObject.java @@ -0,0 +1,26 @@ +package info.frostfs.sdk.dto.object; + +import info.frostfs.sdk.dto.Split; +import info.frostfs.sdk.dto.SplitId; +import info.frostfs.sdk.dto.container.ContainerId; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.List; + +public class LinkObject extends ObjectFrostFS { + + public LinkObject(ContainerId cid, SplitId splitId, LargeObject largeObject) { + super(cid, new byte[]{}); + var split = new Split(splitId); + split.setParentHeader(largeObject.getHeader()); + this.getHeader().setSplit(split); + } + + public void addChildren(List objectIds) { + if (CollectionUtils.isEmpty(objectIds)) { + return; + } + + this.getHeader().getSplit().getChildren().addAll(objectIds); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/ObjectAttribute.java b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectAttribute.java new file mode 100644 index 0000000..60a3ede --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectAttribute.java @@ -0,0 +1,25 @@ +package info.frostfs.sdk.dto.object; + +import org.apache.commons.lang3.StringUtils; + +public class ObjectAttribute { + private final String key; + private final String value; + + public ObjectAttribute(String key, String value) { + if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) { + throw new IllegalArgumentException("One of the input attributes is missing"); + } + + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/ObjectFilter.java b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectFilter.java new file mode 100644 index 0000000..1443ee2 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectFilter.java @@ -0,0 +1,60 @@ +package info.frostfs.sdk.dto.object; + +import info.frostfs.sdk.dto.OwnerId; +import info.frostfs.sdk.dto.Version; +import info.frostfs.sdk.enums.ObjectMatchType; + +public class ObjectFilter { + private static final String HEADER_PREFIX = "$Object:"; + + private ObjectMatchType matchType; + private String key; + private String value; + + + public ObjectFilter(ObjectMatchType matchType, String key, String value) { + this.matchType = matchType; + this.key = key; + this.value = value; + } + + public static ObjectFilter ObjectIdFilter(ObjectMatchType matchType, ObjectId objectId) { + return new ObjectFilter(matchType, HEADER_PREFIX + "objectID", objectId.getValue()); + } + + public static ObjectFilter OwnerFilter(ObjectMatchType matchType, OwnerId ownerId) { + return new ObjectFilter(matchType, HEADER_PREFIX + "ownerID", ownerId.getValue()); + } + + public static ObjectFilter RootFilter() { + return new ObjectFilter(ObjectMatchType.UNSPECIFIED, HEADER_PREFIX + "ROOT", ""); + } + + public static ObjectFilter VersionFilter(ObjectMatchType matchType, Version version) { + return new ObjectFilter(matchType, HEADER_PREFIX + "version", version.toString()); + } + + public ObjectMatchType getMatchType() { + return matchType; + } + + public void setMatchType(ObjectMatchType matchType) { + this.matchType = matchType; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/ObjectFrostFS.java b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectFrostFS.java new file mode 100644 index 0000000..af5f463 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectFrostFS.java @@ -0,0 +1,80 @@ +package info.frostfs.sdk.dto.object; + +import info.frostfs.sdk.dto.Split; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.enums.ObjectType; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.isNull; + +public class ObjectFrostFS { + private final ObjectHeader header; + private ObjectId objectId; + private byte[] payload; + + public ObjectFrostFS(ObjectHeader header, ObjectId objectId, byte[] payload) { + if (isNull(header)) { + throw new IllegalArgumentException("Object header is missing"); + } + + this.header = header; + this.objectId = objectId; + this.payload = payload; + } + + public ObjectFrostFS(ContainerId containerId, byte[] payload) { + this.payload = payload; + this.header = new ObjectHeader(containerId, new ArrayList<>()); + } + + public ObjectFrostFS(ContainerId containerId, byte[] payload, ObjectType objectType) { + this.payload = payload; + this.header = new ObjectHeader(containerId, objectType, new ArrayList<>()); + } + + public ObjectHeader getHeader() { + return header; + } + + public ObjectId getObjectId() { + return objectId; + } + + private void setObjectId(ObjectId objectId) { + this.objectId = objectId; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public void setSplit(Split split) { + header.setSplit(split); + } + + public void addAttribute(String key, String value) { + header.getAttributes().add(new ObjectAttribute(key, value)); + } + + public void addAttribute(ObjectAttribute attribute) { + header.getAttributes().add(attribute); + } + + public void addAttributes(List attributes) { + header.getAttributes().addAll(attributes); + } + + public void setParent(LargeObject largeObject) { + if (isNull(header.getSplit())) { + throw new IllegalArgumentException("The object is not initialized properly"); + } + + header.getSplit().setParentHeader(largeObject.getHeader()); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/ObjectHeader.java b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectHeader.java new file mode 100644 index 0000000..208b936 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectHeader.java @@ -0,0 +1,128 @@ +package info.frostfs.sdk.dto.object; + +import info.frostfs.sdk.dto.OwnerId; +import info.frostfs.sdk.dto.Split; +import info.frostfs.sdk.dto.Version; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.enums.ObjectType; + +import java.util.List; + +import static java.util.Objects.isNull; + +public class ObjectHeader { + private final ContainerId containerId; + private final ObjectType objectType; + private List attributes; + private long size; + private Version version; + private OwnerId ownerId; + private long payloadLength; + private byte[] payloadCheckSum; + private Split split; + + public ObjectHeader(ContainerId containerId, ObjectType objectType, + List attributes, long size, Version version) { + if (isNull(containerId) || isNull(objectType)) { + throw new IllegalArgumentException("ContainerId or objectType is not present"); + } + + this.attributes = attributes; + this.containerId = containerId; + this.size = size; + this.objectType = objectType; + this.version = version; + } + + public ObjectHeader(ContainerId containerId, ObjectType objectType, List attributes) { + if (isNull(containerId) || isNull(objectType)) { + throw new IllegalArgumentException("ContainerId or objectType is not present"); + } + + this.attributes = attributes; + this.containerId = containerId; + this.objectType = objectType; + } + + public ObjectHeader(ContainerId containerId, List attributes) { + if (isNull(containerId)) { + throw new IllegalArgumentException("ContainerId is not present"); + } + + this.attributes = attributes; + this.containerId = containerId; + this.objectType = ObjectType.REGULAR; + } + + public OwnerId getOwnerId() { + return ownerId; + } + + public void setOwnerId(OwnerId ownerId) { + this.ownerId = ownerId; + } + + public long getPayloadLength() { + return payloadLength; + } + + public void setPayloadLength(long payloadLength) { + this.payloadLength = payloadLength; + } + + public void increasePayloadLength(long payloadLength) { + this.payloadLength += payloadLength; + } + + public byte[] getPayloadCheckSum() { + return payloadCheckSum; + } + + public void setPayloadCheckSum(byte[] payloadCheckSum) { + this.payloadCheckSum = payloadCheckSum; + } + + public Split getSplit() { + return split; + } + + public void setSplit(Split split) { + if (isNull(split)) { + throw new IllegalArgumentException("Split is not present"); + } + + this.split = split; + } + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + public ContainerId getContainerId() { + return containerId; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public ObjectType getObjectType() { + return objectType; + } + + public Version getVersion() { + return version; + } + + public void setVersion(Version version) { + this.version = version; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/object/ObjectId.java b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectId.java new file mode 100644 index 0000000..7c6bffb --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/object/ObjectId.java @@ -0,0 +1,40 @@ +package info.frostfs.sdk.dto.object; + +import info.frostfs.sdk.Base58; +import info.frostfs.sdk.constants.AppConst; +import org.apache.commons.lang3.StringUtils; + +import static java.util.Objects.isNull; + +public class ObjectId { + private final String value; + + public ObjectId(String value) { + if (StringUtils.isEmpty(value)) { + throw new IllegalArgumentException("ObjectId value is missing"); + } + + this.value = value; + } + + public ObjectId(byte[] hash) { + if (isNull(hash) || hash.length != AppConst.SHA256_HASH_LENGTH) { + throw new IllegalArgumentException("ObjectId must be a sha256 hash."); + } + + this.value = Base58.encode(hash); + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + public byte[] toHash() { + return Base58.decode(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/BasicAcl.java b/models/src/main/java/info/frostfs/sdk/enums/BasicAcl.java new file mode 100644 index 0000000..52bab99 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/BasicAcl.java @@ -0,0 +1,33 @@ +package info.frostfs.sdk.enums; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum BasicAcl { + PRIVATE(0x1C8C8CCC), + PUBLIC_RO(0x1FBF8CFF), + PUBLIC_RW(0x1FBFBFFF), + PUBLIC_APPEND(0x1FBF9FFF), + ; + + private static final Map ENUM_MAP_BY_VALUE; + + static { + Map map = new HashMap<>(); + for (BasicAcl basicAcl : BasicAcl.values()) { + map.put(basicAcl.value, basicAcl); + } + ENUM_MAP_BY_VALUE = Collections.unmodifiableMap(map); + } + + public final int value; + + BasicAcl(int value) { + this.value = value; + } + + public static BasicAcl get(int value) { + return ENUM_MAP_BY_VALUE.get(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/NodeState.java b/models/src/main/java/info/frostfs/sdk/enums/NodeState.java new file mode 100644 index 0000000..3f833e3 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/NodeState.java @@ -0,0 +1,33 @@ +package info.frostfs.sdk.enums; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum NodeState { + UNSPECIFIED(0), + ONLINE(1), + OFFLINE(2), + MAINTENANCE(3), + ; + + private static final Map ENUM_MAP_BY_VALUE; + + static { + Map map = new HashMap<>(); + for (NodeState nodeState : NodeState.values()) { + map.put(nodeState.value, nodeState); + } + ENUM_MAP_BY_VALUE = Collections.unmodifiableMap(map); + } + + public final int value; + + NodeState(int value) { + this.value = value; + } + + public static NodeState get(int value) { + return ENUM_MAP_BY_VALUE.get(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/ObjectMatchType.java b/models/src/main/java/info/frostfs/sdk/enums/ObjectMatchType.java new file mode 100644 index 0000000..2122ad6 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/ObjectMatchType.java @@ -0,0 +1,16 @@ +package info.frostfs.sdk.enums; + +public enum ObjectMatchType { + UNSPECIFIED(0), + EQUALS(1), + NOT_EQUALS(2), + KEY_ABSENT(3), + STARTS_WITH(4), + ; + + public final int value; + + ObjectMatchType(int value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/ObjectType.java b/models/src/main/java/info/frostfs/sdk/enums/ObjectType.java new file mode 100644 index 0000000..d252a7a --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/ObjectType.java @@ -0,0 +1,32 @@ +package info.frostfs.sdk.enums; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum ObjectType { + REGULAR(0), + TOMBSTONE(1), + LOCK(3), + ; + + private static final Map ENUM_MAP_BY_VALUE; + + static { + Map map = new HashMap<>(); + for (ObjectType objectType : ObjectType.values()) { + map.put(objectType.value, objectType); + } + ENUM_MAP_BY_VALUE = Collections.unmodifiableMap(map); + } + + public final int value; + + ObjectType(int value) { + this.value = value; + } + + public static ObjectType get(int value) { + return ENUM_MAP_BY_VALUE.get(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/SignatureScheme.java b/models/src/main/java/info/frostfs/sdk/enums/SignatureScheme.java new file mode 100644 index 0000000..7bb7511 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/SignatureScheme.java @@ -0,0 +1,14 @@ +package info.frostfs.sdk.enums; + +public enum SignatureScheme { + ECDSA_SHA512(0), + ECDSA_RFC6979_SHA256(1), + ECDSA_RFC6979_SHA256_WALLET_CONNECT(2), + ; + + public final int value; + + SignatureScheme(int value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/StatusCode.java b/models/src/main/java/info/frostfs/sdk/enums/StatusCode.java new file mode 100644 index 0000000..b67e766 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/StatusCode.java @@ -0,0 +1,46 @@ +package info.frostfs.sdk.enums; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum StatusCode { + SUCCESS(0), + INTERNAL(1024), + WRONG_MAGIC_NUMBER(1025), + SIGNATURE_VERIFICATION_FAILURE(1026), + NODE_UNDER_MAINTENANCE(1027), + OBJECT_ACCESS_DENIED(2048), + OBJECT_NOT_FOUND(2049), + OBJECT_LOCKED(2050), + LOCK_NOT_REGULAR_OBJECT(2051), + OBJECT_ALREADY_REMOVED(2052), + OUT_OF_RANGE(2053), + CONTAINER_NOT_FOUND(3072), + E_ACL_NOT_FOUND(3073), + CONTAINER_ACCESS_DENIED(3074), + TOKEN_NOT_FOUND(4096), + TOKEN_EXPIRED(4097), + APE_MANAGER_ACCESS_DENIED(5120), + ; + + private static final Map ENUM_MAP_BY_VALUE; + + static { + Map map = new HashMap<>(); + for (StatusCode statusCode : StatusCode.values()) { + map.put(statusCode.value, statusCode); + } + ENUM_MAP_BY_VALUE = Collections.unmodifiableMap(map); + } + + public final int value; + + StatusCode(int value) { + this.value = value; + } + + public static StatusCode get(int value) { + return ENUM_MAP_BY_VALUE.get(value); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/MetaHeaderMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/MetaHeaderMapper.java new file mode 100644 index 0000000..0b90453 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/MetaHeaderMapper.java @@ -0,0 +1,24 @@ +package info.frostfs.sdk.mappers; + +import frostfs.session.Types; +import info.frostfs.sdk.dto.MetaHeader; + +import static java.util.Objects.isNull; + +public class MetaHeaderMapper { + + private MetaHeaderMapper() { + } + + public static Types.RequestMetaHeader toGrpcMessage(MetaHeader metaHeader) { + if (isNull(metaHeader)) { + return null; + } + + return Types.RequestMetaHeader.newBuilder() + .setVersion(VersionMapper.toGrpcMessage(metaHeader.getVersion())) + .setEpoch(metaHeader.getEpoch()) + .setTtl(metaHeader.getTtl()) + .build(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/OwnerIdMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/OwnerIdMapper.java new file mode 100644 index 0000000..a0a6b64 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/OwnerIdMapper.java @@ -0,0 +1,23 @@ +package info.frostfs.sdk.mappers; + +import com.google.protobuf.ByteString; +import frostfs.refs.Types; +import info.frostfs.sdk.dto.OwnerId; + +import static java.util.Objects.isNull; + +public class OwnerIdMapper { + + private OwnerIdMapper() { + } + + public static Types.OwnerID toGrpcMessage(OwnerId ownerId) { + if (isNull(ownerId)) { + return null; + } + + return Types.OwnerID.newBuilder() + .setValue(ByteString.copyFrom(ownerId.toHash())) + .build(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/SessionMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/SessionMapper.java new file mode 100644 index 0000000..45331cf --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/SessionMapper.java @@ -0,0 +1,42 @@ +package info.frostfs.sdk.mappers; + +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.InvalidProtocolBufferException; +import frostfs.session.Types; + +import java.io.IOException; + +import static java.util.Objects.isNull; + +public class SessionMapper { + + private SessionMapper() { + } + + public static byte[] serialize(Types.SessionToken token) { + if (isNull(token)) { + throw new IllegalArgumentException("Token is not present"); + } + + try { + byte[] bytes = new byte[token.getSerializedSize()]; + CodedOutputStream stream = CodedOutputStream.newInstance(bytes); + token.writeTo(stream); + return bytes; + } catch (IOException exp) { + throw new IllegalArgumentException(exp.getMessage()); + } + } + + public static Types.SessionToken deserializeSessionToken(byte[] bytes) { + if (isNull(bytes) || bytes.length == 0) { + throw new IllegalArgumentException("Token is not present"); + } + + try { + return Types.SessionToken.newBuilder().mergeFrom(bytes).build(); + } catch (InvalidProtocolBufferException exp) { + throw new IllegalArgumentException(exp.getMessage()); + } + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/SignatureMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/SignatureMapper.java new file mode 100644 index 0000000..5f14f4a --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/SignatureMapper.java @@ -0,0 +1,32 @@ +package info.frostfs.sdk.mappers; + +import com.google.protobuf.ByteString; +import frostfs.refs.Types; +import info.frostfs.sdk.dto.Signature; + +import static java.util.Objects.isNull; + +public class SignatureMapper { + + private SignatureMapper() { + } + + public static Types.Signature toGrpcMessage(Signature signature) { + if (isNull(signature)) { + return null; + } + + var scheme = Types.SignatureScheme.forNumber(signature.getScheme().value); + if (isNull(scheme)) { + throw new IllegalArgumentException( + String.format("Unknown SignatureScheme. Value: %s.", signature.getScheme().name()) + ); + } + + return Types.Signature.newBuilder() + .setKey(ByteString.copyFrom(signature.getKey())) + .setSign(ByteString.copyFrom(signature.getSign())) + .setScheme(scheme) + .build(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/StatusMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/StatusMapper.java new file mode 100644 index 0000000..2bfa717 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/StatusMapper.java @@ -0,0 +1,28 @@ +package info.frostfs.sdk.mappers; + +import frostfs.status.Types; +import info.frostfs.sdk.dto.Status; +import info.frostfs.sdk.enums.StatusCode; + +import static java.util.Objects.isNull; + +public class StatusMapper { + + private StatusMapper() { + } + + public static Status toModel(Types.Status status) { + if (isNull(status)) { + return new Status(StatusCode.SUCCESS); + } + + var statusCode = StatusCode.get(status.getCode()); + if (isNull(statusCode)) { + throw new IllegalArgumentException( + String.format("Unknown StatusCode. Value: %s.", status.getCode()) + ); + } + + return new Status(statusCode, status.getMessage()); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/VersionMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/VersionMapper.java new file mode 100644 index 0000000..13e7a0d --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/VersionMapper.java @@ -0,0 +1,31 @@ +package info.frostfs.sdk.mappers; + +import frostfs.refs.Types; +import info.frostfs.sdk.dto.Version; + +import static java.util.Objects.isNull; + +public class VersionMapper { + + private VersionMapper() { + } + + public static Types.Version toGrpcMessage(Version version) { + if (isNull(version)) { + return null; + } + + return Types.Version.newBuilder() + .setMajor(version.getMajor()) + .setMinor(version.getMinor()) + .build(); + } + + public static Version toModel(Types.Version version) { + if (isNull(version)) { + return null; + } + + return new Version(version.getMajor(), version.getMinor()); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/container/ContainerIdMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/container/ContainerIdMapper.java new file mode 100644 index 0000000..26e941c --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/container/ContainerIdMapper.java @@ -0,0 +1,23 @@ +package info.frostfs.sdk.mappers.container; + +import com.google.protobuf.ByteString; +import frostfs.refs.Types; +import info.frostfs.sdk.dto.container.ContainerId; + +import static java.util.Objects.isNull; + +public class ContainerIdMapper { + + private ContainerIdMapper() { + } + + public static Types.ContainerID toGrpcMessage(ContainerId containerId) { + if (isNull(containerId)) { + return null; + } + + return Types.ContainerID.newBuilder() + .setValue(ByteString.copyFrom(containerId.toHash())) + .build(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/container/ContainerMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/container/ContainerMapper.java new file mode 100644 index 0000000..9aa1741 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/container/ContainerMapper.java @@ -0,0 +1,48 @@ +package info.frostfs.sdk.mappers.container; + +import com.google.protobuf.ByteString; +import frostfs.container.Types; +import info.frostfs.sdk.dto.container.Container; +import info.frostfs.sdk.enums.BasicAcl; +import info.frostfs.sdk.mappers.VersionMapper; +import info.frostfs.sdk.mappers.netmap.PlacementPolicyMapper; + +import static info.frostfs.sdk.UUIDExtension.asBytes; +import static info.frostfs.sdk.UUIDExtension.asUuid; +import static java.util.Objects.isNull; + +public class ContainerMapper { + + private ContainerMapper() { + } + + public static Types.Container toGrpcMessage(Container container) { + if (isNull(container)) { + return null; + } + + return Types.Container.newBuilder() + .setBasicAcl(container.getBasicAcl().value) + .setPlacementPolicy(PlacementPolicyMapper.toGrpcMessage(container.getPlacementPolicy())) + .setNonce(ByteString.copyFrom(asBytes(container.getNonce()))) + .build(); + } + + public static Container toModel(Types.Container containerGrpc) { + if (isNull(containerGrpc)) { + return null; + } + + var basicAcl = BasicAcl.get(containerGrpc.getBasicAcl()); + if (isNull(basicAcl)) { + throw new IllegalArgumentException( + String.format("Unknown BasicACL rule. Value: %s.", containerGrpc.getBasicAcl()) + ); + } + + var container = new Container(basicAcl, PlacementPolicyMapper.toModel(containerGrpc.getPlacementPolicy())); + container.setNonce(asUuid(containerGrpc.getNonce().toByteArray())); + container.setVersion(VersionMapper.toModel(containerGrpc.getVersion())); + return container; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/netmap/NetmapSnapshotMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/netmap/NetmapSnapshotMapper.java new file mode 100644 index 0000000..f911e74 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/netmap/NetmapSnapshotMapper.java @@ -0,0 +1,27 @@ +package info.frostfs.sdk.mappers.netmap; + +import frostfs.netmap.Service; +import info.frostfs.sdk.dto.netmap.NetmapSnapshot; + +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; + +public class NetmapSnapshotMapper { + + private NetmapSnapshotMapper() { + } + + public static NetmapSnapshot toModel(Service.NetmapSnapshotResponse netmap) { + if (isNull(netmap)) { + return null; + } + + return new NetmapSnapshot( + netmap.getBody().getNetmap().getEpoch(), + netmap.getBody().getNetmap().getNodesList().stream() + .map(node -> NodeInfoMapper.toModel(node, netmap.getMetaHeader().getVersion())) + .collect(Collectors.toList()) + ); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/netmap/NodeInfoMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/netmap/NodeInfoMapper.java new file mode 100644 index 0000000..d91dcd1 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/netmap/NodeInfoMapper.java @@ -0,0 +1,47 @@ +package info.frostfs.sdk.mappers.netmap; + +import frostfs.netmap.Service; +import frostfs.netmap.Types.NodeInfo.Attribute; +import frostfs.refs.Types; +import info.frostfs.sdk.dto.netmap.NodeInfo; +import info.frostfs.sdk.enums.NodeState; +import info.frostfs.sdk.mappers.VersionMapper; + +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; + +public class NodeInfoMapper { + + private NodeInfoMapper() { + } + + public static NodeInfo toModel(Service.LocalNodeInfoResponse.Body nodeInfo) { + if (isNull(nodeInfo)) { + return null; + } + + return toModel(nodeInfo.getNodeInfo(), nodeInfo.getVersion()); + } + + public static NodeInfo toModel(frostfs.netmap.Types.NodeInfo nodeInfo, Types.Version version) { + if (isNull(nodeInfo)) { + return null; + } + + NodeState nodeState = NodeState.get(nodeInfo.getState().getNumber()); + if (isNull(nodeState)) { + throw new IllegalArgumentException( + String.format("Unknown NodeState. Value: %s.", nodeInfo.getState()) + ); + } + + return new NodeInfo( + nodeState, + VersionMapper.toModel(version), + nodeInfo.getAddressesList(), + nodeInfo.getAttributesList().stream().collect(Collectors.toMap(Attribute::getKey, Attribute::getValue)), + nodeInfo.getPublicKey().toByteArray() + ); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/netmap/PlacementPolicyMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/netmap/PlacementPolicyMapper.java new file mode 100644 index 0000000..f5bdaef --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/netmap/PlacementPolicyMapper.java @@ -0,0 +1,39 @@ +package info.frostfs.sdk.mappers.netmap; + +import frostfs.netmap.Types; +import info.frostfs.sdk.dto.netmap.PlacementPolicy; +import info.frostfs.sdk.dto.netmap.Replica; + +import static java.util.Objects.isNull; + +public class PlacementPolicyMapper { + + private PlacementPolicyMapper() { + } + + public static Types.PlacementPolicy toGrpcMessage(PlacementPolicy placementPolicy) { + if (isNull(placementPolicy)) { + return null; + } + + var pp = Types.PlacementPolicy.newBuilder() + .setUnique(placementPolicy.isUnique()); + + for (Replica replica : placementPolicy.getReplicas()) { + pp.addReplicas(ReplicaMapper.toGrpcMessage(replica)); + } + + return pp.build(); + } + + public static PlacementPolicy toModel(Types.PlacementPolicy placementPolicy) { + if (isNull(placementPolicy)) { + return null; + } + + return new PlacementPolicy( + placementPolicy.getUnique(), + placementPolicy.getReplicasList().stream().map(ReplicaMapper::toModel).toArray(Replica[]::new) + ); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/netmap/ReplicaMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/netmap/ReplicaMapper.java new file mode 100644 index 0000000..e25ec21 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/netmap/ReplicaMapper.java @@ -0,0 +1,31 @@ +package info.frostfs.sdk.mappers.netmap; + +import frostfs.netmap.Types; +import info.frostfs.sdk.dto.netmap.Replica; + +import static java.util.Objects.isNull; + +public class ReplicaMapper { + + private ReplicaMapper() { + } + + public static Types.Replica toGrpcMessage(Replica replica) { + if (isNull(replica)) { + return null; + } + + return Types.Replica.newBuilder() + .setCount(replica.getCount()) + .setSelector(replica.getSelector()) + .build(); + } + + public static Replica toModel(Types.Replica replica) { + if (isNull(replica)) { + return null; + } + + return new Replica(replica.getCount(), replica.getSelector()); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectAttributeMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectAttributeMapper.java new file mode 100644 index 0000000..17c3e94 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectAttributeMapper.java @@ -0,0 +1,31 @@ +package info.frostfs.sdk.mappers.object; + +import frostfs.object.Types; +import info.frostfs.sdk.dto.object.ObjectAttribute; + +import static java.util.Objects.isNull; + +public class ObjectAttributeMapper { + + private ObjectAttributeMapper() { + } + + public static Types.Header.Attribute toGrpcMessage(ObjectAttribute attribute) { + if (isNull(attribute)) { + return null; + } + + return Types.Header.Attribute.newBuilder() + .setKey(attribute.getKey()) + .setValue(attribute.getValue()) + .build(); + } + + public static ObjectAttribute toModel(Types.Header.Attribute attribute) { + if (isNull(attribute)) { + return null; + } + + return new ObjectAttribute(attribute.getKey(), attribute.getValue()); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectFilterMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectFilterMapper.java new file mode 100644 index 0000000..b1c121d --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectFilterMapper.java @@ -0,0 +1,32 @@ +package info.frostfs.sdk.mappers.object; + +import frostfs.object.Service; +import frostfs.object.Types; +import info.frostfs.sdk.dto.object.ObjectFilter; + +import static java.util.Objects.isNull; + +public class ObjectFilterMapper { + + private ObjectFilterMapper() { + } + + public static Service.SearchRequest.Body.Filter toGrpcMessage(ObjectFilter filter) { + if (isNull(filter)) { + return null; + } + + var objectMatchType = Types.MatchType.forNumber(filter.getMatchType().value); + if (isNull(objectMatchType)) { + throw new IllegalArgumentException( + String.format("Unknown MatchType. Value: %s.", filter.getMatchType().name()) + ); + } + + return Service.SearchRequest.Body.Filter.newBuilder() + .setMatchType(objectMatchType) + .setKey(filter.getKey()) + .setValue(filter.getValue()) + .build(); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectFrostFSMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectFrostFSMapper.java new file mode 100644 index 0000000..aa5f698 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectFrostFSMapper.java @@ -0,0 +1,25 @@ +package info.frostfs.sdk.mappers.object; + +import frostfs.object.Types; +import info.frostfs.sdk.dto.object.ObjectFrostFS; +import info.frostfs.sdk.dto.object.ObjectId; + +import static java.util.Objects.isNull; + +public class ObjectFrostFSMapper { + + private ObjectFrostFSMapper() { + } + + public static ObjectFrostFS toModel(Types.Object object) { + if (isNull(object)) { + return null; + } + + return new ObjectFrostFS( + ObjectHeaderMapper.toModel(object.getHeader()), + new ObjectId(object.getObjectId().getValue().toByteArray()), + object.getPayload().toByteArray() + ); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectHeaderMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectHeaderMapper.java new file mode 100644 index 0000000..fc5b4cc --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectHeaderMapper.java @@ -0,0 +1,63 @@ +package info.frostfs.sdk.mappers.object; + +import frostfs.object.Types; +import info.frostfs.sdk.dto.container.ContainerId; +import info.frostfs.sdk.dto.object.ObjectAttribute; +import info.frostfs.sdk.dto.object.ObjectHeader; +import info.frostfs.sdk.enums.ObjectType; +import info.frostfs.sdk.mappers.VersionMapper; +import info.frostfs.sdk.mappers.container.ContainerIdMapper; + +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; + +public class ObjectHeaderMapper { + + private ObjectHeaderMapper() { + } + + public static Types.Header toGrpcMessage(ObjectHeader header) { + if (isNull(header)) { + return null; + } + + var objectType = Types.ObjectType.forNumber(header.getObjectType().value); + if (isNull(objectType)) { + throw new IllegalArgumentException( + String.format("Unknown ObjectType. Value: %s.", header.getObjectType().name()) + ); + } + + var head = Types.Header.newBuilder() + .setContainerId(ContainerIdMapper.toGrpcMessage(header.getContainerId())) + .setObjectType(objectType); + + for (ObjectAttribute objectAttribute : header.getAttributes()) { + head.addAttributes(ObjectAttributeMapper.toGrpcMessage(objectAttribute)); + } + + return head.build(); + } + + public static ObjectHeader toModel(Types.Header header) { + if (isNull(header)) { + return null; + } + + var objectType = ObjectType.get(header.getObjectTypeValue()); + if (isNull(objectType)) { + throw new IllegalArgumentException( + String.format("Unknown ObjectType. Value: %s.", header.getObjectType()) + ); + } + + return new ObjectHeader( + new ContainerId(header.getContainerId().getValue().toByteArray()), + objectType, + header.getAttributesList().stream().map(ObjectAttributeMapper::toModel).collect(Collectors.toList()), + header.getPayloadLength(), + VersionMapper.toModel(header.getVersion()) + ); + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectIdMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectIdMapper.java new file mode 100644 index 0000000..af51095 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/mappers/object/ObjectIdMapper.java @@ -0,0 +1,27 @@ +package info.frostfs.sdk.mappers.object; + +import com.google.protobuf.ByteString; +import frostfs.refs.Types; +import info.frostfs.sdk.dto.object.ObjectId; + +import static java.util.Objects.isNull; + +public class ObjectIdMapper { + + private ObjectIdMapper() { + } + + public static Types.ObjectID toGrpcMessage(ObjectId objectId) { + if (isNull(objectId)) { + return null; + } + + return Types.ObjectID.newBuilder() + .setValue(ByteString.copyFrom(objectId.toHash())) + .build(); + } + + public static ObjectId toModel(Types.ObjectID objectId) { + return new ObjectId(objectId.getValue().toByteArray()); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..561413c --- /dev/null +++ b/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + info.frostfs.sdk + frostfs-sdk-java + 0.1.0 + pom + + client + cryptography + models + protos + + + + 11 + 11 + UTF-8 + + \ No newline at end of file diff --git a/protos/pom.xml b/protos/pom.xml new file mode 100644 index 0000000..d2e06dc --- /dev/null +++ b/protos/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + info.frostfs.sdk + frostfs-sdk-java + 0.1.0 + + + protos + + + 11 + 11 + 3.23.0 + 1.65.1 + UTF-8 + + + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-services + ${grpc.version} + runtime + + + javax.annotation + javax.annotation-api + 1.3.2 + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + + grpc-java + + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + + compile + compile-custom + + test-compile + test-compile-custom + + + + + + + + \ No newline at end of file diff --git a/protos/src/main/proto/accounting/service.proto b/protos/src/main/proto/accounting/service.proto new file mode 100644 index 0000000..414d71d --- /dev/null +++ b/protos/src/main/proto/accounting/service.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package neo.fs.v2.accounting; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting/grpc;accounting"; +option java_package = "frostfs.accounting"; + +import "accounting/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// Accounting service provides methods for interaction with NeoFS sidechain via +// other NeoFS nodes to get information about the account balance. Deposit and +// Withdraw operations can't be implemented here, as they require Mainnet NeoFS +// smart contract invocation. Transfer operations between internal NeoFS +// accounts are possible if both use the same token type. +service AccountingService { + // Returns the amount of funds in GAS token for the requested NeoFS account. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // balance has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc Balance(BalanceRequest) returns (BalanceResponse); +} + +// BalanceRequest message +message BalanceRequest { + // To indicate the account for which the balance is requested, its identifier + // is used. It can be any existing account in NeoFS sidechain `Balance` smart + // contract. If omitted, client implementation MUST set it to the request's + // signer `OwnerID`. + message Body { + // Valid user identifier in `OwnerID` format for which the balance is + // requested. Required field. + neo.fs.v2.refs.OwnerID owner_id = 1; + } + // Body of the balance request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// BalanceResponse message +message BalanceResponse { + // The amount of funds in GAS token for the `OwnerID`'s account requested. + // Balance is given in the `Decimal` format to avoid precision issues with + // rounding. + message Body { + // Amount of funds in GAS token for the requested account. + Decimal balance = 1; + } + // Body of the balance response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/protos/src/main/proto/accounting/types.proto b/protos/src/main/proto/accounting/types.proto new file mode 100644 index 0000000..61952f5 --- /dev/null +++ b/protos/src/main/proto/accounting/types.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package neo.fs.v2.accounting; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting/grpc;accounting"; +option java_package = "frostfs.accounting"; + +// Standard floating point data type can't be used in NeoFS due to inexactness +// of the result when doing lots of small number operations. To solve the lost +// precision issue, special `Decimal` format is used for monetary computations. +// +// Please see [The General Decimal Arithmetic +// Specification](http://speleotrove.com/decimal/) for detailed problem +// description. +message Decimal { + // Number in the smallest Token fractions. + int64 value = 1 [ json_name = "value" ]; + + // Precision value indicating how many smallest fractions can be in one + // integer. + uint32 precision = 2 [ json_name = "precision" ]; +} diff --git a/protos/src/main/proto/acl/types.proto b/protos/src/main/proto/acl/types.proto new file mode 100644 index 0000000..b59ac7d --- /dev/null +++ b/protos/src/main/proto/acl/types.proto @@ -0,0 +1,227 @@ +syntax = "proto3"; + +package neo.fs.v2.acl; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl/grpc;acl"; +option java_package = "frostfs.acl"; + +import "refs/types.proto"; + +// Target role of the access control rule in access control list. +enum Role { + // Unspecified role, default value + ROLE_UNSPECIFIED = 0; + + // User target rule is applied if sender is the owner of the container + USER = 1; + + // System target rule is applied if sender is a storage node within the + // container or an inner ring node + SYSTEM = 2; + + // Others target rule is applied if sender is neither a user nor a system + // target + OTHERS = 3; +} + +// MatchType is an enumeration of match types. +enum MatchType { + // Unspecified match type, default value. + MATCH_TYPE_UNSPECIFIED = 0; + + // Return true if strings are equal + STRING_EQUAL = 1; + + // Return true if strings are different + STRING_NOT_EQUAL = 2; +} + +// Request's operation type to match if the rule is applicable to a particular +// request. +enum Operation { + // Unspecified operation, default value + OPERATION_UNSPECIFIED = 0; + + // Get + GET = 1; + + // Head + HEAD = 2; + + // Put + PUT = 3; + + // Delete + DELETE = 4; + + // Search + SEARCH = 5; + + // GetRange + GETRANGE = 6; + + // GetRangeHash + GETRANGEHASH = 7; +} + +// Rule execution result action. Either allows or denies access if the rule's +// filters match. +enum Action { + // Unspecified action, default value + ACTION_UNSPECIFIED = 0; + + // Allow action + ALLOW = 1; + + // Deny action + DENY = 2; +} + +// Enumeration of possible sources of Headers to apply filters. +enum HeaderType { + // Unspecified header, default value. + HEADER_UNSPECIFIED = 0; + + // Filter request headers + REQUEST = 1; + + // Filter object headers + OBJECT = 2; + + // Filter service headers. These are not processed by NeoFS nodes and + // exist for service use only. + SERVICE = 3; +} + +// Describes a single eACL rule. +message EACLRecord { + // NeoFS request Verb to match + Operation operation = 1 [ json_name = "operation" ]; + + // Rule execution result. Either allows or denies access if filters match. + Action action = 2 [ json_name = "action" ]; + + // Filter to check particular properties of the request or the object. + // + // By default `key` field refers to the corresponding object's `Attribute`. + // Some Object's header fields can also be accessed by adding `$Object:` + // prefix to the name. Here is the list of fields available via this prefix: + // + // * $Object:version \ + // version + // * $Object:objectID \ + // object_id + // * $Object:containerID \ + // container_id + // * $Object:ownerID \ + // owner_id + // * $Object:creationEpoch \ + // creation_epoch + // * $Object:payloadLength \ + // payload_length + // * $Object:payloadHash \ + // payload_hash + // * $Object:objectType \ + // object_type + // * $Object:homomorphicHash \ + // homomorphic_hash + // + // Please note, that if request or response does not have object's headers of + // full object (Range, RangeHash, Search, Delete), it will not be possible to + // filter by object header fields or user attributes. From the well-known list + // only `$Object:objectID` and `$Object:containerID` will be available, as + // it's possible to take that information from the requested address. + message Filter { + // Define if Object or Request header will be used + HeaderType header_type = 1 [ json_name = "headerType" ]; + + // Match operation type + MatchType match_type = 2 [ json_name = "matchType" ]; + + // Name of the Header to use + string key = 3 [ json_name = "key" ]; + + // Expected Header Value or pattern to match + string value = 4 [ json_name = "value" ]; + } + + // List of filters to match and see if rule is applicable + repeated Filter filters = 3 [ json_name = "filters" ]; + + // Target to apply ACL rule. Can be a subject's role class or a list of public + // keys to match. + message Target { + // Target subject's role class + Role role = 1 [ json_name = "role" ]; + + // List of public keys to identify target subject + repeated bytes keys = 2 [ json_name = "keys" ]; + } + // List of target subjects to apply ACL rule to + repeated Target targets = 4 [ json_name = "targets" ]; +} + +// Extended ACL rules table. A list of ACL rules defined additionally to Basic +// ACL. Extended ACL rules can be attached to a container and can be updated +// or may be defined in `BearerToken` structure. Please see the corresponding +// NeoFS Technical Specification section for detailed description. +message EACLTable { + // eACL format version. Effectively, the version of API library used to create + // eACL Table. + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Identifier of the container that should use given access control rules + neo.fs.v2.refs.ContainerID container_id = 2 [ json_name = "containerID" ]; + + // List of Extended ACL rules + repeated EACLRecord records = 3 [ json_name = "records" ]; +} + +// BearerToken allows to attach signed Extended ACL rules to the request in +// `RequestMetaHeader`. If container's Basic ACL rules allow, the attached rule +// set will be checked instead of one attached to the container itself. Just +// like [JWT](https://jwt.io), it has a limited lifetime and scope, hence can be +// used in the similar use cases, like providing authorisation to externally +// authenticated party. +// +// BearerToken can be issued only by the container's owner and must be signed +// using the key associated with the container's `OwnerID`. +message BearerToken { + // Bearer Token body structure contains Extended ACL table issued by the + // container owner with additional information preventing token abuse. + message Body { + // Table of Extended ACL rules to use instead of the ones attached to the + // container. If it contains `container_id` field, bearer token is only + // valid for this specific container. Otherwise, any container of the same + // owner is allowed. + EACLTable eacl_table = 1 [ json_name = "eaclTable" ]; + + // `OwnerID` defines to whom the token was issued. It must match the request + // originator's `OwnerID`. If empty, any token bearer will be accepted. + neo.fs.v2.refs.OwnerID owner_id = 2 [ json_name = "ownerID" ]; + + // Lifetime parameters of the token. Field names taken from + // [rfc7519](https://tools.ietf.org/html/rfc7519). + message TokenLifetime { + // Expiration Epoch + uint64 exp = 1 [ json_name = "exp" ]; + + // Not valid before Epoch + uint64 nbf = 2 [ json_name = "nbf" ]; + + // Issued at Epoch + uint64 iat = 3 [ json_name = "iat" ]; + } + // Token expiration and valid time period parameters + TokenLifetime lifetime = 3 [ json_name = "lifetime" ]; + + // AllowImpersonate flag to consider token signer as request owner. + // If this field is true extended ACL table in token body isn't processed. + bool allow_impersonate = 4 [ json_name = "allowImpersonate" ]; + } + // Bearer Token body + Body body = 1 [ json_name = "body" ]; + + // Signature of BearerToken body + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; +} diff --git a/protos/src/main/proto/apemanager/service.proto b/protos/src/main/proto/apemanager/service.proto new file mode 100644 index 0000000..d4eeca2 --- /dev/null +++ b/protos/src/main/proto/apemanager/service.proto @@ -0,0 +1,172 @@ +syntax = "proto3"; + +package frostfs.v2.apemanager; + +import "apemanager/types.proto"; +import "session/types.proto"; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager/grpc;apemanager"; +option java_package = "frostfs.apemanager"; + +// `APEManagerService` provides API to manage rule chains within sidechain's +// `Policy` smart contract. +service APEManagerService { + // Add a rule chain for a specific target to `Policy` smart contract. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // the chain has been successfully added; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container (as target) not found; + // - **APE_MANAGER_ACCESS_DENIED** (5120, SECTION_APE_MANAGER): \ + // the operation is denied by the service. + rpc AddChain(AddChainRequest) returns (AddChainResponse); + + // Remove a rule chain for a specific target from `Policy` smart contract. + // RemoveChain is an idempotent operation: removal of non-existing rule chain + // also means success. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // the chain has been successfully removed; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container (as target) not found; + // - **APE_MANAGER_ACCESS_DENIED** (5120, SECTION_APE_MANAGER): \ + // the operation is denied by the service. + rpc RemoveChain(RemoveChainRequest) returns (RemoveChainResponse); + + // List chains defined for a specific target from `Policy` smart contract. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // chains have been successfully listed; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container (as target) not found; + // - **APE_MANAGER_ACCESS_DENIED** (5120, SECTION_APE_MANAGER): \ + // the operation is denied by the service. + rpc ListChains(ListChainsRequest) returns (ListChainsResponse); +} + +message AddChainRequest { + message Body { + // A target for which a rule chain is added. + ChainTarget target = 1; + + // The chain to set for the target. + Chain chain = 2; + } + + // The request's body. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +message AddChainResponse { + message Body { + // Chain ID assigned for the added rule chain. + // If chain ID is left empty in the request, then + // it will be generated. + bytes chain_id = 1; + } + + // The response's body. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +message RemoveChainRequest { + message Body { + // Target for which a rule chain is removed. + ChainTarget target = 1; + + // Chain ID assigned for the rule chain. + bytes chain_id = 2; + } + + // The request's body. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +message RemoveChainResponse { + // Since RemoveChain is an idempotent operation, then the only indicator that + // operation could not be performed is an error returning to a client. + message Body {} + + // The response's body. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +message ListChainsRequest { + message Body { + // Target for which rule chains are listed. + ChainTarget target = 1; + } + + // The request's body. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +message ListChainsResponse { + message Body { + // The list of chains defined for the reqeusted target. + repeated Chain chains = 1; + } + + // The response's body. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} \ No newline at end of file diff --git a/protos/src/main/proto/apemanager/types.proto b/protos/src/main/proto/apemanager/types.proto new file mode 100644 index 0000000..7ca80ae --- /dev/null +++ b/protos/src/main/proto/apemanager/types.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package frostfs.v2.apemanager; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager/grpc;apemanager"; +option java_package = "frostfs.apemanager"; + +// TargetType is a type target to which a rule chain is defined. +enum TargetType { + UNDEFINED = 0; + + NAMESPACE = 1; + + CONTAINER = 2; + + USER = 3; + + GROUP = 4; +} + +// ChainTarget is an object to which a rule chain is defined. +message ChainTarget { + TargetType type = 1; + + string name = 2; +} + +// Chain is a chain of rules defined for a specific target. +message Chain { + oneof kind { + // Raw representation of a serizalized rule chain. + bytes raw = 1; + } +} diff --git a/protos/src/main/proto/container/service.proto b/protos/src/main/proto/container/service.proto new file mode 100644 index 0000000..6a85979 --- /dev/null +++ b/protos/src/main/proto/container/service.proto @@ -0,0 +1,431 @@ +syntax = "proto3"; + +package neo.fs.v2.container; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container/grpc;container"; +option java_package = "frostfs.container"; + +import "acl/types.proto"; +import "container/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// `ContainerService` provides API to interact with `Container` smart contract +// in NeoFS sidechain via other NeoFS nodes. All of those actions can be done +// equivalently by directly issuing transactions and RPC calls to sidechain +// nodes. +service ContainerService { + // `Put` invokes `Container` smart contract's `Put` method and returns + // response immediately. After a new block is issued in sidechain, request is + // verified by Inner Ring nodes. After one more block in sidechain, the + // container is added into smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // request to save the container has been sent to the sidechain; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // container create access denied. + rpc Put(PutRequest) returns (PutResponse); + + // `Delete` invokes `Container` smart contract's `Delete` method and returns + // response immediately. After a new block is issued in sidechain, request is + // verified by Inner Ring nodes. After one more block in sidechain, the + // container is added into smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // request to remove the container has been sent to the sidechain; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // container delete access denied. + rpc Delete(DeleteRequest) returns (DeleteResponse); + + // Returns container structure from `Container` smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // container has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // requested container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied. + rpc Get(GetRequest) returns (GetResponse); + + // Returns all owner's containers from 'Container` smart contract' storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // container list has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // container list access denied. + rpc List(ListRequest) returns (ListResponse); + + // Invokes 'SetEACL' method of 'Container` smart contract and returns response + // immediately. After one more block in sidechain, changes in an Extended ACL + // are added into smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // request to save container eACL has been sent to the sidechain; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // set container eACL access denied. + rpc SetExtendedACL(SetExtendedACLRequest) returns (SetExtendedACLResponse); + + // Returns Extended ACL table and signature from `Container` smart contract + // storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // container eACL has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container not found; + // - **EACL_NOT_FOUND** (3073, SECTION_CONTAINER): \ + // eACL table not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container eACL is denied. + rpc GetExtendedACL(GetExtendedACLRequest) returns (GetExtendedACLResponse); + + // Announces the space values used by the container for P2P synchronization. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // estimation of used space has been successfully announced; + // - Common failures (SECTION_FAILURE_COMMON). + rpc AnnounceUsedSpace(AnnounceUsedSpaceRequest) + returns (AnnounceUsedSpaceResponse); +} + +// New NeoFS Container creation request +message PutRequest { + // Container creation request has container structure's signature as a + // separate field. It's not stored in sidechain, just verified on container + // creation by `Container` smart contract. `ContainerID` is a SHA256 hash of + // the stable-marshalled container strucutre, hence there is no need for + // additional signature checks. + message Body { + // Container structure to register in NeoFS + container.Container container = 1; + + // Signature of a stable-marshalled container according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + } + // Body of container put request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// New NeoFS Container creation response +message PutResponse { + // Container put response body contains information about the newly registered + // container as seen by `Container` smart contract. `ContainerID` can be + // calculated beforehand from the container structure and compared to the one + // returned here to make sure everything has been done as expected. + message Body { + // Unique identifier of the newly created container + neo.fs.v2.refs.ContainerID container_id = 1; + } + // Body of container put response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Container removal request +message DeleteRequest { + // Container removal request body has signed `ContainerID` as a proof of + // the container owner's intent. The signature will be verified by `Container` + // smart contract, so signing algorithm must be supported by NeoVM. + message Body { + // Identifier of the container to delete from NeoFS + neo.fs.v2.refs.ContainerID container_id = 1; + + // `ContainerID` signed with the container owner's key according to + // RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + } + // Body of container delete request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// `DeleteResponse` has an empty body because delete operation is asynchronous +// and done via consensus in Inner Ring nodes. +message DeleteResponse { + // `DeleteResponse` has an empty body because delete operation is asynchronous + // and done via consensus in Inner Ring nodes. + message Body {} + // Body of container delete response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get container structure +message GetRequest { + // Get container structure request body. + message Body { + // Identifier of the container to get + neo.fs.v2.refs.ContainerID container_id = 1; + } + // Body of container get request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get container structure +message GetResponse { + // Get container response body does not have container structure signature. It + // has been already verified upon container creation. + message Body { + // Requested container structure + Container container = 1; + + // Signature of a stable-marshalled container according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + + // Session token if the container has been created within the session + neo.fs.v2.session.SessionToken session_token = 3; + } + // Body of container get response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// List containers +message ListRequest { + // List containers request body. + message Body { + // Identifier of the container owner + neo.fs.v2.refs.OwnerID owner_id = 1; + } + // Body of list containers request message + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// List containers +message ListResponse { + // List containers response body. + message Body { + // List of `ContainerID`s belonging to the requested `OwnerID` + repeated refs.ContainerID container_ids = 1; + } + + // Body of list containers response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Set Extended ACL +message SetExtendedACLRequest { + // Set Extended ACL request body does not have separate `ContainerID` + // reference. It will be taken from `EACLTable.container_id` field. + message Body { + // Extended ACL table to set for the container + neo.fs.v2.acl.EACLTable eacl = 1; + + // Signature of stable-marshalled Extended ACL table according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + } + // Body of set extended acl request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Set Extended ACL +message SetExtendedACLResponse { + // `SetExtendedACLResponse` has an empty body because the operation is + // asynchronous and the update should be reflected in `Container` smart + // contract's storage after next block is issued in sidechain. + message Body {} + + // Body of set extended acl response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get Extended ACL +message GetExtendedACLRequest { + // Get Extended ACL request body + message Body { + // Identifier of the container having Extended ACL + neo.fs.v2.refs.ContainerID container_id = 1; + } + + // Body of get extended acl request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get Extended ACL +message GetExtendedACLResponse { + // Get Extended ACL Response body can be empty if the requested container does + // not have Extended ACL Table attached or Extended ACL has not been allowed + // at the time of container creation. + message Body { + // Extended ACL requested, if available + neo.fs.v2.acl.EACLTable eacl = 1; + + // Signature of stable-marshalled Extended ACL according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + + // Session token if Extended ACL was set within a session + neo.fs.v2.session.SessionToken session_token = 3; + } + // Body of get extended acl response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Announce container used space +message AnnounceUsedSpaceRequest { + // Container used space announcement body. + message Body { + // Announcement contains used space information for a single container. + message Announcement { + // Epoch number for which the container size estimation was produced. + uint64 epoch = 1; + + // Identifier of the container. + neo.fs.v2.refs.ContainerID container_id = 2; + + // Used space is a sum of object payload sizes of a specified + // container, stored in the node. It must not include inhumed objects. + uint64 used_space = 3; + } + + // List of announcements. If nodes share several containers, + // announcements are transferred in a batch. + repeated Announcement announcements = 1; + } + + // Body of announce used space request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Announce container used space +message AnnounceUsedSpaceResponse { + // `AnnounceUsedSpaceResponse` has an empty body because announcements are + // one way communication. + message Body {} + + // Body of announce used space response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/protos/src/main/proto/container/types.proto b/protos/src/main/proto/container/types.proto new file mode 100644 index 0000000..fc523ca --- /dev/null +++ b/protos/src/main/proto/container/types.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +package neo.fs.v2.container; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container/grpc;container"; +option java_package = "frostfs.container"; + +import "netmap/types.proto"; +import "refs/types.proto"; + +// Container is a structure that defines object placement behaviour. Objects can +// be stored only within containers. They define placement rule, attributes and +// access control information. An ID of a container is a 32 byte long SHA256 +// hash of stable-marshalled container message. +message Container { + // Container format version. Effectively, the version of API library used to + // create the container. + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Identifier of the container owner + neo.fs.v2.refs.OwnerID owner_id = 2 [ json_name = "ownerID" ]; + + // Nonce is a 16 byte UUIDv4, used to avoid collisions of `ContainerID`s + bytes nonce = 3 [ json_name = "nonce" ]; + + // `BasicACL` contains access control rules for the owner, system and others + // groups, as well as permission bits for `BearerToken` and `Extended ACL` + uint32 basic_acl = 4 [ json_name = "basicACL" ]; + + // `Attribute` is a user-defined Key-Value metadata pair attached to the + // container. Container attributes are immutable. They are set at the moment + // of container creation and can never be added or updated. + // + // Key name must be a container-unique valid UTF-8 string. Value can't be + // empty. Containers with duplicated attribute names or attributes with empty + // values will be considered invalid. + // + // There are some "well-known" attributes affecting system behaviour: + // + // * [ __SYSTEM__NAME ] \ + // (`__NEOFS__NAME` is deprecated) \ + // String of a human-friendly container name registered as a domain in + // NNS contract. + // * [ __SYSTEM__ZONE ] \ + // (`__NEOFS__ZONE` is deprecated) \ + // String of a zone for `__SYSTEM__NAME` (`__NEOFS__NAME` is deprecated). + // Used as a TLD of a domain name in NNS contract. If no zone is specified, + // use default zone: `container`. + // * [ __SYSTEM__DISABLE_HOMOMORPHIC_HASHING ] \ + // (`__NEOFS__DISABLE_HOMOMORPHIC_HASHING` is deprecated) \ + // Disables homomorphic hashing for the container if the value equals "true" + // string. Any other values are interpreted as missing attribute. Container + // could be accepted in a NeoFS network only if the global network hashing + // configuration value corresponds with that attribute's value. After + // container inclusion, network setting is ignored. + // + // And some well-known attributes used by applications only: + // + // * Name \ + // Human-friendly name + // * Timestamp \ + // User-defined local time of container creation in Unix Timestamp format + message Attribute { + // Attribute name key + string key = 1 [ json_name = "key" ]; + + // Attribute value + string value = 2 [ json_name = "value" ]; + } + // Attributes represent immutable container's meta data + repeated Attribute attributes = 5 [ json_name = "attributes" ]; + + // Placement policy for the object inside the container + neo.fs.v2.netmap.PlacementPolicy placement_policy = 6 + [ json_name = "placementPolicy" ]; +} diff --git a/protos/src/main/proto/lock/types.proto b/protos/src/main/proto/lock/types.proto new file mode 100644 index 0000000..e4a8879 --- /dev/null +++ b/protos/src/main/proto/lock/types.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package neo.fs.v2.lock; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/lock/grpc;lock"; +option java_package = "frostfs.lock"; + +import "refs/types.proto"; + +// Lock objects protects a list of objects from being deleted. The lifetime of a +// lock object is limited similar to regular objects in +// `__SYSTEM__EXPIRATION_EPOCH` (`__NEOFS__EXPIRATION_EPOCH` is deprecated) +// attribute. Lock object MUST have expiration epoch. It is impossible to delete +// a lock object via ObjectService.Delete RPC call. +message Lock { + // List of objects to lock. Must not be empty or carry empty IDs. + // All members must be of the `REGULAR` type. + repeated neo.fs.v2.refs.ObjectID members = 1 [ json_name = "members" ]; +} diff --git a/protos/src/main/proto/netmap/service.proto b/protos/src/main/proto/netmap/service.proto new file mode 100644 index 0000000..7e97e09 --- /dev/null +++ b/protos/src/main/proto/netmap/service.proto @@ -0,0 +1,162 @@ +syntax = "proto3"; + +package neo.fs.v2.netmap; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap/grpc;netmap"; +option java_package = "frostfs.netmap"; + +import "netmap/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// `NetmapService` provides methods to work with `Network Map` and the +// information required to build it. The resulting `Network Map` is stored in +// sidechain `Netmap` smart contract, while related information can be obtained +// from other NeoFS nodes. +service NetmapService { + // Get NodeInfo structure from the particular node directly. + // Node information can be taken from `Netmap` smart contract. In some cases, + // though, one may want to get recent information directly or to talk to the + // node not yet present in the `Network Map` to find out what API version can + // be used for further communication. This can be also used to check if a node + // is up and running. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // information about the server has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc LocalNodeInfo(LocalNodeInfoRequest) returns (LocalNodeInfoResponse); + + // Read recent information about the NeoFS network. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // information about the current network state has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc NetworkInfo(NetworkInfoRequest) returns (NetworkInfoResponse); + + // Returns network map snapshot of the current NeoFS epoch. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // information about the current network map has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc NetmapSnapshot(NetmapSnapshotRequest) returns (NetmapSnapshotResponse); +} + +// Get NodeInfo structure directly from a particular node +message LocalNodeInfoRequest { + // LocalNodeInfo request body is empty. + message Body {} + // Body of the LocalNodeInfo request message + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Local Node Info, including API Version in use +message LocalNodeInfoResponse { + // Local Node Info, including API Version in use. + message Body { + // Latest NeoFS API version in use + neo.fs.v2.refs.Version version = 1; + + // NodeInfo structure with recent information from node itself + NodeInfo node_info = 2; + } + // Body of the balance response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect response execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get NetworkInfo structure with the network view from a particular node. +message NetworkInfoRequest { + // NetworkInfo request body is empty. + message Body {} + // Body of the NetworkInfo request message + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Response with NetworkInfo structure including current epoch and +// sidechain magic number. +message NetworkInfoResponse { + // Information about the network. + message Body { + // NetworkInfo structure with recent information. + NetworkInfo network_info = 1; + } + // Body of the NetworkInfo response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect response execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get netmap snapshot request +message NetmapSnapshotRequest { + // Get netmap snapshot request body. + message Body {} + + // Body of get netmap snapshot request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Response with current netmap snapshot +message NetmapSnapshotResponse { + // Get netmap snapshot response body + message Body { + // Structure of the requested network map. + Netmap netmap = 1 [ json_name = "netmap" ]; + } + + // Body of get netmap snapshot response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect response execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/protos/src/main/proto/netmap/types.proto b/protos/src/main/proto/netmap/types.proto new file mode 100644 index 0000000..3c311ba --- /dev/null +++ b/protos/src/main/proto/netmap/types.proto @@ -0,0 +1,323 @@ +syntax = "proto3"; + +package neo.fs.v2.netmap; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap/grpc;netmap"; +option java_package = "frostfs.netmap"; + +// Operations on filters +enum Operation { + // No Operation defined + OPERATION_UNSPECIFIED = 0; + + // Equal + EQ = 1; + + // Not Equal + NE = 2; + + // Greater then + GT = 3; + + // Greater or equal + GE = 4; + + // Less then + LT = 5; + + // Less or equal + LE = 6; + + // Logical OR + OR = 7; + + // Logical AND + AND = 8; + + // Logical negation + NOT = 9; +} + +// Selector modifier shows how the node set will be formed. By default selector +// just groups nodes into a bucket by attribute, selecting nodes only by their +// hash distance. +enum Clause { + // No modifier defined. Nodes will be selected from the bucket randomly + CLAUSE_UNSPECIFIED = 0; + + // SAME will select only nodes having the same value of bucket attribute + SAME = 1; + + // DISTINCT will select nodes having different values of bucket attribute + DISTINCT = 2; +} + +// This filter will return the subset of nodes from `NetworkMap` or another +// filter's results that will satisfy filter's conditions. +message Filter { + // Name of the filter or a reference to a named filter. '*' means + // application to the whole unfiltered NetworkMap. At top level it's used as a + // filter name. At lower levels it's considered to be a reference to another + // named filter + string name = 1 [ json_name = "name" ]; + + // Key to filter + string key = 2 [ json_name = "key" ]; + + // Filtering operation + Operation op = 3 [ json_name = "op" ]; + + // Value to match + string value = 4 [ json_name = "value" ]; + + // List of inner filters. Top level operation will be applied to the whole + // list. + repeated Filter filters = 5 [ json_name = "filters" ]; +} + +// Selector chooses a number of nodes from the bucket taking the nearest nodes +// to the provided `ContainerID` by hash distance. +message Selector { + // Selector name to reference in object placement section + string name = 1 [ json_name = "name" ]; + + // How many nodes to select from the bucket + uint32 count = 2 [ json_name = "count" ]; + + // Selector modifier showing how to form a bucket + Clause clause = 3 [ json_name = "clause" ]; + + // Bucket attribute to select from + string attribute = 4 [ json_name = "attribute" ]; + + // Filter reference to select from + string filter = 5 [ json_name = "filter" ]; +} + +// Number of object replicas in a set of nodes from the defined selector. If no +// selector set, the root bucket containing all possible nodes will be used by +// default. +message Replica { + // How many object replicas to put + uint32 count = 1 [ json_name = "count" ]; + + // Named selector bucket to put replicas + string selector = 2 [ json_name = "selector" ]; + + // Data shards count + uint32 ec_data_count = 3 [ json_name = "ecDataCount" ]; + + // Parity shards count + uint32 ec_parity_count = 4 [ json_name = "ecParityCount" ]; +} + +// Set of rules to select a subset of nodes from `NetworkMap` able to store +// container's objects. The format is simple enough to transpile from different +// storage policy definition languages. +message PlacementPolicy { + // Rules to set number of object replicas and place each one into a named + // bucket + repeated Replica replicas = 1 [ json_name = "replicas" ]; + + // Container backup factor controls how deep NeoFS will search for nodes + // alternatives to include into container's nodes subset + uint32 container_backup_factor = 2 [ json_name = "containerBackupFactor" ]; + + // Set of Selectors to form the container's nodes subset + repeated Selector selectors = 3 [ json_name = "selectors" ]; + + // List of named filters to reference in selectors + repeated Filter filters = 4 [ json_name = "filters" ]; + + // Unique flag defines non-overlapping application for replicas + bool unique = 5 [ json_name = "unique" ]; +} + +// NeoFS node description +message NodeInfo { + // Public key of the NeoFS node in a binary format + bytes public_key = 1 [ json_name = "publicKey" ]; + + // Ways to connect to a node + repeated string addresses = 2 [ json_name = "addresses" ]; + + // Administrator-defined Attributes of the NeoFS Storage Node. + // + // `Attribute` is a Key-Value metadata pair. Key name must be a valid UTF-8 + // string. Value can't be empty. + // + // Attributes can be constructed into a chain of attributes: any attribute can + // have a parent attribute and a child attribute (except the first and the + // last one). A string representation of the chain of attributes in NeoFS + // Storage Node configuration uses ":" and "/" symbols, e.g.: + // + // `NEOFS_NODE_ATTRIBUTE_1=key1:val1/key2:val2` + // + // Therefore the string attribute representation in the Node configuration + // must use "\:", "\/" and "\\" escaped symbols if any of them appears in an + // attribute's key or value. + // + // Node's attributes are mostly used during Storage Policy evaluation to + // calculate object's placement and find a set of nodes satisfying policy + // requirements. There are some "well-known" node attributes common to all the + // Storage Nodes in the network and used implicitly with default values if not + // explicitly set: + // + // * Capacity \ + // Total available disk space in Gigabytes. + // * Price \ + // Price in GAS tokens for storing one GB of data during one Epoch. In node + // attributes it's a string presenting floating point number with comma or + // point delimiter for decimal part. In the Network Map it will be saved as + // 64-bit unsigned integer representing number of minimal token fractions. + // * UN-LOCODE \ + // Node's geographic location in + // [UN/LOCODE](https://www.unece.org/cefact/codesfortrade/codes_index.html) + // format approximated to the nearest point defined in the standard. + // * CountryCode \ + // Country code in + // [ISO 3166-1_alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + // format. Calculated automatically from `UN-LOCODE` attribute. + // * Country \ + // Country short name in English, as defined in + // [ISO-3166](https://www.iso.org/obp/ui/#search). Calculated automatically + // from `UN-LOCODE` attribute. + // * Location \ + // Place names are given, whenever possible, in their national language + // versions as expressed in the Roman alphabet using the 26 characters of + // the character set adopted for international trade data interchange, + // written without diacritics . Calculated automatically from `UN-LOCODE` + // attribute. + // * SubDivCode \ + // Country's administrative subdivision where node is located. Calculated + // automatically from `UN-LOCODE` attribute based on `SubDiv` field. + // Presented in [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) + // format. + // * SubDiv \ + // Country's administrative subdivision name, as defined in + // [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2). Calculated + // automatically from `UN-LOCODE` attribute. + // * Continent \ + // Node's continent name according to the [Seven-Continent model] + // (https://en.wikipedia.org/wiki/Continent#Number). Calculated + // automatically from `UN-LOCODE` attribute. + // * ExternalAddr + // Node's preferred way for communications with external clients. + // Clients SHOULD use these addresses if possible. + // Must contain a comma-separated list of multi-addresses. + // + // For detailed description of each well-known attribute please see the + // corresponding section in NeoFS Technical Specification. + message Attribute { + // Key of the node attribute + string key = 1 [ json_name = "key" ]; + + // Value of the node attribute + string value = 2 [ json_name = "value" ]; + + // Parent keys, if any. For example for `City` it could be `Region` and + // `Country`. + repeated string parents = 3 [ json_name = "parents" ]; + } + // Carries list of the NeoFS node attributes in a key-value form. Key name + // must be a node-unique valid UTF-8 string. Value can't be empty. NodeInfo + // structures with duplicated attribute names or attributes with empty values + // will be considered invalid. + repeated Attribute attributes = 3 [ json_name = "attributes" ]; + + // Represents the enumeration of various states of the NeoFS node. + enum State { + // Unknown state + UNSPECIFIED = 0; + + // Active state in the network + ONLINE = 1; + + // Network unavailable state + OFFLINE = 2; + + // Maintenance state + MAINTENANCE = 3; + } + + // Carries state of the NeoFS node + State state = 4 [ json_name = "state" ]; +} + +// Network map structure +message Netmap { + // Network map revision number. + uint64 epoch = 1 [ json_name = "epoch" ]; + + // Nodes presented in network. + repeated NodeInfo nodes = 2 [ json_name = "nodes" ]; +} + +// NeoFS network configuration +message NetworkConfig { + // Single configuration parameter. Key MUST be network-unique. + // + // System parameters: + // - **AuditFee** \ + // Fee paid by the storage group owner to the Inner Ring member. + // Value: little-endian integer. Default: 0. + // - **BasicIncomeRate** \ + // Cost of storing one gigabyte of data for a period of one epoch. Paid by + // container owner to container nodes. + // Value: little-endian integer. Default: 0. + // - **ContainerAliasFee** \ + // Fee paid for named container's creation by the container owner. + // Value: little-endian integer. Default: 0. + // - **ContainerFee** \ + // Fee paid for container creation by the container owner. + // Value: little-endian integer. Default: 0. + // - **EpochDuration** \ + // NeoFS epoch duration measured in Sidechain blocks. + // Value: little-endian integer. Default: 0. + // - **HomomorphicHashingDisabled** \ + // Flag of disabling the homomorphic hashing of objects' payload. + // Value: true if any byte != 0. Default: false. + // - **InnerRingCandidateFee** \ + // Fee for entrance to the Inner Ring paid by the candidate. + // Value: little-endian integer. Default: 0. + // - **MaintenanceModeAllowed** \ + // Flag allowing setting the MAINTENANCE state to storage nodes. + // Value: true if any byte != 0. Default: false. + // - **MaxObjectSize** \ + // Maximum size of physically stored NeoFS object measured in bytes. + // Value: little-endian integer. Default: 0. + // - **WithdrawFee** \ + // Fee paid for withdrawal of funds paid by the account owner. + // Value: little-endian integer. Default: 0. + // - **MaxECDataCount** \ + // Maximum number of data shards for EC placement policy. + // Value: little-endian integer. Default: 0. + // - **MaxECParityCount** \ + // Maximum number of parity shards for EC placement policy. + // Value: little-endian integer. Default: 0. + message Parameter { + // Parameter key. UTF-8 encoded string + bytes key = 1 [ json_name = "key" ]; + + // Parameter value + bytes value = 2 [ json_name = "value" ]; + } + // List of parameter values + repeated Parameter parameters = 1 [ json_name = "parameters" ]; +} + +// Information about NeoFS network +message NetworkInfo { + // Number of the current epoch in the NeoFS network + uint64 current_epoch = 1 [ json_name = "currentEpoch" ]; + + // Magic number of the sidechain of the NeoFS network + uint64 magic_number = 2 [ json_name = "magicNumber" ]; + + // MillisecondsPerBlock network parameter of the sidechain of the NeoFS + // network + int64 ms_per_block = 3 [ json_name = "msPerBlock" ]; + + // NeoFS network configuration + NetworkConfig network_config = 4 [ json_name = "networkConfig" ]; +} diff --git a/protos/src/main/proto/object/service.proto b/protos/src/main/proto/object/service.proto new file mode 100644 index 0000000..7635f8a --- /dev/null +++ b/protos/src/main/proto/object/service.proto @@ -0,0 +1,816 @@ +syntax = "proto3"; + +package neo.fs.v2.object; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object/grpc;object"; +option java_package = "frostfs.object"; + +import "object/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// `ObjectService` provides API for manipulating objects. Object operations do +// not affect the sidechain and are only served by nodes in p2p style. +service ObjectService { + // Receive full object structure, including Headers and payload. Response uses + // gRPC stream. First response message carries the object with the requested + // address. Chunk messages are parts of the object's payload if it is needed. + // All messages, except the first one, carry payload chunks. The requested + // object can be restored by concatenation of object message payload and all + // chunks keeping the receiving order. + // + // Extended headers can change `Get` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requsted version of Network Map for object placement + // calculation. + // * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ + // (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ + // Will try older versions (starting from `__SYSTEM__NETMAP_EPOCH` + // (`__NEOFS__NETMAP_EPOCH` is deprecated) if specified or the latest one + // otherwise) of Network Map to find an object until the depth limit is + // reached. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // read access to the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Get(GetRequest) returns (stream GetResponse); + + // Put the object into container. Request uses gRPC stream. First message + // SHOULD be of PutHeader type. `ContainerID` and `OwnerID` of an object + // SHOULD be set. Session token SHOULD be obtained before `PUT` operation (see + // session package). Chunk messages are considered by server as a part of an + // object payload. All messages, except first one, SHOULD be payload chunks. + // Chunk messages SHOULD be sent in the direct order of fragmentation. + // + // Extended headers can change `Put` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requsted version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully saved in the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // write access to the container is denied; + // - **LOCKED** (2050, SECTION_OBJECT): \ + // placement of an object of type TOMBSTONE that includes at least one + // locked object is prohibited; + // - **LOCK_NON_REGULAR_OBJECT** (2051, SECTION_OBJECT): \ + // placement of an object of type LOCK that includes at least one object of + // type other than REGULAR is prohibited; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object storage container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_NOT_FOUND** (4096, SECTION_SESSION): \ + // (for trusted object preparation) session private key does not exist or + // has + // been deleted; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Put(stream PutRequest) returns (PutResponse); + + // Delete the object from a container. There is no immediate removal + // guarantee. Object will be marked for removal and deleted eventually. + // + // Extended headers can change `Delete` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully marked to be removed from the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // delete access to the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // the object could not be deleted because it has not been \ + // found within the container; + // - **LOCKED** (2050, SECTION_OBJECT): \ + // deleting a locked object is prohibited; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Delete(DeleteRequest) returns (DeleteResponse); + + // Returns the object Headers without data payload. By default full header is + // returned. If `main_only` request field is set, the short header with only + // the very minimal information will be returned instead. + // + // Extended headers can change `Head` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object header has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation HEAD of the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Head(HeadRequest) returns (HeadResponse); + + // Search objects in container. Search query allows to match by Object + // Header's filed values. Please see the corresponding NeoFS Technical + // Specification section for more details. + // + // Extended headers can change `Search` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // objects have been successfully selected; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation SEARCH of the object is denied; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // search container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Search(SearchRequest) returns (stream SearchResponse); + + // Get byte range of data payload. Range is set as an (offset, length) tuple. + // Like in `Get` method, the response uses gRPC stream. Requested range can be + // restored by concatenation of all received payload chunks keeping the + // receiving order. + // + // Extended headers can change `GetRange` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ + // (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ + // Will try older versions of Network Map to find an object until the depth + // limit is reached. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // data range of the object payload has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation RANGE of the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted. + // - **OUT_OF_RANGE** (2053, SECTION_OBJECT): \ + // the requested range is out of bounds; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc GetRange(GetRangeRequest) returns (stream GetRangeResponse); + + // Returns homomorphic or regular hash of object's payload range after + // applying XOR operation with the provided `salt`. Ranges are set of (offset, + // length) tuples. Hashes order in response corresponds to the ranges order in + // the request. Note that hash is calculated for XORed data. + // + // Extended headers can change `GetRangeHash` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ + // (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ + // Will try older versions of Network Map to find an object until the depth + // limit is reached. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // data range of the object payload has been successfully hashed; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation RANGEHASH of the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OUT_OF_RANGE** (2053, SECTION_OBJECT): \ + // the requested range is out of bounds; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc GetRangeHash(GetRangeHashRequest) returns (GetRangeHashResponse); + + // Put the prepared object into container. + // `ContainerID`, `ObjectID`, `OwnerID`, `PayloadHash` and `PayloadLength` of + // an object MUST be set. + // + // Extended headers can change `Put` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully saved in the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // write access to the container is denied; + // - **LOCKED** (2050, SECTION_OBJECT): \ + // placement of an object of type TOMBSTONE that includes at least one + // locked object is prohibited; + // - **LOCK_NON_REGULAR_OBJECT** (2051, SECTION_OBJECT): \ + // placement of an object of type LOCK that includes at least one object of + // type other than REGULAR is prohibited; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object storage container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_NOT_FOUND** (4096, SECTION_SESSION): \ + // (for trusted object preparation) session private key does not exist or + // has + // been deleted; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc PutSingle(PutSingleRequest) returns (PutSingleResponse); +} + +// GET object request +message GetRequest { + // GET Object request body + message Body { + // Address of the requested object + neo.fs.v2.refs.Address address = 1; + + // If `raw` flag is set, request will work only with objects that are + // physically stored on the peer node + bool raw = 2; + } + // Body of get object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// GET object response +message GetResponse { + // GET Object Response body + message Body { + // Initial part of the `Object` structure stream. Technically it's a + // set of all `Object` structure's fields except `payload`. + message Init { + // Object's unique identifier. + neo.fs.v2.refs.ObjectID object_id = 1; + + // Signed `ObjectID` + neo.fs.v2.refs.Signature signature = 2; + + // Object metadata headers + Header header = 3; + } + // Single message in the response stream. + oneof object_part { + // Initial part of the object stream + Init init = 1; + + // Chunked object payload + bytes chunk = 2; + + // Meta information of split hierarchy for object assembly. + SplitInfo split_info = 3; + + // Meta information for EC object assembly. + ECInfo ec_info = 4; + } + } + // Body of get object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// PUT object request +message PutRequest { + // PUT request body + message Body { + // Newly created object structure parameters. If some optional parameters + // are not set, they will be calculated by a peer node. + message Init { + // ObjectID if available. + neo.fs.v2.refs.ObjectID object_id = 1; + + // Object signature if available + neo.fs.v2.refs.Signature signature = 2; + + // Object's Header + Header header = 3; + + // Number of copies of the object to store within the RPC call. By + // default, object is processed according to the container's placement + // policy. Can be one of: + // 1. A single number; applied to the whole request and is treated as + // a minimal number of nodes that must store an object to complete the + // request successfully. + // 2. An ordered array; every number is treated as a minimal number of + // nodes in a corresponding placement vector that must store an object + // to complete the request successfully. The length MUST equal the + // placement vectors number, otherwise request is considered malformed. + repeated uint32 copies_number = 4; + } + // Single message in the request stream. + oneof object_part { + // Initial part of the object stream + Init init = 1; + + // Chunked object payload + bytes chunk = 2; + } + } + // Body of put object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// PUT Object response +message PutResponse { + // PUT Object response body + message Body { + // Identifier of the saved object + neo.fs.v2.refs.ObjectID object_id = 1; + } + // Body of put object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object DELETE request +message DeleteRequest { + // Object DELETE request body + message Body { + // Address of the object to be deleted + neo.fs.v2.refs.Address address = 1; + } + // Body of delete object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// DeleteResponse body is empty because we cannot guarantee permanent object +// removal in distributed system. +message DeleteResponse { + // Object DELETE Response has an empty body. + message Body { + // Address of the tombstone created for the deleted object + neo.fs.v2.refs.Address tombstone = 1; + } + + // Body of delete object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object HEAD request +message HeadRequest { + // Object HEAD request body + message Body { + // Address of the object with the requested Header + neo.fs.v2.refs.Address address = 1; + + // Return only minimal header subset + bool main_only = 2; + + // If `raw` flag is set, request will work only with objects that are + // physically stored on the peer node + bool raw = 3; + } + // Body of head object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Tuple of a full object header and signature of an `ObjectID`. \ +// Signed `ObjectID` is present to verify full header's authenticity through the +// following steps: +// +// 1. Calculate `SHA-256` of the marshalled `Header` structure +// 2. Check if the resulting hash matches `ObjectID` +// 3. Check if `ObjectID` signature in `signature` field is correct +message HeaderWithSignature { + // Full object header + Header header = 1 [ json_name = "header" ]; + + // Signed `ObjectID` to verify full header's authenticity + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; +} + +// Object HEAD response +message HeadResponse { + // Object HEAD response body + message Body { + // Requested object header, it's part or meta information about split + // object. + oneof head { + // Full object's `Header` with `ObjectID` signature + HeaderWithSignature header = 1; + + // Short object header + ShortHeader short_header = 2; + + // Meta information of split hierarchy. + SplitInfo split_info = 3; + + // Meta information for EC object assembly. + ECInfo ec_info = 4; + } + } + // Body of head object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object Search request +message SearchRequest { + // Object Search request body + message Body { + // Container identifier were to search + neo.fs.v2.refs.ContainerID container_id = 1; + + // Version of the Query Language used + uint32 version = 2; + // Filter structure checks if the object header field or the attribute + // content matches a value. + // + // If no filters are set, search request will return all objects of the + // container, including Regular object and Tombstone + // objects. Most human users expect to get only object they can directly + // work with. In that case, `$Object:ROOT` filter should be used. + // + // By default `key` field refers to the corresponding object's `Attribute`. + // Some Object's header fields can also be accessed by adding `$Object:` + // prefix to the name. Here is the list of fields available via this prefix: + // + // * $Object:version \ + // version + // * $Object:objectID \ + // object_id + // * $Object:containerID \ + // container_id + // * $Object:ownerID \ + // owner_id + // * $Object:creationEpoch \ + // creation_epoch + // * $Object:payloadLength \ + // payload_length + // * $Object:payloadHash \ + // payload_hash + // * $Object:objectType \ + // object_type + // * $Object:homomorphicHash \ + // homomorphic_hash + // * $Object:split.parent \ + // object_id of parent + // * $Object:split.splitID \ + // 16 byte UUIDv4 used to identify the split object hierarchy parts + // + // There are some well-known filter aliases to match objects by certain + // properties: + // + // * $Object:ROOT \ + // Returns only `REGULAR` type objects that are not split or that are the + // top level root objects in a split hierarchy. This includes objects not + // present physically, like large objects split into smaller objects + // without a separate top-level root object. Objects of other types like + // Locks and Tombstones will not be shown. This filter may be + // useful for listing objects like `ls` command of some virtual file + // system. This filter is activated if the `key` exists, disregarding the + // value and matcher type. + // * $Object:PHY \ + // Returns only objects physically stored in the system. This filter is + // activated if the `key` exists, disregarding the value and matcher type. + // + // Note: using filters with a key with prefix `$Object:` and match type + // `NOT_PRESENT `is not recommended since this is not a cross-version + // approach. Behavior when processing this kind of filters is undefined. + message Filter { + // Match type to use + MatchType match_type = 1 [ json_name = "matchType" ]; + + // Attribute or Header fields to match + string key = 2 [ json_name = "key" ]; + + // Value to match + string value = 3 [ json_name = "value" ]; + } + // List of search expressions + repeated Filter filters = 3; + } + // Body of search object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Search response +message SearchResponse { + // Object Search response body + message Body { + // List of `ObjectID`s that match the search query + repeated neo.fs.v2.refs.ObjectID id_list = 1; + } + // Body of search object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object payload range.Ranges of zero length SHOULD be considered as invalid. +message Range { + // Offset of the range from the object payload start + uint64 offset = 1; + + // Length in bytes of the object payload range + uint64 length = 2; +} + +// Request part of object's payload +message GetRangeRequest { + // Byte range of object's payload request body + message Body { + // Address of the object containing the requested payload range + neo.fs.v2.refs.Address address = 1; + + // Requested payload range + Range range = 2; + + // If `raw` flag is set, request will work only with objects that are + // physically stored on the peer node. + bool raw = 3; + } + + // Body of get range object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get part of object's payload +message GetRangeResponse { + // Get Range response body uses streams to transfer the response. Because + // object payload considered a byte sequence, there is no need to have some + // initial preamble message. The requested byte range is sent as a series + // chunks. + message Body { + // Requested object range or meta information about split object. + oneof range_part { + // Chunked object payload's range. + bytes chunk = 1; + + // Meta information of split hierarchy. + SplitInfo split_info = 2; + + // Meta information for EC object assembly. + ECInfo ec_info = 3; + } + } + + // Body of get range object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get hash of object's payload part +message GetRangeHashRequest { + // Get hash of object's payload part request body. + message Body { + // Address of the object that containing the requested payload range + neo.fs.v2.refs.Address address = 1; + + // List of object's payload ranges to calculate homomorphic hash + repeated Range ranges = 2; + + // Binary salt to XOR object's payload ranges before hash calculation + bytes salt = 3; + + // Checksum algorithm type + neo.fs.v2.refs.ChecksumType type = 4; + } + // Body of get range hash object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get hash of object's payload part +message GetRangeHashResponse { + // Get hash of object's payload part response body. + message Body { + // Checksum algorithm type + neo.fs.v2.refs.ChecksumType type = 1; + + // List of range hashes in a binary format + repeated bytes hash_list = 2; + } + // Body of get range hash object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object PUT Single request +message PutSingleRequest { + // PUT Single request body + message Body { + // Prepared object with payload. + Object object = 1; + // Number of copies of the object to store within the RPC call. By default, + // object is processed according to the container's placement policy. + // Every number is treated as a minimal number of + // nodes in a corresponding placement vector that must store an object + // to complete the request successfully. The length MUST equal the placement + // vectors number, otherwise request is considered malformed. + repeated uint32 copies_number = 2; + } + // Body of put single object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Object PUT Single response +message PutSingleResponse { + // PUT Single Object response body + message Body {} + // Body of put single object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} \ No newline at end of file diff --git a/protos/src/main/proto/object/types.proto b/protos/src/main/proto/object/types.proto new file mode 100644 index 0000000..623c2a3 --- /dev/null +++ b/protos/src/main/proto/object/types.proto @@ -0,0 +1,266 @@ +syntax = "proto3"; + +package neo.fs.v2.object; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object/grpc;object"; +option java_package = "frostfs.object"; + +import "refs/types.proto"; +import "session/types.proto"; + +// Type of the object payload content. Only `REGULAR` type objects can be split, +// hence `TOMBSTONE` and `LOCK` payload is limited by the +// maximum object size. +// +// String presentation of object type is the same as definition: +// * REGULAR +// * TOMBSTONE +// * LOCK +enum ObjectType { + // Just a normal object + REGULAR = 0; + + // Used internally to identify deleted objects + TOMBSTONE = 1; + + // Unused (previously storageGroup information) + // _ = 2; + + // Object lock + LOCK = 3; +} + +// Type of match expression +enum MatchType { + // Unknown. Not used + MATCH_TYPE_UNSPECIFIED = 0; + + // Full string match + STRING_EQUAL = 1; + + // Full string mismatch + STRING_NOT_EQUAL = 2; + + // Lack of key + NOT_PRESENT = 3; + + // String prefix match + COMMON_PREFIX = 4; +} + +// Short header fields +message ShortHeader { + // Object format version. Effectively, the version of API library used to + // create particular object. + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Epoch when the object was created + uint64 creation_epoch = 2 [ json_name = "creationEpoch" ]; + + // Object's owner + neo.fs.v2.refs.OwnerID owner_id = 3 [ json_name = "ownerID" ]; + + // Type of the object payload content + ObjectType object_type = 4 [ json_name = "objectType" ]; + + // Size of payload in bytes. + // `0xFFFFFFFFFFFFFFFF` means `payload_length` is unknown + uint64 payload_length = 5 [ json_name = "payloadLength" ]; + + // Hash of payload bytes + neo.fs.v2.refs.Checksum payload_hash = 6 [ json_name = "payloadHash" ]; + + // Homomorphic hash of the object payload + neo.fs.v2.refs.Checksum homomorphic_hash = 7 + [ json_name = "homomorphicHash" ]; +} + +// Object Header +message Header { + // Object format version. Effectively, the version of API library used to + // create particular object + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Object's container + neo.fs.v2.refs.ContainerID container_id = 2 [ json_name = "containerID" ]; + + // Object's owner + neo.fs.v2.refs.OwnerID owner_id = 3 [ json_name = "ownerID" ]; + + // Object creation Epoch + uint64 creation_epoch = 4 [ json_name = "creationEpoch" ]; + + // Size of payload in bytes. + // `0xFFFFFFFFFFFFFFFF` means `payload_length` is unknown. + uint64 payload_length = 5 [ json_name = "payloadLength" ]; + + // Hash of payload bytes + neo.fs.v2.refs.Checksum payload_hash = 6 [ json_name = "payloadHash" ]; + + // Type of the object payload content + ObjectType object_type = 7 [ json_name = "objectType" ]; + + // Homomorphic hash of the object payload + neo.fs.v2.refs.Checksum homomorphic_hash = 8 + [ json_name = "homomorphicHash" ]; + + // Session token, if it was used during Object creation. Need it to verify + // integrity and authenticity out of Request scope. + neo.fs.v2.session.SessionToken session_token = 9 + [ json_name = "sessionToken" ]; + + // `Attribute` is a user-defined Key-Value metadata pair attached to an + // object. + // + // Key name must be an object-unique valid UTF-8 string. Value can't be empty. + // Objects with duplicated attribute names or attributes with empty values + // will be considered invalid. + // + // There are some "well-known" attributes starting with `__SYSTEM__` + // (`__NEOFS__` is deprecated) prefix that affect system behaviour: + // + // * [ __SYSTEM__UPLOAD_ID ] \ + // (`__NEOFS__UPLOAD_ID` is deprecated) \ + // Marks smaller parts of a split bigger object + // * [ __SYSTEM__EXPIRATION_EPOCH ] \ + // (`__NEOFS__EXPIRATION_EPOCH` is deprecated) \ + // The epoch after which object with no LOCKs on it becomes unavailable. + // Locked object continues to be available until each of the LOCKs expire. + // * [ __SYSTEM__TICK_EPOCH ] \ + // (`__NEOFS__TICK_EPOCH` is deprecated) \ + // Decimal number that defines what epoch must produce + // object notification with UTF-8 object address in a + // body (`0` value produces notification right after + // object put) + // * [ __SYSTEM__TICK_TOPIC ] \ + // (`__NEOFS__TICK_TOPIC` is deprecated) \ + // UTF-8 string topic ID that is used for object notification + // + // And some well-known attributes used by applications only: + // + // * Name \ + // Human-friendly name + // * FileName \ + // File name to be associated with the object on saving + // * FilePath \ + // Full path to be associated with the object on saving. Should start with a + // '/' and use '/' as a delimiting symbol. Trailing '/' should be + // interpreted as a virtual directory marker. If an object has conflicting + // FilePath and FileName, FilePath should have higher priority, because it + // is used to construct the directory tree. FilePath with trailing '/' and + // non-empty FileName attribute should not be used together. + // * Timestamp \ + // User-defined local time of object creation in Unix Timestamp format + // * Content-Type \ + // MIME Content Type of object's payload + // + // For detailed description of each well-known attribute please see the + // corresponding section in NeoFS Technical Specification. + message Attribute { + // string key to the object attribute + string key = 1 [ json_name = "key" ]; + // string value of the object attribute + string value = 2 [ json_name = "value" ]; + } + // User-defined object attributes + repeated Attribute attributes = 10 [ json_name = "attributes" ]; + + // Bigger objects can be split into a chain of smaller objects. Information + // about inter-dependencies between spawned objects and how to re-construct + // the original one is in the `Split` headers. Parent and children objects + // must be within the same container. + message Split { + // Identifier of the origin object. Known only to the minor child. + neo.fs.v2.refs.ObjectID parent = 1 [ json_name = "parent" ]; + + // Identifier of the left split neighbor + neo.fs.v2.refs.ObjectID previous = 2 [ json_name = "previous" ]; + + // `signature` field of the parent object. Used to reconstruct parent. + neo.fs.v2.refs.Signature parent_signature = 3 + [ json_name = "parentSignature" ]; + + // `header` field of the parent object. Used to reconstruct parent. + Header parent_header = 4 [ json_name = "parentHeader" ]; + + // List of identifiers of the objects generated by splitting current one. + repeated neo.fs.v2.refs.ObjectID children = 5 [ json_name = "children" ]; + + // 16 byte UUIDv4 used to identify the split object hierarchy parts. Must be + // unique inside container. All objects participating in the split must have + // the same `split_id` value. + bytes split_id = 6 [ json_name = "splitID" ]; + } + // Position of the object in the split hierarchy + Split split = 11 [ json_name = "split" ]; + + // Erasure code can be applied to any object. + // Information about encoded object structure is stored in `EC` header. + // All objects belonging to a single EC group have the same `parent` field. + message EC { + // Identifier of the origin object. Known to all chunks. + neo.fs.v2.refs.ObjectID parent = 1 [ json_name = "parent" ]; + // Index of this chunk. + uint32 index = 2 [ json_name = "index" ]; + // Total number of chunks in this split. + uint32 total = 3 [ json_name = "total" ]; + // Total length of a parent header. Used to trim padding zeroes. + uint32 header_length = 4 [ json_name = "headerLength" ]; + // Chunk of a parent header. + bytes header = 5 [ json_name = "header" ]; + } + // Erasure code chunk information. + EC ec = 12 [ json_name = "ec" ]; +} + +// Object structure. Object is immutable and content-addressed. It means +// `ObjectID` will change if the header or the payload changes. It's calculated +// as a hash of header field which contains hash of the object's payload. +// +// For non-regular object types payload format depends on object type specified +// in the header. +message Object { + // Object's unique identifier. + neo.fs.v2.refs.ObjectID object_id = 1 [ json_name = "objectID" ]; + + // Signed object_id + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; + + // Object metadata headers + Header header = 3 [ json_name = "header" ]; + + // Payload bytes + bytes payload = 4 [ json_name = "payload" ]; +} + +// Meta information of split hierarchy for object assembly. With the last part +// one can traverse linked list of split hierarchy back to the first part and +// assemble the original object. With a linking object one can assemble an +// object right from the object parts. +message SplitInfo { + // 16 byte UUID used to identify the split object hierarchy parts. + bytes split_id = 1; + + // The identifier of the last object in split hierarchy parts. It contains + // split header with the original object header. + neo.fs.v2.refs.ObjectID last_part = 2; + + // The identifier of a linking object for split hierarchy parts. It contains + // split header with the original object header and a sorted list of + // object parts. + neo.fs.v2.refs.ObjectID link = 3; +} + +// Meta information for the erasure-encoded object. +message ECInfo { + message Chunk { + // Object ID of the chunk. + neo.fs.v2.refs.ObjectID id = 1; + // Index of the chunk. + uint32 index = 2; + // Total number of chunks in this split. + uint32 total = 3; + } + // Chunk stored on the node. + repeated Chunk chunks = 1; +} \ No newline at end of file diff --git a/protos/src/main/proto/refs/types.proto b/protos/src/main/proto/refs/types.proto new file mode 100644 index 0000000..0ed5840 --- /dev/null +++ b/protos/src/main/proto/refs/types.proto @@ -0,0 +1,150 @@ +syntax = "proto3"; + +package neo.fs.v2.refs; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs/grpc;refs"; +option java_package = "frostfs.refs"; + +// Objects in NeoFS are addressed by their ContainerID and ObjectID. +// +// String presentation of `Address` is a concatenation of string encoded +// `ContainerID` and `ObjectID` delimited by '/' character. +message Address { + // Container identifier + ContainerID container_id = 1 [ json_name = "containerID" ]; + // Object identifier + ObjectID object_id = 2 [ json_name = "objectID" ]; +} + +// NeoFS Object unique identifier. Objects are immutable and content-addressed. +// It means `ObjectID` will change if the `header` or the `payload` changes. +// +// `ObjectID` is a 32 byte long +// [SHA256](https://csrc.nist.gov/publications/detail/fips/180/4/final) hash of +// the object's `header` field, which, in it's turn, contains the hash of the +// object's payload. +// +// String presentation is a +// [base58](https://tools.ietf.org/html/draft-msporny-base58-02) encoded string. +// +// JSON value will be data encoded as a string using standard base64 +// encoding with paddings. Either +// [standard](https://tools.ietf.org/html/rfc4648#section-4) or +// [URL-safe](https://tools.ietf.org/html/rfc4648#section-5) base64 encoding +// with/without paddings are accepted. +message ObjectID { + // Object identifier in a binary format + bytes value = 1 [ json_name = "value" ]; +} + +// NeoFS container identifier. Container structures are immutable and +// content-addressed. +// +// `ContainerID` is a 32 byte long +// [SHA256](https://csrc.nist.gov/publications/detail/fips/180/4/final) hash of +// stable-marshalled container message. +// +// String presentation is a +// [base58](https://tools.ietf.org/html/draft-msporny-base58-02) encoded string. +// +// JSON value will be data encoded as a string using standard base64 +// encoding with paddings. Either +// [standard](https://tools.ietf.org/html/rfc4648#section-4) or +// [URL-safe](https://tools.ietf.org/html/rfc4648#section-5) base64 encoding +// with/without paddings are accepted. +message ContainerID { + // Container identifier in a binary format. + bytes value = 1 [ json_name = "value" ]; +} + +// `OwnerID` is a derivative of a user's main public key. The transformation +// algorithm is the same as for Neo3 wallet addresses. Neo3 wallet address can +// be directly used as `OwnerID`. +// +// `OwnerID` is a 25 bytes sequence starting with Neo version prefix byte +// followed by 20 bytes of ScrptHash and 4 bytes of checksum. +// +// String presentation is a [Base58 +// Check](https://en.bitcoin.it/wiki/Base58Check_encoding) Encoded string. +// +// JSON value will be data encoded as a string using standard base64 +// encoding with paddings. Either +// [standard](https://tools.ietf.org/html/rfc4648#section-4) or +// [URL-safe](https://tools.ietf.org/html/rfc4648#section-5) base64 encoding +// with/without paddings are accepted. +message OwnerID { + // Identifier of the container owner in a binary format + bytes value = 1 [ json_name = "value" ]; +} + +// API version used by a node. +// +// String presentation is a Semantic Versioning 2.0.0 compatible version string +// with 'v' prefix. i.e. `vX.Y`, where `X` is the major number, `Y` is the minor +// number. +message Version { + // Major API version + uint32 major = 1 [ json_name = "major" ]; + + // Minor API version + uint32 minor = 2 [ json_name = "minor" ]; +} + +// Signature of something in NeoFS. +message Signature { + // Public key used for signing + bytes key = 1 [ json_name = "key" ]; + // Signature + bytes sign = 2 [ json_name = "signature" ]; + // Scheme contains digital signature scheme identifier + SignatureScheme scheme = 3 [ json_name = "scheme" ]; +} + +// Signature scheme describes digital signing scheme used for (key, signature) +// pair. +enum SignatureScheme { + // ECDSA with SHA-512 hashing (FIPS 186-3) + ECDSA_SHA512 = 0; + + // Deterministic ECDSA with SHA-256 hashing (RFC 6979) + ECDSA_RFC6979_SHA256 = 1; + + // Deterministic ECDSA with SHA-256 hashing using WalletConnect API. + // Here the algorithm is the same, but the message format differs. + ECDSA_RFC6979_SHA256_WALLET_CONNECT = 2; +} + +// RFC 6979 signature. +message SignatureRFC6979 { + // Public key used for signing + bytes key = 1 [ json_name = "key" ]; + // Deterministic ECDSA with SHA-256 hashing + bytes sign = 2 [ json_name = "signature" ]; +} + +// Checksum algorithm type. +enum ChecksumType { + // Unknown. Not used + CHECKSUM_TYPE_UNSPECIFIED = 0; + + // Tillich-Zemor homomorphic hash function + TZ = 1; + + // SHA-256 + SHA256 = 2; +} + +// Checksum message. +// Depending on checksum algorithm type, the string presentation may vary: +// +// * TZ \ +// Hex encoded string without `0x` prefix +// * SHA256 \ +// Hex encoded string without `0x` prefix +message Checksum { + // Checksum algorithm type + ChecksumType type = 1 [ json_name = "type" ]; + + // Checksum itself + bytes sum = 2 [ json_name = "sum" ]; +} diff --git a/protos/src/main/proto/session/service.proto b/protos/src/main/proto/session/service.proto new file mode 100644 index 0000000..b31bd8e --- /dev/null +++ b/protos/src/main/proto/session/service.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package neo.fs.v2.session; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session/grpc;session"; +option java_package = "frostfs.session"; + +import "refs/types.proto"; +import "session/types.proto"; + +// `SessionService` allows to establish a temporary trust relationship between +// two peer nodes and generate a `SessionToken` as the proof of trust to be +// attached in requests for further verification. Please see corresponding +// section of NeoFS Technical Specification for details. +service SessionService { + // Open a new session between two peers. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // session has been successfully opened; + // - Common failures (SECTION_FAILURE_COMMON). + rpc Create(CreateRequest) returns (CreateResponse); +} + +// Information necessary for opening a session. +message CreateRequest { + // Session creation request body + message Body { + // Session initiating user's or node's key derived `OwnerID` + neo.fs.v2.refs.OwnerID owner_id = 1; + // Session expiration `Epoch` + uint64 expiration = 2; + } + // Body of a create session token request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Information about the opened session. +message CreateResponse { + // Session creation response body + message Body { + // Identifier of a newly created session + bytes id = 1; + + // Public key used for session + bytes session_key = 2; + } + + // Body of create session token response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/protos/src/main/proto/session/types.proto b/protos/src/main/proto/session/types.proto new file mode 100644 index 0000000..9f5259a --- /dev/null +++ b/protos/src/main/proto/session/types.proto @@ -0,0 +1,238 @@ +syntax = "proto3"; + +package neo.fs.v2.session; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session/grpc;session"; +option java_package = "frostfs.session"; + +import "refs/types.proto"; +import "acl/types.proto"; +import "status/types.proto"; + +// Context information for Session Tokens related to ObjectService requests +message ObjectSessionContext { + // Object request verbs + enum Verb { + // Unknown verb + VERB_UNSPECIFIED = 0; + + // Refers to object.Put RPC call + PUT = 1; + + // Refers to object.Get RPC call + GET = 2; + + // Refers to object.Head RPC call + HEAD = 3; + + // Refers to object.Search RPC call + SEARCH = 4; + + // Refers to object.Delete RPC call + DELETE = 5; + + // Refers to object.GetRange RPC call + RANGE = 6; + + // Refers to object.GetRangeHash RPC call + RANGEHASH = 7; + } + // Type of request for which the token is issued + Verb verb = 1 [ json_name = "verb" ]; + + // Carries objects involved in the object session. + message Target { + // Indicates which container the session is spread to. Field MUST be set + // and correct. + refs.ContainerID container = 1 [ json_name = "container" ]; + + // Indicates which objects the session is spread to. Objects are expected + // to be stored in the NeoFS container referenced by `container` field. + // Each element MUST have correct format. + repeated refs.ObjectID objects = 2 [ json_name = "objects" ]; + } + // Object session target. MUST be correctly formed and set. If `objects` + // field is not empty, then the session applies only to these elements, + // otherwise, to all objects from the specified container. + Target target = 2 [ json_name = "target" ]; +} + +// Context information for Session Tokens related to ContainerService requests. +message ContainerSessionContext { + // Container request verbs + enum Verb { + // Unknown verb + VERB_UNSPECIFIED = 0; + + // Refers to container.Put RPC call + PUT = 1; + + // Refers to container.Delete RPC call + DELETE = 2; + + // Refers to container.SetExtendedACL RPC call + SETEACL = 3; + } + // Type of request for which the token is issued + Verb verb = 1 [ json_name = "verb" ]; + + // Spreads the action to all owner containers. + // If set, container_id field is ignored. + bool wildcard = 2 [ json_name = "wildcard" ]; + + // Particular container to which the action applies. + // Ignored if wildcard flag is set. + refs.ContainerID container_id = 3 [ json_name = "containerID" ]; +} + +// NeoFS Session Token. +message SessionToken { + // Session Token body + message Body { + // Token identifier is a valid UUIDv4 in binary form + bytes id = 1 [ json_name = "id" ]; + + // Identifier of the session initiator + neo.fs.v2.refs.OwnerID owner_id = 2 [ json_name = "ownerID" ]; + + // Lifetime parameters of the token. Field names taken from rfc7519. + message TokenLifetime { + // Expiration Epoch + uint64 exp = 1 [ json_name = "exp" ]; + + // Not valid before Epoch + uint64 nbf = 2 [ json_name = "nbf" ]; + + // Issued at Epoch + uint64 iat = 3 [ json_name = "iat" ]; + } + // Lifetime of the session + TokenLifetime lifetime = 3 [ json_name = "lifetime" ]; + + // Public key used in session + bytes session_key = 4 [ json_name = "sessionKey" ]; + + // Session Context information + oneof context { + // ObjectService session context + ObjectSessionContext object = 5 [ json_name = "object" ]; + + // ContainerService session context + ContainerSessionContext container = 6 [ json_name = "container" ]; + } + } + // Session Token contains the proof of trust between peers to be attached in + // requests for further verification. Please see corresponding section of + // NeoFS Technical Specification for details. + Body body = 1 [ json_name = "body" ]; + + // Signature of `SessionToken` information + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; +} + +// Extended headers for Request/Response. They may contain any user-defined +// headers to be interpreted on application level. +// +// Key name must be a unique valid UTF-8 string. Value can't be empty. Requests +// or Responses with duplicated header names or headers with empty values will +// be considered invalid. +// +// There are some "well-known" headers starting with `__SYSTEM__` (`__NEOFS__` +// is deprecated) prefix that affect system behaviour: +// +// * [ __SYSTEM__NETMAP_EPOCH ] \ +// (`__NEOFS__NETMAP_EPOCH` is deprecated) \ +// Netmap epoch to use for object placement calculation. The `value` is string +// encoded `uint64` in decimal presentation. If set to '0' or not set, the +// current epoch only will be used. +// * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ +// (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ +// If object can't be found using current epoch's netmap, this header limits +// how many past epochs the node can look up through. The `value` is string +// encoded `uint64` in decimal presentation. If set to '0' or not set, only +// the current epoch will be used. +message XHeader { + // Key of the X-Header + string key = 1 [ json_name = "key" ]; + + // Value of the X-Header + string value = 2 [ json_name = "value" ]; +} + +// Meta information attached to the request. When forwarded between peers, +// request meta headers are folded in matryoshka style. +message RequestMetaHeader { + // Peer's API version used + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Peer's local epoch number. Set to 0 if unknown. + uint64 epoch = 2 [ json_name = "epoch" ]; + + // Maximum number of intermediate nodes in the request route + uint32 ttl = 3 [ json_name = "ttl" ]; + + // Request X-Headers + repeated XHeader x_headers = 4 [ json_name = "xHeaders" ]; + + // Session token within which the request is sent + SessionToken session_token = 5 [ json_name = "sessionToken" ]; + + // `BearerToken` with eACL overrides for the request + neo.fs.v2.acl.BearerToken bearer_token = 6 [ json_name = "bearerToken" ]; + + // `RequestMetaHeader` of the origin request + RequestMetaHeader origin = 7 [ json_name = "origin" ]; + + // NeoFS network magic. Must match the value for the network + // that the server belongs to. + uint64 magic_number = 8 [ json_name = "magicNumber" ]; +} + +// Information about the response +message ResponseMetaHeader { + // Peer's API version used + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Peer's local epoch number + uint64 epoch = 2 [ json_name = "epoch" ]; + + // Maximum number of intermediate nodes in the request route + uint32 ttl = 3 [ json_name = "ttl" ]; + + // Response X-Headers + repeated XHeader x_headers = 4 [ json_name = "xHeaders" ]; + + // `ResponseMetaHeader` of the origin request + ResponseMetaHeader origin = 5 [ json_name = "origin" ]; + + // Status return + neo.fs.v2.status.Status status = 6 [ json_name = "status" ]; +} + +// Verification info for the request signed by all intermediate nodes. +message RequestVerificationHeader { + // Request Body signature. Should be generated once by the request initiator. + neo.fs.v2.refs.Signature body_signature = 1 [ json_name = "bodySignature" ]; + // Request Meta signature is added and signed by each intermediate node + neo.fs.v2.refs.Signature meta_signature = 2 [ json_name = "metaSignature" ]; + // Signature of previous hops + neo.fs.v2.refs.Signature origin_signature = 3 + [ json_name = "originSignature" ]; + + // Chain of previous hops signatures + RequestVerificationHeader origin = 4 [ json_name = "origin" ]; +} + +// Verification info for the response signed by all intermediate nodes +message ResponseVerificationHeader { + // Response Body signature. Should be generated once by an answering node. + neo.fs.v2.refs.Signature body_signature = 1 [ json_name = "bodySignature" ]; + // Response Meta signature is added and signed by each intermediate node + neo.fs.v2.refs.Signature meta_signature = 2 [ json_name = "metaSignature" ]; + // Signature of previous hops + neo.fs.v2.refs.Signature origin_signature = 3 + [ json_name = "originSignature" ]; + + // Chain of previous hops signatures + ResponseVerificationHeader origin = 4 [ json_name = "origin" ]; +} diff --git a/protos/src/main/proto/status/types.proto b/protos/src/main/proto/status/types.proto new file mode 100644 index 0000000..7d8f8e9 --- /dev/null +++ b/protos/src/main/proto/status/types.proto @@ -0,0 +1,157 @@ +syntax = "proto3"; + +package neo.fs.v2.status; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status/grpc;status"; +option java_package = "frostfs.status"; + +// Declares the general format of the status returns of the NeoFS RPC protocol. +// Status is present in all response messages. Each RPC of NeoFS protocol +// describes the possible outcomes and details of the operation. +// +// Each status is assigned a one-to-one numeric code. Any unique result of an +// operation in NeoFS is unambiguously associated with the code value. +// +// Numerical set of codes is split into 1024-element sections. An enumeration +// is defined for each section. Values can be referred to in the following ways: +// +// * numerical value ranging from 0 to 4,294,967,295 (global code); +// +// * values from enumeration (local code). The formula for the ratio of the +// local code (`L`) of a defined section (`S`) to the global one (`G`): +// `G = 1024 * S + L`. +// +// All outcomes are divided into successful and failed, which corresponds +// to the success or failure of the operation. The definition of success +// follows the semantics of RPC and the description of its purpose. +// The server must not attach code that is the opposite of the outcome type. +// +// See the set of return codes in the description for calls. +// +// Each status can carry a developer-facing error message. It should be a human +// readable text in English. The server should not transmit (and the client +// should not expect) useful information in the message. Field `details` +// should make the return more detailed. +message Status { + // The status code + uint32 code = 1; + + // Developer-facing error message + string message = 2; + + // Return detail. It contains additional information that can be used to + // analyze the response. Each code defines a set of details that can be + // attached to a status. Client should not handle details that are not + // covered by the code. + message Detail { + // Detail ID. The identifier is required to determine the binary format + // of the detail and how to decode it. + uint32 id = 1; + + // Binary status detail. Must follow the format associated with ID. + // The possibility of missing a value must be explicitly allowed. + bytes value = 2; + } + + // Data detailing the outcome of the operation. Must be unique by ID. + repeated Detail details = 3; +} + +// Section identifiers. +enum Section { + // Successful return codes. + SECTION_SUCCESS = 0; + + // Failure codes regardless of the operation. + SECTION_FAILURE_COMMON = 1; + + // Object service-specific errors. + SECTION_OBJECT = 2; + + // Container service-specific errors. + SECTION_CONTAINER = 3; + + // Session service-specific errors. + SECTION_SESSION = 4; + + // Session service-specific errors. + SECTION_APE_MANAGER = 5; +} + +// Section of NeoFS successful return codes. +enum Success { + // [**0**] Default success. Not detailed. + // If the server cannot match successful outcome to the code, it should + // use this code. + OK = 0; +} + +// Section of failed statuses independent of the operation. +enum CommonFail { + // [**1024**] Internal server error, default failure. Not detailed. + // If the server cannot match failed outcome to the code, it should + // use this code. + INTERNAL = 0; + + // [**1025**] Wrong magic of the NeoFS network. + // Details: + // - [**0**] Magic number of the served NeoFS network (big-endian 64-bit + // unsigned integer). + WRONG_MAGIC_NUMBER = 1; + + // [**1026**] Signature verification failure. + SIGNATURE_VERIFICATION_FAIL = 2; + + // [**1027**] Node is under maintenance. + NODE_UNDER_MAINTENANCE = 3; +} + +// Section of statuses for object-related operations. +enum Object { + // [**2048**] Access denied by ACL. + // Details: + // - [**0**] Human-readable description (UTF-8 encoded string). + ACCESS_DENIED = 0; + + // [**2049**] Object not found. + OBJECT_NOT_FOUND = 1; + + // [**2050**] Operation rejected by the object lock. + LOCKED = 2; + + // [**2051**] Locking an object with a non-REGULAR type rejected. + LOCK_NON_REGULAR_OBJECT = 3; + + // [**2052**] Object has been marked deleted. + OBJECT_ALREADY_REMOVED = 4; + + // [**2053**] Invalid range has been requested for an object. + OUT_OF_RANGE = 5; +} + +// Section of statuses for container-related operations. +enum Container { + // [**3072**] Container not found. + CONTAINER_NOT_FOUND = 0; + + // [**3073**] eACL table not found. + EACL_NOT_FOUND = 1; + + // [**3074**] Container access denied. + CONTAINER_ACCESS_DENIED = 2; +} + +// Section of statuses for session-related operations. +enum Session { + // [**4096**] Token not found. + TOKEN_NOT_FOUND = 0; + + // [**4097**] Token has expired. + TOKEN_EXPIRED = 1; +} + +// Section of status for APE manager related operations. +enum APEManager { + // [**5120**] The operation is denied by APE manager. + APE_MANAGER_ACCESS_DENIED = 0; +} \ No newline at end of file diff --git a/protos/src/main/proto/tombstone/types.proto b/protos/src/main/proto/tombstone/types.proto new file mode 100644 index 0000000..9128160 --- /dev/null +++ b/protos/src/main/proto/tombstone/types.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package neo.fs.v2.tombstone; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/tombstone/grpc;tombstone"; +option java_package = "frostfs.tombstone"; + +import "refs/types.proto"; + +// Tombstone keeps record of deleted objects for a few epochs until they are +// purged from the NeoFS network. +message Tombstone { + // Last NeoFS epoch number of the tombstone lifetime. It's set by the + // tombstone creator depending on the current NeoFS network settings. A + // tombstone object must have the same expiration epoch value in + // `__SYSTEM__EXPIRATION_EPOCH` (`__NEOFS__EXPIRATION_EPOCH` is deprecated) + // attribute. Otherwise, the tombstone will be rejected by a storage node. + uint64 expiration_epoch = 1 [ json_name = "expirationEpoch" ]; + + // 16 byte UUID used to identify the split object hierarchy parts. Must be + // unique inside a container. All objects participating in the split must + // have the same `split_id` value. + bytes split_id = 2 [ json_name = "splitID" ]; + + // List of objects to be deleted. + repeated neo.fs.v2.refs.ObjectID members = 3 [ json_name = "members" ]; +}