diff --git a/Q-Apps.md b/Q-Apps.md index 33bf3325..9a6e47b9 100644 --- a/Q-Apps.md +++ b/Q-Apps.md @@ -588,19 +588,9 @@ Publishing an in-development app to mainnet isn't recommended. There are several ### Preview mode -All read-only operations can be tested using preview mode. It can be used as follows: - -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. +Select "Preview" in the UI after choosing the zip. This allows for full Q-App testing without the need to publish any data. -### 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). \ No newline at end of file +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). \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 9569017c..79efc55f 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -38,6 +38,7 @@ import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata; import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Service; import org.qortal.controller.Controller; +import org.qortal.controller.arbitrary.ArbitraryDataRenderManager; import org.qortal.controller.arbitrary.ArbitraryDataStorageManager; import org.qortal.controller.arbitrary.ArbitraryMetadataManager; import org.qortal.data.account.AccountData; @@ -777,6 +778,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String path) { Security.checkApiCallAllowed(request); @@ -785,7 +787,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false, - title, description, tags, category); + title, description, tags, category, preview); } @POST @@ -822,6 +824,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String path) { Security.checkApiCallAllowed(request); @@ -830,7 +833,7 @@ public class ArbitraryResource { } 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("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String base64) { Security.checkApiCallAllowed(request); @@ -876,7 +880,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false, - title, description, tags, category); + title, description, tags, category, preview); } @POST @@ -911,6 +915,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String base64) { Security.checkApiCallAllowed(request); @@ -919,7 +924,7 @@ public class ArbitraryResource { } 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("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String base64Zip) { Security.checkApiCallAllowed(request); @@ -964,7 +970,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true, - title, description, tags, category); + title, description, tags, category, preview); } @POST @@ -999,6 +1005,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String base64Zip) { Security.checkApiCallAllowed(request); @@ -1007,7 +1014,7 @@ public class ArbitraryResource { } 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("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String string) { Security.checkApiCallAllowed(request); @@ -1055,7 +1063,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false, - title, description, tags, category); + title, description, tags, category, preview); } @POST @@ -1092,6 +1100,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("preview") Boolean preview, String string) { Security.checkApiCallAllowed(request); @@ -1100,15 +1109,48 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false, - title, description, tags, category); + title, description, tags, category, preview); } // 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, String path, String string, String base64, boolean zipped, - String title, String description, List tags, Category category) { + String title, String description, List tags, Category category, + Boolean preview) { // Fetch public key from registered name try (final Repository repository = RepositoryManager.getRepository()) { 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 { ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder( repository, publicKey58, Paths.get(path), name, null, service, identifier, diff --git a/src/main/java/org/qortal/api/restricted/resource/RenderResource.java b/src/main/java/org/qortal/api/restricted/resource/RenderResource.java index 95360419..53c56f7b 100644 --- a/src/main/java/org/qortal/api/restricted/resource/RenderResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/RenderResource.java @@ -42,64 +42,6 @@ public class RenderResource { @Context HttpServletResponse response; @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 @Path("/authorize/{resourceId}") @SecurityRequirement(name = "apiKey")