Cherry-pick to fix EntryMaker issue

This commit is contained in:
Octavia Togami 2019-12-16 03:00:12 -08:00 committed by IronApollo
parent 735a37ffd0
commit ff47e6f717
29 changed files with 435 additions and 300 deletions

View File

@ -120,19 +120,7 @@ fun Project.applyShadowConfiguration() {
} }
} }
private val CLASSPATH = listOf("truezip", "truevfs", "js") val CLASSPATH = listOf("truezip", "truevfs", "js")
.map { "$it.jar" } .map { "$it.jar" }
.flatMap { listOf(it, "WorldEdit/$it") } .flatMap { listOf(it, "WorldEdit/$it") }
.joinToString(separator = " ") .joinToString(separator = " ")
fun Project.addJarManifest(includeClasspath: Boolean = false) {
tasks.named<Jar>("jar") {
val attributes = mutableMapOf(
"WorldEdit-Version" to project(":worldedit-core").version
)
if (includeClasspath) {
attributes["Class-Path"] = CLASSPATH
}
manifest.attributes(attributes)
}
}

View File

@ -4,9 +4,9 @@ object Versions {
const val TEXT = "3.0.3" const val TEXT = "3.0.3"
const val TEXT_EXTRAS = "3.0.3" const val TEXT_EXTRAS = "3.0.3"
const val PISTON = "0.5.2" const val PISTON = "0.5.2"
const val AUTO_VALUE = "1.7" const val AUTO_VALUE = "1.6.5"
const val JUNIT = "5.6.1" const val JUNIT = "5.5.0"
const val MOCKITO = "3.3.3" const val MOCKITO = "3.0.0"
const val LOGBACK = "1.2.3" const val LOGBACK = "1.2.3"
} }

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC <!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN" "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd"> "http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<module name="Checker"> <module name="Checker">
<!-- Tabs are strictly banned --> <!-- Tabs are strictly banned -->
<module name="FileTabCharacter"/> <module name="FileTabCharacter"/>

View File

@ -1,7 +1,6 @@
<!DOCTYPE import-control PUBLIC <!DOCTYPE import-control PUBLIC
"-//Checkstyle//DTD ImportControl Configuration 1.4//EN" "-//Puppy Crawl//DTD Import Control 1.1//EN"
"https://checkstyle.org/dtds/import_control_1_4.dtd"> "http://checkstyle.sourceforge.net/dtds/import_control_1_1.dtd">
<import-control pkg="com.sk89q"> <import-control pkg="com.sk89q">
<allow pkg="java"/> <allow pkg="java"/>

View File

