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 000000000..373a55242 Binary files /dev/null and b/worldedit-core/src/test/resources/world_region.mca.gzip differ diff --git a/worldedit-fabric/build.gradle.kts b/worldedit-fabric/build.gradle.kts index de69b0b00..be85099fa 100644 --- a/worldedit-fabric/build.gradle.kts +++ b/worldedit-fabric/build.gradle.kts @@ -61,7 +61,16 @@ tasks.named("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;