diff --git a/Plan/common/build.gradle b/Plan/common/build.gradle index 7d35f62cf..2ad19f3ed 100644 --- a/Plan/common/build.gradle +++ b/Plan/common/build.gradle @@ -120,6 +120,8 @@ task copyYarnBuildResults { } task determineAssetModifications { + dependsOn yarnBundle + inputs.files(fileTree("$rootDir/react/dashboard/build")) inputs.files(fileTree(dir: 'src/main/resources/assets/plan/web')) inputs.files(fileTree(dir: 'src/main/resources/assets/plan/locale')) outputs.file("build/resources/main/assets/plan/AssetVersion.yml") @@ -139,7 +141,7 @@ task determineAssetModifications { // git returns UNIX time in seconds, but most things in Java use UNIX time in milliseconds def modified = gitModifiedAsString.isEmpty() ? System.currentTimeMillis() : Long.parseLong(gitModifiedAsString) * 1000 def relativePath = tree.getDir().toPath().relativize(f.toPath()) // File path relative to the tree - versionFile.text += String.format( // writing YAML as raw text probably isn't the best idea + versionFile.text += String.format( "%s: %s\n", relativePath.toString().replace('.', ','), modified ) } @@ -154,7 +156,16 @@ task determineAssetModifications { // git returns UNIX time in seconds, but most things in Java use UNIX time in milliseconds def modified = gitModifiedAsString.isEmpty() ? System.currentTimeMillis() : Long.parseLong(gitModifiedAsString) * 1000 def relativePath = tree.getDir().toPath().relativize(f.toPath()) // File path relative to the tree - versionFile.text += String.format( // writing YAML as raw text probably isn't the best idea + versionFile.text += String.format( + "%s: %s\n", relativePath.toString().replace('.', ','), modified + ) + } + + tree = fileTree("$rootDir/react/dashboard/build") + tree.forEach { File f -> + def modified = System.currentTimeMillis() + def relativePath = tree.getDir().toPath().relativize(f.toPath()) // File path relative to the tree + versionFile.text += String.format( "%s: %s\n", relativePath.toString().replace('.', ','), modified ) } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/Exporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/Exporter.java index 6976b6f66..7f9a67c3b 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/Exporter.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/Exporter.java @@ -47,6 +47,7 @@ public class Exporter extends FileExporter { private final NetworkPageExporter networkPageExporter; private final Set failedServers; + private final ReactExporter reactExporter; @Inject public Exporter( @@ -55,7 +56,8 @@ public class Exporter extends FileExporter { PlayerPageExporter playerPageExporter, PlayersPageExporter playersPageExporter, ServerPageExporter serverPageExporter, - NetworkPageExporter networkPageExporter + NetworkPageExporter networkPageExporter, + ReactExporter reactExporter ) { this.config = config; this.playerJSONExporter = playerJSONExporter; @@ -63,6 +65,7 @@ public class Exporter extends FileExporter { this.playersPageExporter = playersPageExporter; this.serverPageExporter = serverPageExporter; this.networkPageExporter = networkPageExporter; + this.reactExporter = reactExporter; failedServers = new HashSet<>(); } @@ -161,4 +164,14 @@ public class Exporter extends FileExporter { throw new ExportException("Failed to export player: " + playerName + ", " + e.toString(), e); } } + + public void exportReact() throws ExportException { + Path toDirectory = config.getPageExportPath(); + + try { + reactExporter.exportReactFiles(toDirectory); + } catch (IOException e) { + throw new ExportException("Failed to export react: " + e.toString(), e); + } + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/FileExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/FileExporter.java index 905146236..248dd6fd9 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/FileExporter.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/FileExporter.java @@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.export; import com.djrapitops.plan.delivery.rendering.html.Html; import com.djrapitops.plan.delivery.web.resource.WebResource; +import com.djrapitops.plan.storage.file.Resource; import org.apache.commons.lang3.StringUtils; import java.io.ByteArrayInputStream; @@ -60,9 +61,15 @@ abstract class FileExporter { export(to, Arrays.asList(StringUtils.split(content, "\r\n"))); } + void export(Path to, Resource resource) throws IOException { + export(to, resource.asWebResource()); + } + void export(Path to, WebResource resource) throws IOException { Path dir = to.getParent(); - if (!Files.isSymbolicLink(dir)) Files.createDirectories(dir); + if (!Files.isSymbolicLink(dir) && !Files.isDirectory(dir)) { + Files.createDirectories(dir); + } try ( InputStream in = resource.asStream(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java new file mode 100644 index 000000000..834eda354 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java @@ -0,0 +1,92 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.delivery.export; + +import com.djrapitops.plan.delivery.web.AssetVersions; +import com.djrapitops.plan.settings.config.PlanConfig; +import com.djrapitops.plan.settings.config.paths.WebserverSettings; +import com.djrapitops.plan.storage.file.PlanFiles; +import com.djrapitops.plan.storage.file.Resource; +import org.apache.commons.lang3.StringUtils; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Exporter in charge of exporting React related files. + * + * @author AuroraLS3 + */ +@Singleton +public class ReactExporter extends FileExporter { + + private final PlanFiles files; + private final PlanConfig config; + private final AssetVersions assetVersions; + + @Inject + public ReactExporter( + PlanFiles files, + PlanConfig config, + AssetVersions assetVersions + ) { + this.files = files; + this.config = config; + this.assetVersions = assetVersions; + } + + public void exportReactFiles(Path toDirectory) throws IOException { + exportAsset(toDirectory, "index.html"); + exportAsset(toDirectory, "asset-manifest.json"); + exportAsset(toDirectory, "favicon.ico"); + exportAsset(toDirectory, "logo192.png"); + exportAsset(toDirectory, "logo512.png"); + exportAsset(toDirectory, "manifest.json"); + exportAsset(toDirectory, "robots.txt"); + exportStaticBundle(toDirectory); + } + + private void exportStaticBundle(Path toDirectory) throws IOException { + List paths = assetVersions.getAssetPaths().stream() + .filter(path -> path.contains("static")) + .collect(Collectors.toList()); + for (String path : paths) { + String resourcePath = path.replace(',', '.'); + Path to = toDirectory.resolve(resourcePath); + Resource resource = files.getResourceFromJar("web/" + resourcePath); + if (resourcePath.endsWith(".js")) { + String withReplacedConstants = StringUtils.replaceEach( + resource.asString(), + new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION"}, + new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true"} + ); + export(to, withReplacedConstants); + } else { + export(to, resource); + } + } + } + + private void exportAsset(Path toDirectory, String asset) throws IOException { + export(toDirectory.resolve(asset), files.getResourceFromJar("web/" + asset)); + } + +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java index 29de242a2..a0a47f948 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java @@ -24,6 +24,7 @@ import com.djrapitops.plan.storage.file.PlanFiles; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; +import java.util.List; import java.util.Optional; @Singleton @@ -61,4 +62,9 @@ public class AssetVersions { return Optional.of(max); } + + public List getAssetPaths() throws IOException { + if (webAssetConfig == null) prepare(); + return webAssetConfig.getConfigPaths(); + } } diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java new file mode 100644 index 000000000..bec1fe1a0 --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java @@ -0,0 +1,66 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.delivery.export; + +import com.djrapitops.plan.PlanSystem; +import com.djrapitops.plan.exceptions.ExportException; +import com.djrapitops.plan.settings.config.PlanConfig; +import extension.FullSystemExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(FullSystemExtension.class) +class ReactExporterTest { + + @BeforeAll + static void enableSystem(PlanSystem system) { + system.enable(); + } + + @AfterAll + static void disableSystem(PlanSystem system) { + system.disable(); + } + + @Test + void allReactFilesAreExported(PlanConfig config, Exporter exporter) throws ExportException, IOException { + Path exportPath = config.getPageExportPath(); + exporter.exportReact(); + + Path reactBuildPath = Path.of(new File("").getAbsolutePath()) + .resolve("../react/dashboard/build"); + + List filesToExport = Files.list(reactBuildPath) + .map(path -> path.relativize(reactBuildPath)) + .collect(Collectors.toList()); + List filesExported = Files.list(exportPath) + .map(path -> path.relativize(exportPath)) + .collect(Collectors.toList()); + assertEquals(filesToExport, filesExported); + } +} \ No newline at end of file diff --git a/Plan/common/src/test/java/extension/FullSystemExtension.java b/Plan/common/src/test/java/extension/FullSystemExtension.java index 43f081516..c5674dcfa 100644 --- a/Plan/common/src/test/java/extension/FullSystemExtension.java +++ b/Plan/common/src/test/java/extension/FullSystemExtension.java @@ -17,6 +17,7 @@ package extension; import com.djrapitops.plan.PlanSystem; +import com.djrapitops.plan.delivery.export.Exporter; import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.paths.WebserverSettings; @@ -80,7 +81,8 @@ public class FullSystemExtension implements ParameterResolver, BeforeAllCallback PlanConfig.class.equals(type) || ServerUUID.class.equals(type) || PlanPluginComponent.class.equals(type) || - Database.class.equals(type); + Database.class.equals(type) || + Exporter.class.equals(type); } @Override @@ -105,6 +107,9 @@ public class FullSystemExtension implements ParameterResolver, BeforeAllCallback if (Database.class.equals(type)) { return planSystem.getDatabaseSystem().getDatabase(); } + if (Exporter.class.equals(type)) { + return planSystem.getExportSystem().getExporter(); + } throw new ParameterResolutionException("Unsupported parameter type " + type.getName()); } } diff --git a/Plan/react/dashboard/src/service/backendConfiguration.js b/Plan/react/dashboard/src/service/backendConfiguration.js index 1752df668..4052a7812 100644 --- a/Plan/react/dashboard/src/service/backendConfiguration.js +++ b/Plan/react/dashboard/src/service/backendConfiguration.js @@ -1,6 +1,9 @@ import axios from "axios"; -const toBeReplaced = "PLAN_BASE_ADDRESS"; +const javaReplaced = { + isStatic: "PLAN_EXPORTED_VERSION", + address: "PLAN_BASE_ADDRESS" +} const isCurrentAddress = (address) => { const is = window.location.href.startsWith(address); @@ -8,7 +11,8 @@ const isCurrentAddress = (address) => { return is; } -export const baseAddress = "PLAN_BASE_ADDRESS" === toBeReplaced || !isCurrentAddress(toBeReplaced) ? "" : toBeReplaced; +export const baseAddress = "PLAN_BASE" + "_ADDRESS" === javaReplaced.address || !isCurrentAddress(javaReplaced.address) ? "" : javaReplaced.address; +export const staticSite = "PLAN_EXPORTED" + "_VERSION" !== javaReplaced.isStatic; export const doSomeGetRequest = async (url, statusOptions) => { return doSomeRequest(url, statusOptions, async () => axios.get(url));