@ -75,7 +75,12 @@ tasks.named<Copy>("processResources") {
exclude("**/worldedit-adapters.jar") exclude("**/worldedit-adapters.jar")
} }
addJarManifest(includeClasspath = true) tasks.named<Jar>("jar") {
manifest {
attributes("Class-Path" to CLASSPATH,
"WorldEdit-Version" to project.version)
}
}
tasks.named<ShadowJar>("shadowJar") { tasks.named<ShadowJar>("shadowJar") {
from(zipTree("src/main/resources/worldedit-adapters.jar").matching { from(zipTree("src/main/resources/worldedit-adapters.jar").matching {

View File

@ -77,8 +77,10 @@ import com.sk89q.worldedit.world.block.BaseBlock;
import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockState;
import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.item.ItemType;
import com.sk89q.worldedit.world.item.ItemTypes; 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 it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import javax.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -140,7 +142,8 @@ public class LocalSession implements TextureHolder {
private transient int maxTimeoutTime; private transient int maxTimeoutTime;
private transient boolean useInventory; private transient boolean useInventory;
private transient com.sk89q.worldedit.world.snapshot.Snapshot snapshot; 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 int cuiVersion = -1;
private transient boolean fastMode = false; private transient boolean fastMode = false;
private transient Mask mask; private transient Mask mask;
@ -977,8 +980,7 @@ public class LocalSession implements TextureHolder {
* *
* @return the snapshot * @return the snapshot
*/ */
public @Nullable public @Nullable Snapshot getSnapshotExperimental() {
com.sk89q.worldedit.world.snapshot.experimental.Snapshot getSnapshotExperimental() {
return snapshotExperimental; return snapshotExperimental;
} }
@ -987,7 +989,7 @@ public class LocalSession implements TextureHolder {
* *
* @param snapshotExperimental a snapshot * @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; this.snapshotExperimental = snapshotExperimental;
} }

View File

@ -190,9 +190,9 @@ class LegacySnapshotCommands {
public Component getComponent(int number) { public Component getComponent(int number) {
final Snapshot snapshot = snapshots.get(number); final Snapshot snapshot = snapshots.get(number);
return TextComponent.of(number + 1 + ". ", TextColor.GOLD) return TextComponent.of(number + 1 + ". ", TextColor.GOLD)
.append(TextComponent.of(snapshot.getName(), TextColor.LIGHT_PURPLE) .append(TextComponent.of(snapshot.getName(), TextColor.LIGHT_PURPLE)
.hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to use"))) .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to use")))
.clickEvent(ClickEvent.of(ClickEvent.Action.RUN_COMMAND, "/snap use " + snapshot.getName()))); .clickEvent(ClickEvent.of(ClickEvent.Action.RUN_COMMAND, "/snap use " + snapshot.getName())));
} }
@Override @Override

View File

@ -48,7 +48,7 @@ class LegacySnapshotUtilCommands {
} }
void restore(Actor actor, World world, LocalSession session, EditSession editSession, void restore(Actor actor, World world, LocalSession session, EditSession editSession,
String snapshotName) throws WorldEditException { String snapshotName) throws WorldEditException {
LocalConfiguration config = we.getConfiguration(); LocalConfiguration config = we.getConfiguration();
Region region = session.getSelection(world); Region region = session.getSelection(world);

View File

@ -31,29 +31,31 @@ import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TextComponent;
import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; 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.World;
import com.sk89q.worldedit.world.snapshot.InvalidSnapshotException; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import com.sk89q.worldedit.world.snapshot.Snapshot; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotRestore;
import com.sk89q.worldedit.world.snapshot.SnapshotRestore;
import com.sk89q.worldedit.world.storage.ChunkStore;
import com.sk89q.worldedit.world.storage.MissingWorldException;
import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.Command;
import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.CommandContainer;
import org.enginehub.piston.annotation.param.Arg; import org.enginehub.piston.annotation.param.Arg;
import java.io.File;
import java.io.IOException; 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; import static com.sk89q.worldedit.command.util.Logging.LogMode.REGION;
@CommandContainer(superTypes = CommandPermissionsConditionGenerator.Registration.class) @CommandContainer(superTypes = CommandPermissionsConditionGenerator.Registration.class)
public class SnapshotUtilCommands { public class SnapshotUtilCommands {
private final WorldEdit we; private final WorldEdit we;
private final LegacySnapshotUtilCommands legacy;
public SnapshotUtilCommands(WorldEdit we) { public SnapshotUtilCommands(WorldEdit we) {
this.we = we; this.we = we;
this.legacy = new LegacySnapshotUtilCommands(we);
} }
@Command( @Command(
@ -65,12 +67,12 @@ public class SnapshotUtilCommands {
@CommandPermissions("worldedit.snapshots.restore") @CommandPermissions("worldedit.snapshots.restore")
public void restore(Actor actor, World world, LocalSession session, EditSession editSession, public void restore(Actor actor, World world, LocalSession session, EditSession editSession,
@Arg(name = "snapshot", desc = "The snapshot to restore", def = "") @Arg(name = "snapshot", desc = "The snapshot to restore", def = "")
String snapshotName) throws WorldEditException { String snapshotName) throws WorldEditException, IOException {
LocalConfiguration config = we.getConfiguration(); LocalConfiguration config = we.getConfiguration();
checkSnapshotsConfigured(config);
if (config.snapshotRepo == null) { if (config.snapshotRepo != null) {
actor.printError(TranslatableComponent.of("worldedit.restore.not-configured")); legacy.restore(actor, world, session, editSession, snapshotName);
return; return;
} }
@ -78,58 +80,41 @@ public class SnapshotUtilCommands {
Snapshot snapshot; Snapshot snapshot;
if (snapshotName != null) { if (snapshotName != null) {
try { URI uri = resolveSnapshotName(config, snapshotName);
snapshot = config.snapshotRepo.getSnapshot(snapshotName); Optional<Snapshot> snapOpt = config.snapshotDatabase.getSnapshot(uri);
} catch (InvalidSnapshotException e) { if (!snapOpt.isPresent()) {
actor.printError(TranslatableComponent.of("worldedit.restore.not-available")); actor.printError(TranslatableComponent.of("worldedit.restore.not-available"));
return; return;
} }
snapshot = snapOpt.get();
} else { } else {
snapshot = session.getSnapshot(); snapshot = session.getSnapshotExperimental();
} }
// No snapshot set? // No snapshot set?
if (snapshot == null) { if (snapshot == null) {
try { try (Stream<Snapshot> snapshotStream =
snapshot = config.snapshotRepo.getDefaultSnapshot(world.getName()); config.snapshotDatabase.getSnapshotsNewestFirst(world.getName())) {
snapshot = snapshotStream
.findFirst().orElse(null);
}
if (snapshot == null) { if (snapshot == null) {
actor.printError(TranslatableComponent.of("worldedit.restore.none-found-console")); actor.printError(TranslatableComponent.of(
"worldedit.restore.none-for-specific-world",
// Okay, let's toss some debugging information! TextComponent.of(world.getName())
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"));
return; return;
} }
} }
actor.printInfo(TranslatableComponent.of(
ChunkStore chunkStore; "worldedit.restore.loaded",
TextComponent.of(snapshot.getInfo().getDisplayName())
// 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;
}
try { try {
// Restore snapshot // 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."); //player.print(restore.getChunksAffected() + " chunk(s) will be loaded.");
restore.restore(); restore.restore();
@ -146,12 +131,12 @@ public class SnapshotUtilCommands {
} }
} else { } else {
actor.printInfo(TranslatableComponent.of("worldedit.restore.restored", actor.printInfo(TranslatableComponent.of("worldedit.restore.restored",
TextComponent.of(restore.getMissingChunks().size()), TextComponent.of(restore.getMissingChunks().size()),
TextComponent.of(restore.getErrorChunks().size()))); TextComponent.of(restore.getErrorChunks().size())));
} }
} finally { } finally {
try { try {
chunkStore.close(); snapshot.close();
} catch (IOException ignored) { } catch (IOException ignored) {
} }
} }

View File

@ -76,8 +76,13 @@ public interface RegionSelector {
/** /**
* Tell the player information about his/her primary selection. * Tell the player information about his/her primary selection.
* *
<<<<<<< HEAD
* @param actor the actor * @param actor the actor
* @param session the session * @param session the session
=======
* @param actor the actor
* @param session the session
>>>>>>> 18a55bc14... Add new experimental snapshot API (#524)
* @param position position * @param position position
*/ */
void explainPrimarySelection(Actor actor, LocalSession session, BlockVector3 position); void explainPrimarySelection(Actor actor, LocalSession session, BlockVector3 position);

View File

@ -27,6 +27,7 @@ import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.util.report.Unreported; import com.sk89q.worldedit.util.report.Unreported;
import com.sk89q.worldedit.world.registry.LegacyMapper; import com.sk89q.worldedit.world.registry.LegacyMapper;
import com.sk89q.worldedit.world.snapshot.SnapshotRepository; import com.sk89q.worldedit.world.snapshot.SnapshotRepository;
import com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabase;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -37,6 +38,10 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; 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.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Properties; import java.util.Properties;

View File

@ -118,9 +118,8 @@ public class YAMLConfiguration extends LocalConfiguration {
serverSideCUI = config.getBoolean("server-side-cui", true); serverSideCUI = config.getBoolean("server-side-cui", true);
String snapshotsDir = config.getString("snapshots.directory", ""); String snapshotsDir = config.getString("snapshots.directory", "");
if (!snapshotsDir.isEmpty()) { boolean experimentalSnapshots = config.getBoolean("snapshots.experimental", false);
snapshotRepo = new SnapshotRepository(snapshotsDir); initializeSnapshotConfiguration(snapshotsDir, experimentalSnapshots);
}
String type = config.getString("shell-save-type", "").trim(); String type = config.getString("shell-save-type", "").trim();
shellSaveType = type.isEmpty() ? null : type; shellSaveType = type.isEmpty() ? null : type;

View File

@ -20,7 +20,6 @@
package com.sk89q.worldedit.util.function; package com.sk89q.worldedit.util.function;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
/** /**
* I/O runnable type. * I/O runnable type.
@ -28,16 +27,6 @@ import java.io.UncheckedIOException;
@FunctionalInterface @FunctionalInterface
public interface IORunnable { public interface IORunnable {
static Runnable unchecked(IORunnable runnable) {
return () -> {
try {
runnable.run();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
}
void run() throws IOException; void run() throws IOException;
} }

View File

@ -20,7 +20,6 @@
package com.sk89q.worldedit.util.io.file; package com.sk89q.worldedit.util.io.file;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
@ -35,6 +34,6 @@ public interface ArchiveNioSupport {
* @param archive the archive to open * @param archive the archive to open
* @return the path for the root of the archive, if available * @return the path for the root of the archive, if available
*/ */
Optional<ArchiveDir> tryOpenAsDir(Path archive) throws IOException; Optional<Path> tryOpenAsDir(Path archive) throws IOException;
} }

View File

@ -0,0 +1,40 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <http://www.gnu.org/licenses/>.
*/
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<Path> tryOpenAsDir(Path archive) throws IOException;
}

View File

@ -45,9 +45,9 @@ public class ArchiveNioSupports {
.build(); .build();
} }
public static Optional<ArchiveDir> tryOpenAsDir(Path archive) throws IOException { public static Optional<Path> tryOpenAsDir(Path archive) throws IOException {
for (ArchiveNioSupport support : SUPPORTS) { for (ArchiveNioSupport support : SUPPORTS) {
Optional<ArchiveDir> fs = support.tryOpenAsDir(archive); Optional<Path> fs = support.tryOpenAsDir(archive);
if (fs.isPresent()) { if (fs.isPresent()) {
return fs; return fs;
} }

View File

@ -22,7 +22,6 @@ package com.sk89q.worldedit.util.io.file;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import net.java.truevfs.access.TArchiveDetector; import net.java.truevfs.access.TArchiveDetector;
import net.java.truevfs.access.TFileSystem;
import net.java.truevfs.access.TPath; import net.java.truevfs.access.TPath;
import java.io.IOException; import java.io.IOException;
@ -46,28 +45,15 @@ public final class TrueVfsArchiveNioSupport implements ArchiveNioSupport {
} }
@Override @Override
public Optional<ArchiveDir> tryOpenAsDir(Path archive) throws IOException { public Optional<Path> tryOpenAsDir(Path archive) throws IOException {
String fileName = archive.getFileName().toString(); String fileName = archive.getFileName().toString();
int dot = fileName.indexOf('.'); int dot = fileName.indexOf('.');
if (dot < 0 || dot >= fileName.length() || !ALLOWED_EXTENSIONS if (dot < 0 || dot >= fileName.length() || !ALLOWED_EXTENSIONS.contains(fileName.substring(dot + 1))) {
.contains(fileName.substring(dot + 1))) {
return Optional.empty(); return Optional.empty();
} }
TFileSystem fileSystem = new TPath(archive).getFileSystem(); TPath root = new TPath(archive).getFileSystem().getPath("/");
TPath root = fileSystem.getPath("/"); return Optional.of(ArchiveNioSupports.skipRootSameName(
Path realRoot = ArchiveNioSupports.skipRootSameName(
root, fileName.substring(0, dot) root, fileName.substring(0, dot)
); ));
return Optional.of(new ArchiveDir() {
@Override
public Path getPath() {
return realRoot;
}
@Override
public void close() throws IOException {
fileSystem.close();
}
});
} }
} }

View File

@ -37,28 +37,17 @@ public final class ZipArchiveNioSupport implements ArchiveNioSupport {
} }
@Override @Override
public Optional<ArchiveDir> tryOpenAsDir(Path archive) throws IOException { public Optional<Path> tryOpenAsDir(Path archive) throws IOException {
if (!archive.getFileName().toString().endsWith(".zip")) { if (!archive.getFileName().toString().endsWith(".zip")) {
return Optional.empty(); return Optional.empty();
} }
FileSystem zipFs = FileSystems.newFileSystem( FileSystem zipFs = FileSystems.newFileSystem(
archive, getClass().getClassLoader() archive, getClass().getClassLoader()
); );
Path root = ArchiveNioSupports.skipRootSameName( return Optional.of(ArchiveNioSupports.skipRootSameName(
zipFs.getPath("/"), archive.getFileName().toString() zipFs.getPath("/"), archive.getFileName().toString()
.replaceFirst("\\.zip$", "") .replaceFirst("\\.zip$", "")
); ));
return Optional.of(new ArchiveDir() {
@Override
public Path getPath() {
return root;
}
@Override
public void close() throws IOException {
zipFs.close();
}
});
} }
} }

View File

@ -21,31 +21,34 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.net.UrlEscapers; 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.function.IORunnable;
import com.sk89q.worldedit.util.io.Closer; 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.ArchiveNioSupport;
import com.sk89q.worldedit.util.io.file.MorePaths; 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.FileNameDateTimeParser;
import com.sk89q.worldedit.util.time.ModificationDateTimeParser; import com.sk89q.worldedit.util.time.ModificationDateTimeParser;
import com.sk89q.worldedit.util.time.SnapshotDateTimeParser; import com.sk89q.worldedit.util.time.SnapshotDateTimeParser;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotDatabase; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotDatabase;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI; import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkArgument; 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 { public class FileSystemSnapshotDatabase implements SnapshotDatabase {
private static final Logger logger = LoggerFactory.getLogger(FileSystemSnapshotDatabase.class);
private static final String SCHEME = "snapfs"; private static final String SCHEME = "snapfs";
private static final List<SnapshotDateTimeParser> DATE_TIME_PARSERS = private static final List<SnapshotDateTimeParser> DATE_TIME_PARSERS =
@ -97,24 +102,15 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
this.archiveNioSupport = archiveNioSupport; this.archiveNioSupport = archiveNioSupport;
} }
/* private SnapshotInfo createSnapshotInfo(Path fullPath, Path realPath) {
* When this code says "idPath" it is the path that uniquely identifies that snapshot. // Try full for parsing out of file name, real for parsing mod time.
* A snapshot can be looked up by its idPath. ZonedDateTime date = tryParseDateInternal(fullPath).orElseGet(() -> tryParseDate(realPath));
* return SnapshotInfo.create(createUri(fullPath.toString()), date);
* 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 Snapshot createSnapshot(Path idPath, Path ioPath, @Nullable Closer closeCallback) { private Snapshot createSnapshot(Path fullPath, Path realPath, @Nullable IORunnable closeCallback) {
return new FolderSnapshot( 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)) { if (!name.getScheme().equals(SCHEME)) {
return Optional.empty(); return Optional.empty();
} }
return getSnapshot(name.getSchemeSpecificPart()); // drop the / in the path to make it absolute
} Path rawResolved = root.resolve(name.getSchemeSpecificPart());
private Optional<Snapshot> getSnapshot(String id) throws IOException {
Path rawResolved = root.resolve(id);
// Catch trickery with paths: // Catch trickery with paths:
Path ioPath = rawResolved.normalize(); Path realPath = rawResolved.normalize();
if (!ioPath.startsWith(root)) { if (!realPath.startsWith(root)) {
return Optional.empty(); return Optional.empty();
} }
Path idPath = root.relativize(ioPath); Optional<Snapshot> result = tryRegularFileSnapshot(root.relativize(realPath), realPath);
Optional<Snapshot> result = tryRegularFileSnapshot(idPath);
if (result.isPresent()) { if (result.isPresent()) {
return result; return result;
} }
if (!Files.isDirectory(ioPath)) { if (!Files.isDirectory(realPath)) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(createSnapshot(idPath, ioPath, null)); return Optional.of(createSnapshot(root.relativize(realPath), realPath, null));
} }
private Optional<Snapshot> tryRegularFileSnapshot(Path idPath) throws IOException { private Optional<Snapshot> tryRegularFileSnapshot(Path fullPath, Path realPath) throws IOException {
Closer closer = Closer.create(); Closer closer = Closer.create();
Path root = this.root; Path root = this.root;
Path relative = idPath; Path relative = root.relativize(realPath);
Iterator<Path> iterator = null; Iterator<Path> iterator = null;
try { try {
while (true) { while (true) {
@ -164,7 +156,6 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
iterator = MorePaths.iterPaths(relative).iterator(); iterator = MorePaths.iterPaths(relative).iterator();
} }
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
closer.close();
return Optional.empty(); return Optional.empty();
} }
Path relativeNext = iterator.next(); Path relativeNext = iterator.next();
@ -173,17 +164,18 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
// This will never be it. // This will never be it.
continue; continue;
} }
Optional<ArchiveDir> newRootOpt = archiveNioSupport.tryOpenAsDir(next); Optional<Path> newRootOpt = archiveNioSupport.tryOpenAsDir(next);
if (newRootOpt.isPresent()) { if (newRootOpt.isPresent()) {
ArchiveDir archiveDir = newRootOpt.get(); root = newRootOpt.get();
root = archiveDir.getPath(); if (root.getFileSystem() != FileSystems.getDefault()) {
closer.register(archiveDir); closer.register(root.getFileSystem());
}
// Switch path to path inside the archive // Switch path to path inside the archive
relative = root.resolve(relativeNext.relativize(relative).toString()); relative = root.resolve(relativeNext.relativize(relative).toString());
iterator = null; iterator = null;
// Check if it exists, if so open snapshot // Check if it exists, if so open snapshot
if (Files.exists(relative)) { 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. // Otherwise, we may have more archives to open.
// Keep searching! // Keep searching!
@ -199,97 +191,119 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
/* /*
There are a few possible snapshot formats we accept: There are a few possible snapshot formats we accept:
- a world directory, identified by <worldName>/level.dat - a world directory, identified by <worldName>/level.dat
<<<<<<< HEAD
- a directory with the world name, but no level.dat - 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 - inside must be a timestamped directory/archive, which then has one of the two world
formats inside of it! formats inside of it!
=======
>>>>>>> 18a55bc14... Add new experimental snapshot API (#524)
- a world archive, identified by <worldName>.ext - a world archive, identified by <worldName>.ext
* does not need to have level.dat inside * does not need to have level.dat inside
- a timestamped directory, identified by <stamp>, that can have - a timestamped directory, identified by <stamp>, that can have
- the two world formats described above, inside the directory - the two world formats described above, inside the directory
- a timestamped archive, identified by <stamp>.ext, that can have - a timestamped archive, identified by <stamp>.ext, that can have
- the same as timestamped directory, but inside the archive. - 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, 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 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. with some files, e.g. world.qux.zip/world.qux is invalid, but world.qux.zip/world isn't.
*/ */
return SafeFiles.noLeakFileList(root) return Stream.of(
.flatMap(IOFunction.unchecked(entry -> { listWorldEntries(Paths.get(""), root, worldName),
String worldEntry = getWorldEntry(worldName, entry); listTimestampedEntries(Paths.get(""), root, worldName)
if (worldEntry != null) { ).flatMap(Function.identity());
return Stream.of(worldEntry); }
private Stream<Snapshot> 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); // Try world archive
if (fileName.equals(worldName) if (Files.isRegularFile(candidate)
&& Files.isDirectory(entry) && fileName.startsWith(worldName + ".")) {
&& !Files.exists(entry.resolve("level.dat"))) { logger.debug("Archive!");
// world dir with timestamp entries try {
return listTimestampedEntries(worldName, entry) return tryRegularFileSnapshot(
.map(id -> worldName + "/" + id); fullPath.resolve(fileName), candidate
).map(Stream::of).orElse(null);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} }
return getTimestampedEntries(worldName, entry); logger.debug("Nothing!");
})) return null;
.map(IOFunction.unchecked(id -> });
getSnapshot(id)
.orElseThrow(() ->
new AssertionError("Could not find discovered snapshot: " + id)
)
));
} }
private Stream<String> listTimestampedEntries(String worldName, Path directory) throws IOException { private boolean isSameDirectoryName(String fileName, String worldName) {
return SafeFiles.noLeakFileList(directory) if (fileName.lastIndexOf('/') == fileName.length() - 1) {
.flatMap(IOFunction.unchecked(entry -> getTimestampedEntries(worldName, entry))); fileName = fileName.substring(0, fileName.length() - 1);
}
return fileName.equalsIgnoreCase(worldName);
} }
private Stream<String> getTimestampedEntries(String worldName, Path entry) throws IOException { private Stream<Snapshot> listTimestampedEntries(Path fullPath, Path root, String worldName) throws IOException {
ZonedDateTime dateTime = FileNameDateTimeParser.getInstance().detectDateTime(entry); logger.debug("Timestamp check in: {}", root);
if (dateTime == null) { return Files.list(root)
// nothing available at this path .filter(candidate -> {
return Stream.of(); ZonedDateTime date = FileNameDateTimeParser.getInstance().detectDateTime(candidate);
} return date != null;
String fileName = SafeFiles.canonicalFileName(entry); })
if (Files.isDirectory(entry)) { .flatMap(candidate -> {
// timestamped directory, find worlds inside logger.debug("Timestamp trying: {}", candidate);
return listWorldEntries(worldName, entry) // Try timestamped directory
.map(id -> fileName + "/" + id); if (Files.isDirectory(candidate)) {
} logger.debug("Timestamped directory");
if (!Files.isRegularFile(entry)) { try {
// not an archive either? return listWorldEntries(
return Stream.of(); fullPath.resolve(candidate.getFileName().toString()), candidate, worldName
} );
Optional<ArchiveDir> asArchive = archiveNioSupport.tryOpenAsDir(entry); } catch (IOException e) {
if (asArchive.isPresent()) { throw new UncheckedIOException(e);
// timestamped archive }
ArchiveDir dir = asArchive.get(); }
return listWorldEntries(worldName, dir.getPath()) // Otherwise archive, get it as a directory & unpack it
.map(id -> fileName + "/" + id) try {
.onClose(IORunnable.unchecked(dir::close)); Optional<Path> newRoot = archiveNioSupport.tryOpenAsDir(candidate);
} if (!newRoot.isPresent()) {
return Stream.of(); logger.debug("Nothing!");
} return null;
}
private Stream<String> listWorldEntries(String worldName, Path directory) throws IOException { logger.debug("Timestamped archive!");
return SafeFiles.noLeakFileList(directory) return listWorldEntries(
.map(IOFunction.unchecked(entry -> getWorldEntry(worldName, entry))) fullPath.resolve(candidate.getFileName().toString()),
.filter(Objects::nonNull); newRoot.get(),
} worldName
);
private String getWorldEntry(String worldName, Path entry) throws IOException { } catch (IOException e) {
String fileName = SafeFiles.canonicalFileName(entry); throw new UncheckedIOException(e);
if (fileName.equals(worldName) && Files.exists(entry.resolve("level.dat"))) { }
// world directory });
return worldName;
}
if (fileName.startsWith(worldName + ".") && Files.isRegularFile(entry)) {
Optional<ArchiveDir> asArchive = archiveNioSupport.tryOpenAsDir(entry);
if (asArchive.isPresent()) {
// world archive
asArchive.get().close();
return fileName;
}
}
return null;
} }
} }

View File

@ -22,7 +22,7 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs;
import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.math.BlockVector3; 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.DataException;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo; import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo;
@ -95,9 +95,9 @@ public class FolderSnapshot implements Snapshot {
private final SnapshotInfo info; private final SnapshotInfo info;
private final Path folder; private final Path folder;
private final AtomicReference<Object> regionFolder = new AtomicReference<>(); private final AtomicReference<Object> 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.info = info;
// This is required to force TrueVfs to properly resolve parents. // This is required to force TrueVfs to properly resolve parents.
// Kinda odd, but whatever works. // Kinda odd, but whatever works.
@ -160,7 +160,7 @@ public class FolderSnapshot implements Snapshot {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
if (closeCallback != null) { if (closeCallback != null) {
closeCallback.close(); closeCallback.run();
} }
} }
} }

View File

@ -0,0 +1,131 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <http://www.gnu.org/licenses/>.
*/
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<T> {
EntryMaker<ZonedDateTime> TIMESTAMPED_DIR = (directory, time) -> {
Path timestampedDir = directory.resolve(time.format(FORMATTER));
Files.createDirectories(timestampedDir);
return timestampedDir;
};
EntryMaker<ZonedDateTime> 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<String> 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<DimInfo> 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<String> 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<String> 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<String> 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;
}

View File

@ -19,8 +19,6 @@
package com.sk89q.worldedit.world.snapshot.experimental.fs; 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.util.io.file.ArchiveNioSupport;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; 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.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
@ -70,34 +67,19 @@ class FSSDContext {
String worldName = Paths.get(name).getFileName().toString(); String worldName = Paths.get(name).getFileName().toString();
// Without an extension // Without an extension
worldName = worldName.split("\\.")[0]; worldName = worldName.split("\\.")[0];
List<Snapshot> snapshots; List<Snapshot> snapshots = db.getSnapshots(worldName).collect(toList());
try (Stream<Snapshot> snapshotStream = db.getSnapshots(worldName)) { assertTrue(1 >= snapshots.size(),
snapshots = snapshotStream.collect(toList()); "Too many snapshots matched for " + worldName);
} return requireSnapshot(name, snapshots.stream().findAny().orElse(null));
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);
}
} }
Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) throws IOException { Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) {
assertNotNull(snapshot, "No snapshot for " + name); assertNotNull(snapshot, "No snapshot for " + name);
try { assertEquals(name, snapshot.getInfo().getDisplayName());
assertEquals(name, snapshot.getInfo().getDisplayName());
} catch (Throwable t) {
Closer closer = Closer.create();
closer.register(snapshot);
throw closer.rethrowAndClose(t);
}
return snapshot; return snapshot;
} }
ArchiveDir getRootOfArchive(Path archive) throws IOException { Path getRootOfArchive(Path archive) throws IOException {
return archiveNioSupport.tryOpenAsDir(archive) return archiveNioSupport.tryOpenAsDir(archive)
.orElseThrow(() -> new AssertionError("No archive opener for " + archive)); .orElseThrow(() -> new AssertionError("No archive opener for " + archive));
} }

