Rework of preview mode.

All /arbitrary endpoints responsible for publishing data now support an optional "preview" query string parameter. If true, these endpoints will return a URL path to open the preview, rather than returning transaction bytes.
This commit is contained in:
CalDescent 2023-03-03 14:20:45 +00:00
parent 8e2dd60ea0
commit d166f625d0
3 changed files with 59 additions and 80 deletions

View File

@ -588,19 +588,9 @@ Publishing an in-development app to mainnet isn't recommended. There are several
### Preview mode ### Preview mode
All read-only operations can be tested using preview mode. It can be used as follows: Select "Preview" in the UI after choosing the zip. This allows for full Q-App testing without the need to publish any data.
1. Ensure Qortal core is running locally on the machine you are developing on. Previewing via a remote node is not currently possible.
2. Make a local API call to `POST /render/preview`, passing in the API key (found in apikey.txt), and the path to the root of your Q-App, for example:
```
curl -X POST "http://localhost:12391/render/preview" -H "X-API-KEY: apiKeyGoesHere" -d "/home/username/Websites/MyApp"
```
3. This returns a URL, which can be copied and pasted into a browser to view the preview
4. Modify the Q-App as required, then repeat from step 2 to generate a new preview URL
This is a short term method until preview functionality has been implemented within the UI.
### Single node testnet ### Testnets
For full read/write testing of a Q-App, you can set up a single node testnet (often referred to as devnet) on your local machine. See [Single Node Testnet Quick Start Guide](TestNets.md#quick-start). For an end-to-end test of Q-App publishing, you can use the official testnet, or set up a single node testnet of your own (often referred to as devnet) on your local machine. See [Single Node Testnet Quick Start Guide](TestNets.md#quick-start).

View File

@ -38,6 +38,7 @@ import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.controller.arbitrary.ArbitraryDataRenderManager;
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager; import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
import org.qortal.controller.arbitrary.ArbitraryMetadataManager; import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
import org.qortal.data.account.AccountData; import org.qortal.data.account.AccountData;
@ -777,6 +778,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String path) { String path) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -785,7 +787,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false, return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false,
title, description, tags, category); title, description, tags, category, preview);
} }
@POST @POST
@ -822,6 +824,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String path) { String path) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -830,7 +833,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false, return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false,
title, description, tags, category); title, description, tags, category, preview);
} }
@ -868,6 +871,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String base64) { String base64) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -876,7 +880,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false, return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false,
title, description, tags, category); title, description, tags, category, preview);
} }
@POST @POST
@ -911,6 +915,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String base64) { String base64) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -919,7 +924,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false, return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false,
title, description, tags, category); title, description, tags, category, preview);
} }
@ -956,6 +961,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String base64Zip) { String base64Zip) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -964,7 +970,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true, return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true,
title, description, tags, category); title, description, tags, category, preview);
} }
@POST @POST
@ -999,6 +1005,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String base64Zip) { String base64Zip) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -1007,7 +1014,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true, return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true,
title, description, tags, category); title, description, tags, category, preview);
} }
@ -1047,6 +1054,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String string) { String string) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -1055,7 +1063,7 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false, return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false,
title, description, tags, category); title, description, tags, category, preview);
} }
@POST @POST
@ -1092,6 +1100,7 @@ public class ArbitraryResource {
@QueryParam("description") String description, @QueryParam("description") String description,
@QueryParam("tags") List<String> tags, @QueryParam("tags") List<String> tags,
@QueryParam("category") Category category, @QueryParam("category") Category category,
@QueryParam("preview") Boolean preview,
String string) { String string) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
@ -1100,15 +1109,48 @@ public class ArbitraryResource {
} }
return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false, return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false,
title, description, tags, category); title, description, tags, category, preview);
} }
// Shared methods // Shared methods
private String preview(String directoryPath, Service service) {
Security.checkApiCallAllowed(request);
ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.PUT;
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP;
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(directoryPath),
null, service, null, method, compression,
null, null, null, null);
try {
arbitraryDataWriter.save();
} catch (IOException | DataException | InterruptedException | MissingDataException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
} catch (RuntimeException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
}
ArbitraryDataFile arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile();
if (arbitraryDataFile != null) {
String digest58 = arbitraryDataFile.digest58();
if (digest58 != null) {
// Pre-authorize resource
ArbitraryDataResource resource = new ArbitraryDataResource(digest58, null, null, null);
ArbitraryDataRenderManager.getInstance().addToAuthorizedResources(resource);
return "/render/hash/" + digest58 + "?secret=" + Base58.encode(arbitraryDataFile.getSecret());
}
}
return "Unable to generate preview URL";
}
private String upload(Service service, String name, String identifier, private String upload(Service service, String name, String identifier,
String path, String string, String base64, boolean zipped, String path, String string, String base64, boolean zipped,
String title, String description, List<String> tags, Category category) { String title, String description, List<String> tags, Category category,
Boolean preview) {
// Fetch public key from registered name // Fetch public key from registered name
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
NameData nameData = repository.getNameRepository().fromName(name); NameData nameData = repository.getNameRepository().fromName(name);
@ -1171,6 +1213,11 @@ public class ArbitraryResource {
} }
} }
// Finish here if user has requested a preview
if (preview != null && preview == true) {
return this.preview(path, service);
}
try { try {
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder( ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
repository, publicKey58, Paths.get(path), name, null, service, identifier, repository, publicKey58, Paths.get(path), name, null, service, identifier,

View File

@ -42,64 +42,6 @@ public class RenderResource {
@Context HttpServletResponse response; @Context HttpServletResponse response;
@Context ServletContext context; @Context ServletContext context;
@POST
@Path("/preview")
@Operation(
summary = "Generate preview URL based on a user-supplied path and service",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "/Users/user/Documents/MyStaticWebsite"
)
)
),
responses = {
@ApiResponse(
description = "a temporary URL to preview the website",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@SecurityRequirement(name = "apiKey")
public String preview(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String directoryPath) {
Security.checkApiCallAllowed(request);
Method method = Method.PUT;
Compression compression = Compression.ZIP;
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(directoryPath),
null, Service.WEBSITE, null, method, compression,
null, null, null, null);
try {
arbitraryDataWriter.save();
} catch (IOException | DataException | InterruptedException | MissingDataException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
} catch (RuntimeException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
}
ArbitraryDataFile arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile();
if (arbitraryDataFile != null) {
String digest58 = arbitraryDataFile.digest58();
if (digest58 != null) {
// Pre-authorize resource
ArbitraryDataResource resource = new ArbitraryDataResource(digest58, null, null, null);
ArbitraryDataRenderManager.getInstance().addToAuthorizedResources(resource);
return "http://localhost:12391/render/hash/" + digest58 + "?secret=" + Base58.encode(arbitraryDataFile.getSecret());
}
}
return "Unable to generate preview URL";
}
@POST @POST
@Path("/authorize/{resourceId}") @Path("/authorize/{resourceId}")
@SecurityRequirement(name = "apiKey") @SecurityRequirement(name = "apiKey")