From d0aafaee60a3e3a53982e24f6fd20c6e7bce8229 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 17 Nov 2021 18:57:46 +0000 Subject: [PATCH] Added POST /arbitrary/../string API endpoints to allow data to be passed to the core as a string. This will be useful for metadata, playlists, etc, as well as some types of data published by Qortal apps. --- .../api/resource/ArbitraryResource.java | 102 ++++++++++++++++-- .../ArbitraryDataTransactionBuilder.java | 2 +- tools/qdata | 39 +++++-- 3 files changed, 128 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 9a076c0a..90a687c0 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -10,6 +10,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -265,7 +269,6 @@ public class ArbitraryResource { @Path("/{service}/{name}") @Operation( summary = "Build raw, unsigned, ARBITRARY transaction, based on a user-supplied path", - description = "A POST transaction automatically selects a PUT or PATCH method based on the data supplied", requestBody = @RequestBody( required = true, content = @Content( @@ -293,14 +296,48 @@ public class ArbitraryResource { String path) { Security.checkApiCallAllowed(request); - return this.upload(null, Service.valueOf(serviceString), name, null, path); + return this.upload(null, Service.valueOf(serviceString), name, null, path, null); } + @POST + @Path("/{service}/{name}/string") + @Operation( + summary = "Build raw, unsigned, ARBITRARY transaction, based on a user-supplied string", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", example = "{\"title\":\"\", \"description\":\"\", \"tags\":[]}" + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, ARBITRARY transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @SecurityRequirement(name = "apiKey") + public String postString(@PathParam("service") String serviceString, + @PathParam("name") String name, + String string) { + Security.checkApiCallAllowed(request); + + return this.upload(null, Service.valueOf(serviceString), name, null, null, string); + } + + @POST @Path("/{service}/{name}/{identifier}") @Operation( summary = "Build raw, unsigned, ARBITRARY transaction, based on a user-supplied path", - description = "A POST transaction automatically selects a PUT or PATCH method based on the data supplied", requestBody = @RequestBody( required = true, content = @Content( @@ -329,10 +366,45 @@ public class ArbitraryResource { String path) { Security.checkApiCallAllowed(request); - return this.upload(null, Service.valueOf(serviceString), name, identifier, path); + return this.upload(null, Service.valueOf(serviceString), name, identifier, path, null); } - private String upload(Method method, Service service, String name, String identifier, String path) { + @POST + @Path("/{service}/{name}/{identifier}/string") + @Operation( + summary = "Build raw, unsigned, ARBITRARY transaction, based on user supplied string", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", example = "{\"title\":\"\", \"description\":\"\", \"tags\":[]}" + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, ARBITRARY transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @SecurityRequirement(name = "apiKey") + public String postString(@PathParam("service") String serviceString, + @PathParam("name") String name, + @PathParam("identifier") String identifier, + String string) { + Security.checkApiCallAllowed(request); + + return this.upload(null, Service.valueOf(serviceString), name, identifier, null, string); + } + + private String upload(Method method, Service service, String name, String identifier, String path, String string) { // Fetch public key from registered name try (final Repository repository = RepositoryManager.getRepository()) { NameData nameData = repository.getNameRepository().fromName(name); @@ -348,6 +420,22 @@ public class ArbitraryResource { byte[] publicKey = accountData.getPublicKey(); String publicKey58 = Base58.encode(publicKey); + if (path == null) { + // See if we have a string instead + if (string != null) { + File tempFile = File.createTempFile("qortal-", ".tmp"); + tempFile.deleteOnExit(); + BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString())); + writer.write(string); + writer.newLine(); + writer.close(); + path = tempFile.toPath().toString(); + } + else { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Missing path or data string"); + } + } + try { ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder( publicKey58, Paths.get(path), name, method, service, identifier @@ -361,8 +449,8 @@ public class ArbitraryResource { throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage()); } - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); + } catch (DataException | IOException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage()); } } diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java index da90a61a..fceab925 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java @@ -125,7 +125,7 @@ public class ArbitraryDataTransactionBuilder { catch (IOException | DataException | MissingDataException | IllegalStateException e) { // Handle matching states separately, as it's best to block transactions with duplicate states if (e.getMessage().equals("Current state matches previous state. Nothing to do.")) { - throw new DataException(e); + throw new DataException(e.getMessage()); } LOGGER.info("Caught exception: {}", e.getMessage()); LOGGER.info("Unable to load existing resource - using PUT to overwrite it."); diff --git a/tools/qdata b/tools/qdata index 55e69f30..4004eb16 100755 --- a/tools/qdata +++ b/tools/qdata @@ -8,7 +8,8 @@ if [ -z "$*" ]; then echo "Usage:" echo echo "Host/update data:" - echo "qdata POST [service] [name] [dirpath] " + echo "qdata POST [service] [name] PATH [dirpath] " + echo "qdata POST [service] [name] STRING [data-string] " echo echo "Fetch data:" echo "qdata GET [service] [name] " @@ -37,20 +38,44 @@ fi if [[ "${method}" == "POST" ]]; then - directory=$4 - identifier=$5 - - if [ -z "${directory}" ]; then - echo "Error: missing directory"; exit + type=$4 + data=$5 + identifier=$6 + + if [ -z "${data}" ]; then + if [[ "${type}" == "PATH" ]]; then + echo "Error: missing directory"; exit + elif [[ "${type}" == "STRING" ]]; then + echo "Error: missing data string"; exit + else + echo "Error: unrecognized type"; exit + fi fi if [ -z "${QORTAL_PRIVKEY}" ]; then echo "Error: missing private key. Set it by running: export QORTAL_PRIVKEY=privkeyhere"; exit fi + # Create identifier component in URL + if [[ -z "${identifier}" || "${identifier}" == "default" ]]; then + identifier_component="" + else + identifier_component="/${identifier}" + fi + + # Create type component in URL + if [[ "${type}" == "PATH" ]]; then + type_component="" + elif [[ "${type}" == "STRING" ]]; then + type_component="/string" + fi + echo "Creating transaction - this can take a while..." - tx_data=$(curl --silent --insecure -X ${method} "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}" -d "${directory}") + tx_data=$(curl --silent --insecure -X ${method} "http://${host}:${port}/arbitrary/${service}/${name}${identifier_component}${type_component}" -d "${data}") + if [[ "${tx_data}" == *"error"* || "${tx_data}" == *"ERROR"* ]]; then echo "${tx_data}"; exit + elif [ -z "${tx_data}" ]; then + echo "Error: no transaction data returned"; exit fi echo "Signing..."