View File

@ -21,7 +21,6 @@ package com.sk89q.worldedit.world.snapshot.experimental.fs;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.sk89q.worldedit.math.BlockVector3; 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.DataException;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot; import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicNode;
@ -30,6 +29,7 @@ import org.junit.jupiter.api.DynamicTest;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.FileTime; import java.nio.file.attribute.FileTime;
@ -38,8 +38,8 @@ import java.util.List;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Stream; 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_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_ONE;
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.TIME_TWO; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.TIME_TWO;
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.WORLD_ALPHA; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.WORLD_ALPHA;
@ -102,11 +102,16 @@ enum FSSDTestType {
List<DynamicTest> getTests(FSSDContext context) throws IOException { List<DynamicTest> getTests(FSSDContext context) throws IOException {
Path worldArchive = EntryMaker.WORLD_ARCHIVE Path worldArchive = EntryMaker.WORLD_ARCHIVE
.createEntry(context.db.getRoot(), WORLD_ALPHA); .createEntry(context.db.getRoot(), WORLD_ALPHA);
try (ArchiveDir rootOfArchive = context.getRootOfArchive(worldArchive)) { Path rootOfArchive = context.getRootOfArchive(worldArchive);
try {
Files.setLastModifiedTime( Files.setLastModifiedTime(
rootOfArchive.getPath(), rootOfArchive,
FileTime.from(TIME_ONE.toInstant()) FileTime.from(TIME_ONE.toInstant())
); );
} finally {
if (rootOfArchive.getFileSystem() != FileSystems.getDefault()) {
rootOfArchive.getFileSystem().close();
}
} }
return singleSnapTest(context, WORLD_ALPHA + ".zip", TIME_ONE); return singleSnapTest(context, WORLD_ALPHA + ".zip", TIME_ONE);
} }
@ -139,9 +144,14 @@ enum FSSDTestType {
Path root = context.db.getRoot(); Path root = context.db.getRoot();
Path timestampedArchive = EntryMaker.TIMESTAMPED_ARCHIVE Path timestampedArchive = EntryMaker.TIMESTAMPED_ARCHIVE
.createEntry(root, TIME_ONE); .createEntry(root, TIME_ONE);
try (ArchiveDir timestampedDir = context.getRootOfArchive(timestampedArchive)) { Path timestampedDir = context.getRootOfArchive(timestampedArchive);
EntryMaker.WORLD_DIR.createEntry(timestampedDir.getPath(), WORLD_ALPHA); try {
EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir.getPath(), WORLD_BETA); 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( return ImmutableList.of(
dynamicContainer("world dir", dynamicContainer("world dir",
@ -251,18 +261,16 @@ enum FSSDTestType {
} }
}; };
List<DynamicTest> singleSnapTest(FSSDContext context, String name, private static List<DynamicTest> singleSnapTest(FSSDContext context, String name,
ZonedDateTime time) { ZonedDateTime time) {
return ImmutableList.of( return ImmutableList.of(
dynamicTest("return a valid snapshot for " + name, () -> { dynamicTest("return a valid snapshot for " + name, () -> {
try (Snapshot snapshot = context.requireSnapshot(name)) { Snapshot snapshot = context.requireSnapshot(name);
assertValidSnapshot(time, snapshot); assertValidSnapshot(time, snapshot);
}
}), }),
dynamicTest("list a valid snapshot for " + name, () -> { dynamicTest("list a valid snapshot for " + name, () -> {
try (Snapshot snapshot = context.requireListsSnapshot(name)) { Snapshot snapshot = context.requireListsSnapshot(name);
assertValidSnapshot(time, snapshot); assertValidSnapshot(time, snapshot);
}
}) })
); );
} }

View File

@ -31,7 +31,6 @@ import com.sk89q.worldedit.util.io.file.ZipArchiveNioSupport;
import com.sk89q.worldedit.world.DataException; import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.storage.ChunkStoreHelper; import com.sk89q.worldedit.world.storage.ChunkStoreHelper;
import com.sk89q.worldedit.world.storage.McRegionReader; import com.sk89q.worldedit.world.storage.McRegionReader;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicNode;
@ -77,8 +76,6 @@ class FileSystemSnapshotDatabaseTest {
.atZone(ZoneId.systemDefault()); .atZone(ZoneId.systemDefault());
static final ZonedDateTime TIME_TWO = TIME_ONE.minusDays(1); static final ZonedDateTime TIME_TWO = TIME_ONE.minusDays(1);
private static Path TEMP_DIR;
@BeforeAll @BeforeAll
static void setUpStatic() throws IOException, DataException { static void setUpStatic() throws IOException, DataException {
try (InputStream in = Resources.getResource("world_region.mca.gzip").openStream(); try (InputStream in = Resources.getResource("world_region.mca.gzip").openStream();
@ -107,17 +104,10 @@ class FileSystemSnapshotDatabaseTest {
} finally { } finally {
reader.close(); 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 { 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 { private static void deleteTree(Path root) throws IOException {
@ -185,6 +175,7 @@ class FileSystemSnapshotDatabaseTest {
try { try {
Path dbRoot = root.resolve("snapshots"); Path dbRoot = root.resolve("snapshots");
Files.createDirectories(dbRoot); 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)); return type.getNamedTests(new FSSDContext(nioSupport, dbRoot));
} catch (Throwable t) { } catch (Throwable t) {
deleteTree(root); deleteTree(root);

View File

@ -61,7 +61,16 @@ tasks.named<Copy>("processResources") {
} }
} }
<<<<<<< HEAD
addJarManifest(includeClasspath = true) addJarManifest(includeClasspath = true)
=======
tasks.named<Jar>("jar") {
manifest {
attributes("Class-Path" to CLASSPATH,
"WorldEdit-Version" to project.version)
}
}
>>>>>>> 18a55bc14... Add new experimental snapshot API (#524)
tasks.named<ShadowJar>("shadowJar") { tasks.named<ShadowJar>("shadowJar") {
archiveClassifier.set("dist-dev") archiveClassifier.set("dist-dev")

View File

@ -87,6 +87,7 @@ tasks.named<ShadowJar>("shadowJar") {
include(dependency("org.slf4j:slf4j-api")) include(dependency("org.slf4j:slf4j-api"))
include(dependency("org.apache.logging.log4j:log4j-slf4j-impl")) include(dependency("org.apache.logging.log4j:log4j-slf4j-impl"))
include(dependency("de.schlichtherle:truezip")) include(dependency("de.schlichtherle:truezip"))
include(dependency("net.java.truevfs:truevfs-profile-default_2.13"))
include(dependency("org.mozilla:rhino")) include(dependency("org.mozilla:rhino"))
} }
minimize { minimize {

View File

@ -25,7 +25,16 @@ sponge {
} }
} }
<<<<<<< HEAD
addJarManifest(includeClasspath = true) addJarManifest(includeClasspath = true)
=======
tasks.named<Jar>("jar") {
manifest {
attributes("Class-Path" to CLASSPATH,
"WorldEdit-Version" to project.version)
}
}
>>>>>>> 18a55bc14... Add new experimental snapshot API (#524)
tasks.named<ShadowJar>("shadowJar") { tasks.named<ShadowJar>("shadowJar") {
dependencies { dependencies {

View File

@ -122,9 +122,8 @@ public class ConfigurateConfiguration extends LocalConfiguration {
showHelpInfo = node.getNode("show-help-on-first-use").getBoolean(true); showHelpInfo = node.getNode("show-help-on-first-use").getBoolean(true);
String snapshotsDir = node.getNode("snapshots", "directory").getString(""); String snapshotsDir = node.getNode("snapshots", "directory").getString("");
if (!snapshotsDir.isEmpty()) { boolean experimentalSnapshots = node.getNode("snapshots", "experimental").getBoolean(false);
snapshotRepo = new SnapshotRepository(snapshotsDir); initializeSnapshotConfiguration(snapshotsDir, experimentalSnapshots);
}
String type = node.getNode("shell-save-type").getString("").trim(); String type = node.getNode("shell-save-type").getString("").trim();
shellSaveType = type.equals("") ? null : type; shellSaveType = type.equals("") ? null : type;