Browse Source

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.
qdn
CalDescent 3 years ago
parent
commit
d0aafaee60
  1. 102
      src/main/java/org/qortal/api/resource/ArbitraryResource.java
  2. 2
      src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java
  3. 39
      tools/qdata

102
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());
}
}

2
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.");

39
tools/qdata

@ -8,7 +8,8 @@ if [ -z "$*" ]; then
echo "Usage:"
echo
echo "Host/update data:"
echo "qdata POST [service] [name] [dirpath] <identifier>"
echo "qdata POST [service] [name] PATH [dirpath] <identifier>"
echo "qdata POST [service] [name] STRING [data-string] <identifier>"
echo
echo "Fetch data:"
echo "qdata GET [service] [name] <identifier-or-default> <filepath-or-default> <rebuild>"
@ -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..."

Loading…
Cancel
Save