From ff47e6f717dfcafc8689030dae21b05ffac1e5d3 Mon Sep 17 00:00:00 2001 From: Octavia Togami Date: Mon, 16 Dec 2019 03:00:12 -0800 Subject: [PATCH] Cherry-pick to fix EntryMaker issue --- buildSrc/src/main/kotlin/PlatformConfig.kt | 20 +- buildSrc/src/main/kotlin/Versions.kt | 6 +- config/checkstyle/checkstyle.xml | 2 +- config/checkstyle/import-control.xml | 5 +- worldedit-bukkit/build.gradle.kts | 7 +- .../com/sk89q/worldedit/LocalSession.java | 12 +- .../command/LegacySnapshotCommands.java | 6 +- .../command/LegacySnapshotUtilCommands.java | 2 +- .../command/SnapshotUtilCommands.java | 87 +++---- .../worldedit/regions/RegionSelector.java | 5 + .../util/PropertiesConfiguration.java | 5 + .../worldedit/util/YAMLConfiguration.java | 5 +- .../worldedit/util/function/IORunnable.java | 11 - .../util/io/file/ArchiveNioSupport.java | 3 +- .... Add new experimental snapshot API (#524) | 40 +++ .../util/io/file/ArchiveNioSupports.java | 4 +- .../io/file/TrueVfsArchiveNioSupport.java | 24 +- .../util/io/file/ZipArchiveNioSupport.java | 17 +- .../fs/FileSystemSnapshotDatabase.java | 230 ++++++++++-------- .../experimental/fs/FolderSnapshot.java | 8 +- .../snapshot/experimental/fs/EntryMaker.java | 131 ++++++++++ .../snapshot/experimental/fs/FSSDContext.java | 32 +-- .../experimental/fs/FSSDTestType.java | 36 +-- .../fs/FileSystemSnapshotDatabaseTest.java | 13 +- .../src/test/resources/world_region.mca.gzip | Bin 0 -> 39443 bytes worldedit-fabric/build.gradle.kts | 9 + worldedit-forge/build.gradle.kts | 1 + worldedit-sponge/build.gradle.kts | 9 + .../config/ConfigurateConfiguration.java | 5 +- 29 files changed, 435 insertions(+), 300 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java~18a55bc14... Add new experimental snapshot API (#524) create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/EntryMaker.java create mode 100644 worldedit-core/src/test/resources/world_region.mca.gzip diff --git a/buildSrc/src/main/kotlin/PlatformConfig.kt b/buildSrc/src/main/kotlin/PlatformConfig.kt index 4175d9362..0c573871c 100644 --- a/buildSrc/src/main/kotlin/PlatformConfig.kt +++ b/buildSrc/src/main/kotlin/PlatformConfig.kt @@ -120,19 +120,7 @@ fun Project.applyShadowConfiguration() { } } -private val CLASSPATH = listOf("truezip", "truevfs", "js") - .map { "$it.jar" } - .flatMap { listOf(it, "WorldEdit/$it") } - .joinToString(separator = " ") - -fun Project.addJarManifest(includeClasspath: Boolean = false) { - tasks.named("jar") { - val attributes = mutableMapOf( - "WorldEdit-Version" to project(":worldedit-core").version - ) - if (includeClasspath) { - attributes["Class-Path"] = CLASSPATH - } - manifest.attributes(attributes) - } -} +val CLASSPATH = listOf("truezip", "truevfs", "js") + .map { "$it.jar" } + .flatMap { listOf(it, "WorldEdit/$it") } + .joinToString(separator = " ") diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index d2451e441..9234ea8fe 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -4,9 +4,9 @@ object Versions { const val TEXT = "3.0.3" const val TEXT_EXTRAS = "3.0.3" const val PISTON = "0.5.2" - const val AUTO_VALUE = "1.7" - const val JUNIT = "5.6.1" - const val MOCKITO = "3.3.3" + const val AUTO_VALUE = "1.6.5" + const val JUNIT = "5.5.0" + const val MOCKITO = "3.0.0" const val LOGBACK = "1.2.3" } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index e2b9d154b..ab1c6d499 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -1,7 +1,7 @@ + "http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd"> diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml index 7b72860a3..06c7c3edc 100644 --- a/config/checkstyle/import-control.xml +++ b/config/checkstyle/import-control.xml @@ -1,7 +1,6 @@ - + "-//Puppy Crawl//DTD Import Control 1.1//EN" + "http://checkstyle.sourceforge.net/dtds/import_control_1_1.dtd"> diff --git a/worldedit-bukkit/build.gradle.kts b/worldedit-bukkit/build.gradle.kts index 9bb48edf5..07fab21be 100644 --- a/worldedit-bukkit/build.gradle.kts +++ b/worldedit-bukkit/build.gradle.kts @@ -75,7 +75,12 @@ tasks.named("processResources") { exclude("**/worldedit-adapters.jar") } -addJarManifest(includeClasspath = true) +tasks.named("jar") { + manifest { + attributes("Class-Path" to CLASSPATH, + "WorldEdit-Version" to project.version) + } +} tasks.named("shadowJar") { from(zipTree("src/main/resources/worldedit-adapters.jar").matching { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java index c4227c9be..cc22a2972 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -77,8 +77,10 @@ import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.item.ItemTypes; -import com.sk89q.worldedit.world.snapshot.Snapshot; +import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import javax.annotation.Nullable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -140,7 +142,8 @@ public class LocalSession implements TextureHolder { private transient int maxTimeoutTime; private transient boolean useInventory; private transient com.sk89q.worldedit.world.snapshot.Snapshot snapshot; - private transient com.sk89q.worldedit.world.snapshot.experimental.Snapshot snapshotExperimental; private transient boolean hasCUISupport = false; + private transient Snapshot snapshotExperimental; + private transient boolean hasCUISupport = false; private transient int cuiVersion = -1; private transient boolean fastMode = false; private transient Mask mask; @@ -977,8 +980,7 @@ public class LocalSession implements TextureHolder { * * @return the snapshot */ - public @Nullable - com.sk89q.worldedit.world.snapshot.experimental.Snapshot getSnapshotExperimental() { + public @Nullable Snapshot getSnapshotExperimental() { return snapshotExperimental; } @@ -987,7 +989,7 @@ public class LocalSession implements TextureHolder { * * @param snapshotExperimental a snapshot */ - public void setSnapshotExperimental(@Nullable com.sk89q.worldedit.world.snapshot.experimental.Snapshot snapshotExperimental) { + public void setSnapshotExperimental(@Nullable Snapshot snapshotExperimental) { this.snapshotExperimental = snapshotExperimental; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotCommands.java index 9c5c1e32f..ee423a1bd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotCommands.java @@ -190,9 +190,9 @@ class LegacySnapshotCommands { public Component getComponent(int number) { final Snapshot snapshot = snapshots.get(number); return TextComponent.of(number + 1 + ". ", TextColor.GOLD) - .append(TextComponent.of(snapshot.getName(), TextColor.LIGHT_PURPLE) - .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to use"))) - .clickEvent(ClickEvent.of(ClickEvent.Action.RUN_COMMAND, "/snap use " + snapshot.getName()))); + .append(TextComponent.of(snapshot.getName(), TextColor.LIGHT_PURPLE) + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to use"))) + .clickEvent(ClickEvent.of(ClickEvent.Action.RUN_COMMAND, "/snap use " + snapshot.getName()))); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotUtilCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotUtilCommands.java index 31b1d00c6..34184632e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotUtilCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/LegacySnapshotUtilCommands.java @@ -48,7 +48,7 @@ class LegacySnapshotUtilCommands { } void restore(Actor actor, World world, LocalSession session, EditSession editSession, - String snapshotName) throws WorldEditException { + String snapshotName) throws WorldEditException { LocalConfiguration config = we.getConfiguration(); Region region = session.getSelection(world); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java index 91cbcf084..eb842e2fc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java @@ -31,29 +31,31 @@ import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; -import com.sk89q.worldedit.world.DataException; import com.sk89q.worldedit.world.World; -import com.sk89q.worldedit.world.snapshot.InvalidSnapshotException; -import com.sk89q.worldedit.world.snapshot.Snapshot; -import com.sk89q.worldedit.world.snapshot.SnapshotRestore; -import com.sk89q.worldedit.world.storage.ChunkStore; -import com.sk89q.worldedit.world.storage.MissingWorldException; +import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; +import com.sk89q.worldedit.world.snapshot.experimental.SnapshotRestore; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.param.Arg; -import java.io.File; import java.io.IOException; +import java.net.URI; +import java.util.Optional; +import java.util.stream.Stream; +import static com.sk89q.worldedit.command.SnapshotCommands.checkSnapshotsConfigured; +import static com.sk89q.worldedit.command.SnapshotCommands.resolveSnapshotName; import static com.sk89q.worldedit.command.util.Logging.LogMode.REGION; @CommandContainer(superTypes = CommandPermissionsConditionGenerator.Registration.class) public class SnapshotUtilCommands { private final WorldEdit we; + private final LegacySnapshotUtilCommands legacy; public SnapshotUtilCommands(WorldEdit we) { this.we = we; + this.legacy = new LegacySnapshotUtilCommands(we); } @Command( @@ -65,12 +67,12 @@ public class SnapshotUtilCommands { @CommandPermissions("worldedit.snapshots.restore") public void restore(Actor actor, World world, LocalSession session, EditSession editSession, @Arg(name = "snapshot", desc = "The snapshot to restore", def = "") - String snapshotName) throws WorldEditException { - + String snapshotName) throws WorldEditException, IOException { LocalConfiguration config = we.getConfiguration(); + checkSnapshotsConfigured(config); - if (config.snapshotRepo == null) { - actor.printError(TranslatableComponent.of("worldedit.restore.not-configured")); + if (config.snapshotRepo != null) { + legacy.restore(actor, world, session, editSession, snapshotName); return; } @@ -78,58 +80,41 @@ public class SnapshotUtilCommands { Snapshot snapshot; if (snapshotName != null) { - try { - snapshot = config.snapshotRepo.getSnapshot(snapshotName); - } catch (InvalidSnapshotException e) { + URI uri = resolveSnapshotName(config, snapshotName); + Optional snapOpt = config.snapshotDatabase.getSnapshot(uri); + if (!snapOpt.isPresent()) { actor.printError(TranslatableComponent.of("worldedit.restore.not-available")); return; } + snapshot = snapOpt.get(); } else { - snapshot = session.getSnapshot(); + snapshot = session.getSnapshotExperimental(); } // No snapshot set? if (snapshot == null) { - try { - snapshot = config.snapshotRepo.getDefaultSnapshot(world.getName()); + try (Stream snapshotStream = + config.snapshotDatabase.getSnapshotsNewestFirst(world.getName())) { + snapshot = snapshotStream + .findFirst().orElse(null); + } - if (snapshot == null) { - actor.printError(TranslatableComponent.of("worldedit.restore.none-found-console")); - - // Okay, let's toss some debugging information! - File dir = config.snapshotRepo.getDirectory(); - - try { - WorldEdit.logger.info("WorldEdit found no snapshots: looked in: " - + dir.getCanonicalPath()); - } catch (IOException e) { - WorldEdit.logger.info("WorldEdit found no snapshots: looked in " - + "(NON-RESOLVABLE PATH - does it exist?): " - + dir.getPath()); - } - - return; - } - } catch (MissingWorldException ex) { - actor.printError(TranslatableComponent.of("worldedit.restore.none-for-world")); + if (snapshot == null) { + actor.printError(TranslatableComponent.of( + "worldedit.restore.none-for-specific-world", + TextComponent.of(world.getName()) + )); return; } } - - ChunkStore chunkStore; - - // Load chunk store - try { - chunkStore = snapshot.getChunkStore(); - actor.printInfo(TranslatableComponent.of("worldedit.restore.loaded", TextComponent.of(snapshot.getName()))); - } catch (DataException | IOException e) { - actor.printError(TranslatableComponent.of("worldedit.restore.failed", TextComponent.of(e.getMessage()))); - return; - } + actor.printInfo(TranslatableComponent.of( + "worldedit.restore.loaded", + TextComponent.of(snapshot.getInfo().getDisplayName()) + )); try { // Restore snapshot - SnapshotRestore restore = new SnapshotRestore(chunkStore, editSession, region); + SnapshotRestore restore = new SnapshotRestore(snapshot, editSession, region); //player.print(restore.getChunksAffected() + " chunk(s) will be loaded."); restore.restore(); @@ -146,12 +131,12 @@ public class SnapshotUtilCommands { } } else { actor.printInfo(TranslatableComponent.of("worldedit.restore.restored", - TextComponent.of(restore.getMissingChunks().size()), - TextComponent.of(restore.getErrorChunks().size()))); + TextComponent.of(restore.getMissingChunks().size()), + TextComponent.of(restore.getErrorChunks().size()))); } } finally { try { - chunkStore.close(); + snapshot.close(); } catch (IOException ignored) { } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/RegionSelector.java b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/RegionSelector.java index 2e257ce69..0a264990b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/RegionSelector.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/RegionSelector.java @@ -76,8 +76,13 @@ public interface RegionSelector { /** * Tell the player information about his/her primary selection. * +<<<<<<< HEAD * @param actor the actor * @param session the session +======= + * @param actor the actor + * @param session the session +>>>>>>> 18a55bc14... Add new experimental snapshot API (#524) * @param position position */ void explainPrimarySelection(Actor actor, LocalSession session, BlockVector3 position); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java index 746ea90ea..393133b5d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java @@ -27,6 +27,7 @@ import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.util.report.Unreported; import com.sk89q.worldedit.world.registry.LegacyMapper; import com.sk89q.worldedit.world.snapshot.SnapshotRepository; +import com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +38,10 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.HashSet; import java.util.Properties; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java index 6cc54bdf3..2fd4aefea 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java @@ -118,9 +118,8 @@ public class YAMLConfiguration extends LocalConfiguration { serverSideCUI = config.getBoolean("server-side-cui", true); String snapshotsDir = config.getString("snapshots.directory", ""); - if (!snapshotsDir.isEmpty()) { - snapshotRepo = new SnapshotRepository(snapshotsDir); - } + boolean experimentalSnapshots = config.getBoolean("snapshots.experimental", false); + initializeSnapshotConfiguration(snapshotsDir, experimentalSnapshots); String type = config.getString("shell-save-type", "").trim(); shellSaveType = type.isEmpty() ? null : type; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IORunnable.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IORunnable.java index a0dc747e4..338a7a0b0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IORunnable.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IORunnable.java @@ -20,7 +20,6 @@ package com.sk89q.worldedit.util.function; import java.io.IOException; -import java.io.UncheckedIOException; /** * I/O runnable type. @@ -28,16 +27,6 @@ import java.io.UncheckedIOException; @FunctionalInterface public interface IORunnable { - static Runnable unchecked(IORunnable runnable) { - return () -> { - try { - runnable.run(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - } - void run() throws IOException; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java index 5705689ee..605aaaf04 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java @@ -20,7 +20,6 @@ package com.sk89q.worldedit.util.io.file; import java.io.IOException; -import java.nio.file.FileSystem; import java.nio.file.Path; import java.util.Optional; @@ -35,6 +34,6 @@ public interface ArchiveNioSupport { * @param archive the archive to open * @return the path for the root of the archive, if available */ - Optional tryOpenAsDir(Path archive) throws IOException; + Optional tryOpenAsDir(Path archive) throws IOException; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java~18a55bc14... Add new experimental snapshot API (#524) b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java~18a55bc14... Add new experimental snapshot API (#524) new file mode 100644 index 000000000..70c9474bf --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupport.java~18a55bc14... Add new experimental snapshot API (#524) @@ -0,0 +1,40 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.sk89q.worldedit.util.io.file; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.util.Optional; + +/** + * Something that can provide access to an archive file as a file system. + */ +public interface ArchiveNioSupport { + + /** + * Try to open the given archive as a file system. + * + * @param archive the archive to open + * @return the path for the root of the archive, if available + */ + Optional tryOpenAsDir(Path archive) throws IOException; + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupports.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupports.java index e47f00a1f..ae80431a5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupports.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupports.java @@ -45,9 +45,9 @@ public class ArchiveNioSupports { .build(); } - public static Optional tryOpenAsDir(Path archive) throws IOException { + public static Optional tryOpenAsDir(Path archive) throws IOException { for (ArchiveNioSupport support : SUPPORTS) { - Optional fs = support.tryOpenAsDir(archive); + Optional fs = support.tryOpenAsDir(archive); if (fs.isPresent()) { return fs; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/TrueVfsArchiveNioSupport.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/TrueVfsArchiveNioSupport.java index 1c3205ff9..e3b3527ee 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/TrueVfsArchiveNioSupport.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/TrueVfsArchiveNioSupport.java @@ -22,7 +22,6 @@ package com.sk89q.worldedit.util.io.file; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; import net.java.truevfs.access.TArchiveDetector; -import net.java.truevfs.access.TFileSystem; import net.java.truevfs.access.TPath; import java.io.IOException; @@ -46,28 +45,15 @@ public final class TrueVfsArchiveNioSupport implements ArchiveNioSupport { } @Override - public Optional tryOpenAsDir(Path archive) throws IOException { + public Optional tryOpenAsDir(Path archive) throws IOException { String fileName = archive.getFileName().toString(); int dot = fileName.indexOf('.'); - if (dot < 0 || dot >= fileName.length() || !ALLOWED_EXTENSIONS - .contains(fileName.substring(dot + 1))) { + if (dot < 0 || dot >= fileName.length() || !ALLOWED_EXTENSIONS.contains(fileName.substring(dot + 1))) { return Optional.empty(); } - TFileSystem fileSystem = new TPath(archive).getFileSystem(); - TPath root = fileSystem.getPath("/"); - Path realRoot = ArchiveNioSupports.skipRootSameName( + TPath root = new TPath(archive).getFileSystem().getPath("/"); + return Optional.of(ArchiveNioSupports.skipRootSameName( root, fileName.substring(0, dot) - ); - return Optional.of(new ArchiveDir() { - @Override - public Path getPath() { - return realRoot; - } - - @Override - public void close() throws IOException { - fileSystem.close(); - } - }); + )); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ZipArchiveNioSupport.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ZipArchiveNioSupport.java index 8fa41d994..069965a5c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ZipArchiveNioSupport.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ZipArchiveNioSupport.java @@ -37,28 +37,17 @@ public final class ZipArchiveNioSupport implements ArchiveNioSupport { } @Override - public Optional tryOpenAsDir(Path archive) throws IOException { + public Optional tryOpenAsDir(Path archive) throws IOException { if (!archive.getFileName().toString().endsWith(".zip")) { return Optional.empty(); } FileSystem zipFs = FileSystems.newFileSystem( archive, getClass().getClassLoader() ); - Path root = ArchiveNioSupports.skipRootSameName( + return Optional.of(ArchiveNioSupports.skipRootSameName( zipFs.getPath("/"), archive.getFileName().toString() .replaceFirst("\\.zip$", "") - ); - return Optional.of(new ArchiveDir() { - @Override - public Path getPath() { - return root; - } - - @Override - public void close() throws IOException { - zipFs.close(); - } - }); + )); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabase.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabase.java index 9fceb1dd4..6b45a0c29 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabase.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabase.java @@ -21,31 +21,34 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs; import com.google.common.collect.ImmutableList; import com.google.common.net.UrlEscapers; -import com.sk89q.worldedit.util.function.IOFunction; import com.sk89q.worldedit.util.function.IORunnable; import com.sk89q.worldedit.util.io.Closer; -import com.sk89q.worldedit.util.io.file.ArchiveDir; import com.sk89q.worldedit.util.io.file.ArchiveNioSupport; import com.sk89q.worldedit.util.io.file.MorePaths; -import com.sk89q.worldedit.util.io.file.SafeFiles; import com.sk89q.worldedit.util.time.FileNameDateTimeParser; import com.sk89q.worldedit.util.time.ModificationDateTimeParser; import com.sk89q.worldedit.util.time.SnapshotDateTimeParser; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotDatabase; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URI; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.time.ZonedDateTime; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; +import java.util.function.Function; import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkArgument; @@ -55,6 +58,8 @@ import static com.google.common.base.Preconditions.checkArgument; */ public class FileSystemSnapshotDatabase implements SnapshotDatabase { + private static final Logger logger = LoggerFactory.getLogger(FileSystemSnapshotDatabase.class); + private static final String SCHEME = "snapfs"; private static final List DATE_TIME_PARSERS = @@ -97,24 +102,15 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { this.archiveNioSupport = archiveNioSupport; } - /* - * When this code says "idPath" it is the path that uniquely identifies that snapshot. - * A snapshot can be looked up by its idPath. - * - * When the code says "ioPath" it is the path that holds the world data, and can actually - * be read from proper. The "idPath" may not even exist, it is purely for the path components - * and not for IO. - */ - - private SnapshotInfo createSnapshotInfo(Path idPath, Path ioPath) { - // Try ID for parsing out of file name, IO for parsing mod time. - ZonedDateTime date = tryParseDateInternal(idPath).orElseGet(() -> tryParseDate(ioPath)); - return SnapshotInfo.create(createUri(idPath.toString()), date); + private SnapshotInfo createSnapshotInfo(Path fullPath, Path realPath) { + // Try full for parsing out of file name, real for parsing mod time. + ZonedDateTime date = tryParseDateInternal(fullPath).orElseGet(() -> tryParseDate(realPath)); + return SnapshotInfo.create(createUri(fullPath.toString()), date); } - private Snapshot createSnapshot(Path idPath, Path ioPath, @Nullable Closer closeCallback) { + private Snapshot createSnapshot(Path fullPath, Path realPath, @Nullable IORunnable closeCallback) { return new FolderSnapshot( - createSnapshotInfo(idPath, ioPath), ioPath, closeCallback + createSnapshotInfo(fullPath, realPath), realPath, closeCallback ); } @@ -132,31 +128,27 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { if (!name.getScheme().equals(SCHEME)) { return Optional.empty(); } - return getSnapshot(name.getSchemeSpecificPart()); - } - - private Optional getSnapshot(String id) throws IOException { - Path rawResolved = root.resolve(id); + // drop the / in the path to make it absolute + Path rawResolved = root.resolve(name.getSchemeSpecificPart()); // Catch trickery with paths: - Path ioPath = rawResolved.normalize(); - if (!ioPath.startsWith(root)) { + Path realPath = rawResolved.normalize(); + if (!realPath.startsWith(root)) { return Optional.empty(); } - Path idPath = root.relativize(ioPath); - Optional result = tryRegularFileSnapshot(idPath); + Optional result = tryRegularFileSnapshot(root.relativize(realPath), realPath); if (result.isPresent()) { return result; } - if (!Files.isDirectory(ioPath)) { + if (!Files.isDirectory(realPath)) { return Optional.empty(); } - return Optional.of(createSnapshot(idPath, ioPath, null)); + return Optional.of(createSnapshot(root.relativize(realPath), realPath, null)); } - private Optional tryRegularFileSnapshot(Path idPath) throws IOException { + private Optional tryRegularFileSnapshot(Path fullPath, Path realPath) throws IOException { Closer closer = Closer.create(); Path root = this.root; - Path relative = idPath; + Path relative = root.relativize(realPath); Iterator iterator = null; try { while (true) { @@ -164,7 +156,6 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { iterator = MorePaths.iterPaths(relative).iterator(); } if (!iterator.hasNext()) { - closer.close(); return Optional.empty(); } Path relativeNext = iterator.next(); @@ -173,17 +164,18 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { // This will never be it. continue; } - Optional newRootOpt = archiveNioSupport.tryOpenAsDir(next); + Optional newRootOpt = archiveNioSupport.tryOpenAsDir(next); if (newRootOpt.isPresent()) { - ArchiveDir archiveDir = newRootOpt.get(); - root = archiveDir.getPath(); - closer.register(archiveDir); + root = newRootOpt.get(); + if (root.getFileSystem() != FileSystems.getDefault()) { + closer.register(root.getFileSystem()); + } // Switch path to path inside the archive relative = root.resolve(relativeNext.relativize(relative).toString()); iterator = null; // Check if it exists, if so open snapshot if (Files.exists(relative)) { - return Optional.of(createSnapshot(idPath, relative, closer)); + return Optional.of(createSnapshot(fullPath, relative, closer::close)); } // Otherwise, we may have more archives to open. // Keep searching! @@ -199,97 +191,119 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { /* There are a few possible snapshot formats we accept: - a world directory, identified by /level.dat +<<<<<<< HEAD - a directory with the world name, but no level.dat - inside must be a timestamped directory/archive, which then has one of the two world formats inside of it! +======= +>>>>>>> 18a55bc14... Add new experimental snapshot API (#524) - a world archive, identified by .ext * does not need to have level.dat inside - a timestamped directory, identified by , that can have - the two world formats described above, inside the directory - a timestamped archive, identified by .ext, that can have - the same as timestamped directory, but inside the archive. +<<<<<<< HEAD +======= + - a directory with the world name, but no level.dat + - inside must be timestamped directory/archive, with the world inside that +>>>>>>> 18a55bc14... Add new experimental snapshot API (#524) All archives may have a root directory with the same name as the archive, minus the extensions. Due to extension detection methods, this won't work properly with some files, e.g. world.qux.zip/world.qux is invalid, but world.qux.zip/world isn't. */ - return SafeFiles.noLeakFileList(root) - .flatMap(IOFunction.unchecked(entry -> { - String worldEntry = getWorldEntry(worldName, entry); - if (worldEntry != null) { - return Stream.of(worldEntry); + return Stream.of( + listWorldEntries(Paths.get(""), root, worldName), + listTimestampedEntries(Paths.get(""), root, worldName) + ).flatMap(Function.identity()); + } + + private Stream listWorldEntries(Path fullPath, Path root, String worldName) throws IOException { + logger.debug("World check in: {}", root); + return Files.list(root) + .flatMap(candidate -> { + logger.debug("World trying: {}", candidate); + // Try world directory + String fileName = candidate.getFileName().toString(); + if (isSameDirectoryName(fileName, worldName)) { + // Direct + if (Files.exists(candidate.resolve("level.dat"))) { + logger.debug("Direct!"); + return Stream.of(createSnapshot( + fullPath.resolve(fileName), candidate, null + )); + } + // Container for time-stamped entries + try { + return listTimestampedEntries( + fullPath.resolve(fileName), candidate, worldName + ); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - String fileName = SafeFiles.canonicalFileName(entry); - if (fileName.equals(worldName) - && Files.isDirectory(entry) - && !Files.exists(entry.resolve("level.dat"))) { - // world dir with timestamp entries - return listTimestampedEntries(worldName, entry) - .map(id -> worldName + "/" + id); + // Try world archive + if (Files.isRegularFile(candidate) + && fileName.startsWith(worldName + ".")) { + logger.debug("Archive!"); + try { + return tryRegularFileSnapshot( + fullPath.resolve(fileName), candidate + ).map(Stream::of).orElse(null); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - return getTimestampedEntries(worldName, entry); - })) - .map(IOFunction.unchecked(id -> - getSnapshot(id) - .orElseThrow(() -> - new AssertionError("Could not find discovered snapshot: " + id) - ) - )); + logger.debug("Nothing!"); + return null; + }); } - private Stream listTimestampedEntries(String worldName, Path directory) throws IOException { - return SafeFiles.noLeakFileList(directory) - .flatMap(IOFunction.unchecked(entry -> getTimestampedEntries(worldName, entry))); + private boolean isSameDirectoryName(String fileName, String worldName) { + if (fileName.lastIndexOf('/') == fileName.length() - 1) { + fileName = fileName.substring(0, fileName.length() - 1); + } + return fileName.equalsIgnoreCase(worldName); } - private Stream getTimestampedEntries(String worldName, Path entry) throws IOException { - ZonedDateTime dateTime = FileNameDateTimeParser.getInstance().detectDateTime(entry); - if (dateTime == null) { - // nothing available at this path - return Stream.of(); - } - String fileName = SafeFiles.canonicalFileName(entry); - if (Files.isDirectory(entry)) { - // timestamped directory, find worlds inside - return listWorldEntries(worldName, entry) - .map(id -> fileName + "/" + id); - } - if (!Files.isRegularFile(entry)) { - // not an archive either? - return Stream.of(); - } - Optional asArchive = archiveNioSupport.tryOpenAsDir(entry); - if (asArchive.isPresent()) { - // timestamped archive - ArchiveDir dir = asArchive.get(); - return listWorldEntries(worldName, dir.getPath()) - .map(id -> fileName + "/" + id) - .onClose(IORunnable.unchecked(dir::close)); - } - return Stream.of(); - } - - private Stream listWorldEntries(String worldName, Path directory) throws IOException { - return SafeFiles.noLeakFileList(directory) - .map(IOFunction.unchecked(entry -> getWorldEntry(worldName, entry))) - .filter(Objects::nonNull); - } - - private String getWorldEntry(String worldName, Path entry) throws IOException { - String fileName = SafeFiles.canonicalFileName(entry); - if (fileName.equals(worldName) && Files.exists(entry.resolve("level.dat"))) { - // world directory - return worldName; - } - if (fileName.startsWith(worldName + ".") && Files.isRegularFile(entry)) { - Optional asArchive = archiveNioSupport.tryOpenAsDir(entry); - if (asArchive.isPresent()) { - // world archive - asArchive.get().close(); - return fileName; - } - } - return null; + private Stream listTimestampedEntries(Path fullPath, Path root, String worldName) throws IOException { + logger.debug("Timestamp check in: {}", root); + return Files.list(root) + .filter(candidate -> { + ZonedDateTime date = FileNameDateTimeParser.getInstance().detectDateTime(candidate); + return date != null; + }) + .flatMap(candidate -> { + logger.debug("Timestamp trying: {}", candidate); + // Try timestamped directory + if (Files.isDirectory(candidate)) { + logger.debug("Timestamped directory"); + try { + return listWorldEntries( + fullPath.resolve(candidate.getFileName().toString()), candidate, worldName + ); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + // Otherwise archive, get it as a directory & unpack it + try { + Optional newRoot = archiveNioSupport.tryOpenAsDir(candidate); + if (!newRoot.isPresent()) { + logger.debug("Nothing!"); + return null; + } + logger.debug("Timestamped archive!"); + return listWorldEntries( + fullPath.resolve(candidate.getFileName().toString()), + newRoot.get(), + worldName + ); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FolderSnapshot.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FolderSnapshot.java index f753d2507..b14c61882 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FolderSnapshot.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FolderSnapshot.java @@ -22,7 +22,7 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.util.io.Closer; +import com.sk89q.worldedit.util.function.IORunnable; import com.sk89q.worldedit.world.DataException; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo; @@ -95,9 +95,9 @@ public class FolderSnapshot implements Snapshot { private final SnapshotInfo info; private final Path folder; private final AtomicReference regionFolder = new AtomicReference<>(); - private final @Nullable Closer closeCallback; + private final @Nullable IORunnable closeCallback; - public FolderSnapshot(SnapshotInfo info, Path folder, @Nullable Closer closeCallback) { + public FolderSnapshot(SnapshotInfo info, Path folder, @Nullable IORunnable closeCallback) { this.info = info; // This is required to force TrueVfs to properly resolve parents. // Kinda odd, but whatever works. @@ -160,7 +160,7 @@ public class FolderSnapshot implements Snapshot { @Override public void close() throws IOException { if (closeCallback != null) { - closeCallback.close(); + closeCallback.run(); } } } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/EntryMaker.java b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/EntryMaker.java new file mode 100644 index 000000000..768a7b98a --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/EntryMaker.java @@ -0,0 +1,131 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.sk89q.worldedit.world.snapshot.experimental.fs; + +import com.google.common.collect.ImmutableMap; +import com.sk89q.worldedit.world.storage.LegacyChunkStore; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.ZonedDateTime; + +import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_DATA; +import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_POS; +import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.FORMATTER; +import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.REGION_DATA; + +interface EntryMaker { + EntryMaker TIMESTAMPED_DIR = (directory, time) -> { + Path timestampedDir = directory.resolve(time.format(FORMATTER)); + Files.createDirectories(timestampedDir); + return timestampedDir; + }; + EntryMaker TIMESTAMPED_ARCHIVE = (directory, time) -> { + Path zipFile = directory.resolve(time.format(FORMATTER) + ".zip"); + try (FileSystem zipFs = FileSystems.newFileSystem( + URI.create("jar:" + zipFile.toUri() + "!/"), + ImmutableMap.of("create", "true") + )) { + TIMESTAMPED_DIR.createEntry(zipFs.getPath("/"), time); + } + return zipFile; + }; + EntryMaker WORLD_DIR = (directory, worldName) -> { + Path worldDir = directory.resolve(worldName); + Files.createDirectories(worldDir); + Files.createFile(worldDir.resolve("level.dat")); + Path regionFolder = worldDir.resolve("region"); + Files.createDirectory(regionFolder); + Files.write(regionFolder.resolve("r.0.0.mca"), REGION_DATA); + Files.write(regionFolder.resolve("r.1.1.mcr"), REGION_DATA); + return worldDir; + }; + + class DimInfo { + final String worldName; + final int dim; + + DimInfo(String worldName, int dim) { + this.worldName = worldName; + this.dim = dim; + } + } + + EntryMaker WORLD_DIM_DIR = (directory, dimInfo) -> { + Path worldDir = directory.resolve(dimInfo.worldName); + Files.createDirectories(worldDir); + Files.createFile(worldDir.resolve("level.dat")); + Path dimFolder = worldDir.resolve("DIM" + dimInfo.dim).resolve("region"); + Files.createDirectories(dimFolder); + Files.write(dimFolder.resolve("r.0.0.mca"), REGION_DATA); + Files.write(dimFolder.resolve("r.1.1.mcr"), REGION_DATA); + return worldDir; + }; + EntryMaker WORLD_NO_REGION_DIR = (directory, worldName) -> { + Path worldDir = directory.resolve(worldName); + Files.createDirectories(worldDir); + Files.createFile(worldDir.resolve("level.dat")); + Files.write(worldDir.resolve("r.0.0.mca"), REGION_DATA); + Files.write(worldDir.resolve("r.1.1.mcr"), REGION_DATA); + return worldDir; + }; + EntryMaker WORLD_LEGACY_DIR = (directory, worldName) -> { + Path worldDir = directory.resolve(worldName); + Files.createDirectories(worldDir); + Files.createFile(worldDir.resolve("level.dat")); + Path chunkFile = worldDir.resolve(LegacyChunkStore.getFilename( + CHUNK_POS.toBlockVector2(), "/" + )); + Files.createDirectories(chunkFile.getParent()); + Files.write(chunkFile, CHUNK_DATA); + chunkFile = worldDir.resolve(LegacyChunkStore.getFilename( + CHUNK_POS.add(32, 0, 32).toBlockVector2(), "/" + )); + Files.createDirectories(chunkFile.getParent()); + Files.write(chunkFile, CHUNK_DATA); + return worldDir; + }; + EntryMaker WORLD_ARCHIVE = (directory, worldName) -> { + Path tempDir = Files.createTempDirectory("worldedit-fs-snap-db" + worldName); + Path temp = tempDir.resolve(worldName + ".zip"); + try { + Files.deleteIfExists(temp); + try (FileSystem zipFs = FileSystems.newFileSystem( + URI.create("jar:" + temp.toUri() + "!/"), + ImmutableMap.of("create", "true") + )) { + WORLD_DIR.createEntry(zipFs.getPath("/"), worldName); + } + Path zipFile = directory.resolve(worldName + ".zip"); + Files.copy(temp, zipFile); + return zipFile; + } finally { + Files.deleteIfExists(temp); + Files.deleteIfExists(tempDir); + } + }; + + Path createEntry(Path directory, T name) throws IOException; + +} diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDContext.java b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDContext.java index d63ff3936..0c34a2a71 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDContext.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDContext.java @@ -19,8 +19,6 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs; -import com.sk89q.worldedit.util.io.Closer; -import com.sk89q.worldedit.util.io.file.ArchiveDir; import com.sk89q.worldedit.util.io.file.ArchiveNioSupport; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; @@ -30,7 +28,6 @@ import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkArgument; import static java.util.stream.Collectors.toList; @@ -70,34 +67,19 @@ class FSSDContext { String worldName = Paths.get(name).getFileName().toString(); // Without an extension worldName = worldName.split("\\.")[0]; - List snapshots; - try (Stream snapshotStream = db.getSnapshots(worldName)) { - snapshots = snapshotStream.collect(toList()); - } - try { - assertTrue(snapshots.size() <= 1, - "Too many snapshots matched for " + worldName); - return requireSnapshot(name, snapshots.stream().findAny().orElse(null)); - } catch (Throwable t) { - Closer closer = Closer.create(); - snapshots.forEach(closer::register); - throw closer.rethrowAndClose(t); - } + List snapshots = db.getSnapshots(worldName).collect(toList()); + assertTrue(1 >= snapshots.size(), + "Too many snapshots matched for " + worldName); + return requireSnapshot(name, snapshots.stream().findAny().orElse(null)); } - Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) throws IOException { + Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) { assertNotNull(snapshot, "No snapshot for " + name); - try { - assertEquals(name, snapshot.getInfo().getDisplayName()); - } catch (Throwable t) { - Closer closer = Closer.create(); - closer.register(snapshot); - throw closer.rethrowAndClose(t); - } + assertEquals(name, snapshot.getInfo().getDisplayName()); return snapshot; } - ArchiveDir getRootOfArchive(Path archive) throws IOException { + Path getRootOfArchive(Path archive) throws IOException { return archiveNioSupport.tryOpenAsDir(archive) .orElseThrow(() -> new AssertionError("No archive opener for " + archive)); } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDTestType.java b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDTestType.java index 259f10650..d470afcd8 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDTestType.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FSSDTestType.java @@ -21,7 +21,6 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs; import com.google.common.collect.ImmutableList; import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.util.io.file.ArchiveDir; import com.sk89q.worldedit.world.DataException; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import org.junit.jupiter.api.DynamicNode; @@ -30,6 +29,7 @@ import org.junit.jupiter.api.DynamicTest; import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; @@ -38,8 +38,8 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Stream; -import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_POS; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_TAG; +import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_POS; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.TIME_ONE; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.TIME_TWO; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.WORLD_ALPHA; @@ -102,11 +102,16 @@ enum FSSDTestType { List getTests(FSSDContext context) throws IOException { Path worldArchive = EntryMaker.WORLD_ARCHIVE .createEntry(context.db.getRoot(), WORLD_ALPHA); - try (ArchiveDir rootOfArchive = context.getRootOfArchive(worldArchive)) { + Path rootOfArchive = context.getRootOfArchive(worldArchive); + try { Files.setLastModifiedTime( - rootOfArchive.getPath(), + rootOfArchive, FileTime.from(TIME_ONE.toInstant()) ); + } finally { + if (rootOfArchive.getFileSystem() != FileSystems.getDefault()) { + rootOfArchive.getFileSystem().close(); + } } return singleSnapTest(context, WORLD_ALPHA + ".zip", TIME_ONE); } @@ -139,9 +144,14 @@ enum FSSDTestType { Path root = context.db.getRoot(); Path timestampedArchive = EntryMaker.TIMESTAMPED_ARCHIVE .createEntry(root, TIME_ONE); - try (ArchiveDir timestampedDir = context.getRootOfArchive(timestampedArchive)) { - EntryMaker.WORLD_DIR.createEntry(timestampedDir.getPath(), WORLD_ALPHA); - EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir.getPath(), WORLD_BETA); + Path timestampedDir = context.getRootOfArchive(timestampedArchive); + try { + EntryMaker.WORLD_DIR.createEntry(timestampedDir, WORLD_ALPHA); + EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir, WORLD_BETA); + } finally { + if (timestampedDir.getFileSystem() != FileSystems.getDefault()) { + timestampedDir.getFileSystem().close(); + } } return ImmutableList.of( dynamicContainer("world dir", @@ -251,18 +261,16 @@ enum FSSDTestType { } }; - List singleSnapTest(FSSDContext context, String name, + private static List singleSnapTest(FSSDContext context, String name, ZonedDateTime time) { return ImmutableList.of( dynamicTest("return a valid snapshot for " + name, () -> { - try (Snapshot snapshot = context.requireSnapshot(name)) { - assertValidSnapshot(time, snapshot); - } + Snapshot snapshot = context.requireSnapshot(name); + assertValidSnapshot(time, snapshot); }), dynamicTest("list a valid snapshot for " + name, () -> { - try (Snapshot snapshot = context.requireListsSnapshot(name)) { - assertValidSnapshot(time, snapshot); - } + Snapshot snapshot = context.requireListsSnapshot(name); + assertValidSnapshot(time, snapshot); }) ); } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabaseTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabaseTest.java index b79c88675..cb93f294a 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabaseTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/world/snapshot/experimental/fs/FileSystemSnapshotDatabaseTest.java @@ -31,7 +31,6 @@ import com.sk89q.worldedit.util.io.file.ZipArchiveNioSupport; import com.sk89q.worldedit.world.DataException; import com.sk89q.worldedit.world.storage.ChunkStoreHelper; import com.sk89q.worldedit.world.storage.McRegionReader; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicNode; @@ -77,8 +76,6 @@ class FileSystemSnapshotDatabaseTest { .atZone(ZoneId.systemDefault()); static final ZonedDateTime TIME_TWO = TIME_ONE.minusDays(1); - private static Path TEMP_DIR; - @BeforeAll static void setUpStatic() throws IOException, DataException { try (InputStream in = Resources.getResource("world_region.mca.gzip").openStream(); @@ -107,17 +104,10 @@ class FileSystemSnapshotDatabaseTest { } finally { reader.close(); } - - TEMP_DIR = Files.createTempDirectory("worldedit-fs-snap-dbs"); - } - - @AfterAll - static void afterAll() throws IOException { - deleteTree(TEMP_DIR); } private static Path newTempDb() throws IOException { - return Files.createTempDirectory(TEMP_DIR, "db"); + return Files.createTempDirectory("worldedit-fs-snap-db"); } private static void deleteTree(Path root) throws IOException { @@ -185,6 +175,7 @@ class FileSystemSnapshotDatabaseTest { try { Path dbRoot = root.resolve("snapshots"); Files.createDirectories(dbRoot); + // we leak `root` here, but I can't see a good way to clean it up. return type.getNamedTests(new FSSDContext(nioSupport, dbRoot)); } catch (Throwable t) { deleteTree(root); diff --git a/worldedit-core/src/test/resources/world_region.mca.gzip b/worldedit-core/src/test/resources/world_region.mca.gzip new file mode 100644 index 0000000000000000000000000000000000000000..373a552429bedcd4aaf5e8b5fa06c35a5b564e89 GIT binary patch literal 39443 zcmafZRa6~Kw=D@FxVr~;hu{$0f?M$5?(PW?+%?$7-Q6L$ySux)?%g-%j`Mxz{P*Sd zOI1De7~ONOHP>8KMH&GI)fbYh2L*k$rm2Lrlm_eYA&G&UNhE;sT^ywgCJd#tQnd`m z2lONP00%?Qp+|u8fPUpNW zjMShYDMSy=#q4(e$fq*2d5yu@tceu+bG#q#E__(g_nE^mB2l!N*AkpplU18RDT%0$ zCTSh`%`Z-JCmVGnC!kIfIP%7EIW%{4zuZ{q3q- zPwG|}bFv02uIU0!RW5Y0Y`E~oMF-=HSmZ)I-(sLpqwf>$dcPom_G(zy+3)e0P*XW+ z3A%O%*M_oWlIEW1or@-#dSLln1SNG7{O5GLDb93)r`4R<<;r0Wy(hxwxzU0L8S*Q0 zKdcGDwwe``pWLOiC3nY6?UPk4|L*wpfhdo#Lod;^7!|$@L%eY3j|nq1e_0#JJaX72 zd1~h^LE4H}ORZwe?gzsCdb~9aDuJb*&$O}z_6^2$od}es*E%#F>h`g62m?uT@W1a7 ziErQ~RABr9$NCCYf>Bb6Es&3c2$z%psCB+!J_d8 zMLqBG1P|8HP-zd`BJ3`+t{b06d#70uwI+gmLR#XM1n1hce#Zg4qs4TdT?DUR*&8!4 z3npVnI_)J3dGM$#@ukXx5H&~hV0`d|r2L4CuR!PR*vo_!8Z>urCxdY2AHnt)-|Zc) z11aXP*6yKVO~F+66+3=+_~fiXY@en?UQ#mW<|}SD@F|&uP}z*TFNG4X!qT&${5?un{gcl%M=Kq4qb_kV5ocL`J{|177r(#a=<5$+ zaM|-g@Kv?JfoN*9e(VW{o@p(9{!ql7-pts>i0_;^4M(6?G2U>@uXC+~gJ%3B9o?tLO!zS7dj ziOzUNJg+4F+B3JcU}Y@5_hWb%!HlD(%m6@%e+PF`rw}$(j4CIY?ZZJ?hif-VS>W zi5{0DF?oCa@2HD{-^EQI(DgB1db~qx^+Z*lsn(MiQ!c{Y{dT% z*O;ZOHKyu6t*IMlGS=@h(!~z?(;Fnnv!28$Y2r_Vodce?zDyKRsxWn}*dkq*_?OK; zS?j{%KqFuX0e&~mrwiU>9Re(Gqi_CG_Rjz*00N{YD{5_)bzQHv96&x3w@xh8+)Id~ zT`Xe(dH#ndXTR)v56kxJg(MsRX25?ljQD@wX3T?K7 z%gYiorLZ(0uB^5`Ji!Nbx0}SadAkC_O(_znK_7k?lemQnQ-I>^?(``Tb23h?SwDe(O$hrIgLkl3me=huK^r|C)2^A1J$r39pz zqX%@iqSET>Tm|4jQi?&Nloa?J>u$!9Kj$}l`_Xb|9e!=SY!`026B7#^ce-qtitbk& zylWx9U>TQnyK}{5)Ca;fZ9->a=*0|kd5?0pFLnLh;^mRJYB4~K=VC+1aqW7z$41L=>?oJoP+Mq+c`cYbKGI1BK z%=is-8xd{4TJYs9EGVb5zVna2SL-xTEfjuz-kKbmWKP-YvA%~~u-$0m>p%uJqSs#s zWcVpkZ!$rfoRMye*whNG&^7uM$LS!Yvu-~X3sCg3q1MLQ)-~H~7(G!(A@NnVKdN7R zXDa4p)35$?;wGX7?jaVUPpA}_#*1xg*&s-d2-uXfGBa^`4WCb$Uc0x%_t3f1JjA)oSn_3K1tB za&;cyhar-2cc3-`L+ek7a8B)qsLmYS*Pr>0reZ&1{?u~e{B<(9u<)r>i&GoMj|=VB z@>sJg(S9jNkSOEu1dEjd=e%>6pv+>pu>DKG@a{VhY0vA3gqoET8>g_YitT z6kB3LCnF?_hot%Rdpkwhol%4HPu2ko`o&CsO)aYu30sYrD)U4-`W%rYCqyV{4bw6% zgr$xr+BnH{HP|9OGl-CKf!bMb#zD9$ty)} zM5$O|7<((<51DJ^I8s=$iQ~U)6-nrzEnml0u}KLwxlFClV`0kMmQd~Tv6s$R&A)u0 z&6B0yRWh#YC!uC(zb+PHnk{%)Ag)A=�E`a9d~gP8chzM;M-Saer&0wcplh`fxW` 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"processResources") { } } +<<<<<<< HEAD addJarManifest(includeClasspath = true) +======= +tasks.named("jar") { + manifest { + attributes("Class-Path" to CLASSPATH, + "WorldEdit-Version" to project.version) + } +} +>>>>>>> 18a55bc14... Add new experimental snapshot API (#524) tasks.named("shadowJar") { archiveClassifier.set("dist-dev") diff --git a/worldedit-forge/build.gradle.kts b/worldedit-forge/build.gradle.kts index f8274e831..9441e7abb 100644 --- a/worldedit-forge/build.gradle.kts +++ b/worldedit-forge/build.gradle.kts @@ -87,6 +87,7 @@ tasks.named("shadowJar") { include(dependency("org.slf4j:slf4j-api")) include(dependency("org.apache.logging.log4j:log4j-slf4j-impl")) include(dependency("de.schlichtherle:truezip")) + include(dependency("net.java.truevfs:truevfs-profile-default_2.13")) include(dependency("org.mozilla:rhino")) } minimize { diff --git a/worldedit-sponge/build.gradle.kts b/worldedit-sponge/build.gradle.kts index d80932652..58812b85a 100644 --- a/worldedit-sponge/build.gradle.kts +++ b/worldedit-sponge/build.gradle.kts @@ -25,7 +25,16 @@ sponge { } } +<<<<<<< HEAD addJarManifest(includeClasspath = true) +======= +tasks.named("jar") { + manifest { + attributes("Class-Path" to CLASSPATH, + "WorldEdit-Version" to project.version) + } +} +>>>>>>> 18a55bc14... Add new experimental snapshot API (#524) tasks.named("shadowJar") { dependencies { diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java index 350a7c9a6..dbea5f0b3 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java @@ -122,9 +122,8 @@ public class ConfigurateConfiguration extends LocalConfiguration { showHelpInfo = node.getNode("show-help-on-first-use").getBoolean(true); String snapshotsDir = node.getNode("snapshots", "directory").getString(""); - if (!snapshotsDir.isEmpty()) { - snapshotRepo = new SnapshotRepository(snapshotsDir); - } + boolean experimentalSnapshots = node.getNode("snapshots", "experimental").getBoolean(false); + initializeSnapshotConfiguration(snapshotsDir, experimentalSnapshots); String type = node.getNode("shell-save-type").getString("").trim(); shellSaveType = type.equals("") ? null : type;