Accidentally fixed the EntryMaker issue before I pulled 374ad99. Should reflect upstream now.
This commit is contained in:
Octavia Togami 2020-04-05 12:17:26 -04:00 committed by IronApollo
parent 8b97a11fa4
commit 415e91b519
10 changed files with 209 additions and 168 deletions

View File

@ -20,6 +20,7 @@
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.
@ -27,6 +28,16 @@ import java.io.IOException;
@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

@ -34,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<Path> tryOpenAsDir(Path archive) throws IOException; Optional<ArchiveDir> tryOpenAsDir(Path archive) throws IOException;
} }

View File

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

View File

@ -22,6 +22,7 @@ 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;
@ -45,15 +46,28 @@ public final class TrueVfsArchiveNioSupport implements ArchiveNioSupport {
} }
@Override @Override
public Optional<Path> tryOpenAsDir(Path archive) throws IOException { public Optional<ArchiveDir> 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.contains(fileName.substring(dot + 1))) { if (dot < 0 || dot >= fileName.length() || !ALLOWED_EXTENSIONS
.contains(fileName.substring(dot + 1))) {
return Optional.empty(); return Optional.empty();
} }
TPath root = new TPath(archive).getFileSystem().getPath("/"); TFileSystem fileSystem = new TPath(archive).getFileSystem();
return Optional.of(ArchiveNioSupports.skipRootSameName( TPath root = fileSystem.getPath("/");
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,17 +37,28 @@ public final class ZipArchiveNioSupport implements ArchiveNioSupport {
} }
@Override @Override
public Optional<Path> tryOpenAsDir(Path archive) throws IOException { public Optional<ArchiveDir> 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()
); );
return Optional.of(ArchiveNioSupports.skipRootSameName( Path root = 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,34 +21,31 @@ 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;
@ -58,8 +55,6 @@ 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 =
@ -102,15 +97,24 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
this.archiveNioSupport = archiveNioSupport; this.archiveNioSupport = archiveNioSupport;
} }
private SnapshotInfo createSnapshotInfo(Path fullPath, Path realPath) { /*
// Try full for parsing out of file name, real for parsing mod time. * When this code says "idPath" it is the path that uniquely identifies that snapshot.
ZonedDateTime date = tryParseDateInternal(fullPath).orElseGet(() -> tryParseDate(realPath)); * A snapshot can be looked up by its idPath.
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 fullPath, Path realPath, @Nullable IORunnable closeCallback) { private Snapshot createSnapshot(Path idPath, Path ioPath, @Nullable Closer closeCallback) {
return new FolderSnapshot( return new FolderSnapshot(
createSnapshotInfo(fullPath, realPath), realPath, closeCallback createSnapshotInfo(idPath, ioPath), ioPath, closeCallback
); );
} }
@ -128,27 +132,31 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
if (!name.getScheme().equals(SCHEME)) { if (!name.getScheme().equals(SCHEME)) {
return Optional.empty(); return Optional.empty();
} }
// drop the / in the path to make it absolute return getSnapshot(name.getSchemeSpecificPart());
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 realPath = rawResolved.normalize(); Path ioPath = rawResolved.normalize();
if (!realPath.startsWith(root)) { if (!ioPath.startsWith(root)) {
return Optional.empty(); return Optional.empty();
} }
Optional<Snapshot> result = tryRegularFileSnapshot(root.relativize(realPath), realPath); Path idPath = root.relativize(ioPath);
Optional<Snapshot> result = tryRegularFileSnapshot(idPath);
if (result.isPresent()) { if (result.isPresent()) {
return result; return result;
} }
if (!Files.isDirectory(realPath)) { if (!Files.isDirectory(ioPath)) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(createSnapshot(root.relativize(realPath), realPath, null)); return Optional.of(createSnapshot(idPath, ioPath, null));
} }
private Optional<Snapshot> tryRegularFileSnapshot(Path fullPath, Path realPath) throws IOException { private Optional<Snapshot> tryRegularFileSnapshot(Path idPath) throws IOException {
Closer closer = Closer.create(); Closer closer = Closer.create();
Path root = this.root; Path root = this.root;
Path relative = root.relativize(realPath); Path relative = idPath;
Iterator<Path> iterator = null; Iterator<Path> iterator = null;
try { try {
while (true) { while (true) {
@ -156,6 +164,7 @@ 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();
@ -164,18 +173,17 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase {
// This will never be it. // This will never be it.
continue; continue;
} }
Optional<Path> newRootOpt = archiveNioSupport.tryOpenAsDir(next); Optional<ArchiveDir> newRootOpt = archiveNioSupport.tryOpenAsDir(next);
if (newRootOpt.isPresent()) { if (newRootOpt.isPresent()) {
root = newRootOpt.get(); ArchiveDir archiveDir = newRootOpt.get();
if (root.getFileSystem() != FileSystems.getDefault()) { root = archiveDir.getPath();
closer.register(root.getFileSystem()); closer.register(archiveDir);
}
// 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(fullPath, relative, closer::close)); return Optional.of(createSnapshot(idPath, relative, closer));
} }
// Otherwise, we may have more archives to open. // Otherwise, we may have more archives to open.
// Keep searching! // Keep searching!
@ -191,119 +199,96 @@ 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 Stream.of( return SafeFiles.noLeakFileList(root)
listWorldEntries(Paths.get(""), root, worldName), .flatMap(IOFunction.unchecked(entry -> {
listTimestampedEntries(Paths.get(""), root, worldName) String worldEntry = getWorldEntry(worldName, entry);
).flatMap(Function.identity()); if (worldEntry != null) {
return Stream.of(worldEntry);
}
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);
}
return getTimestampedEntries(worldName, entry);
}))
.map(IOFunction.unchecked(id ->
getSnapshot(id)
.orElseThrow(() ->
new AssertionError("Could not find discovered snapshot: " + id)
)
));
} }
private Stream<Snapshot> listWorldEntries(Path fullPath, Path root, String worldName) throws IOException { private Stream<String> listTimestampedEntries(String worldName, Path directory) throws IOException {
logger.debug("World check in: {}", root); return SafeFiles.noLeakFileList(directory)
return Files.list(root) .flatMap(IOFunction.unchecked(entry -> getTimestampedEntries(worldName, entry)));
.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);
}
}
// 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);
}
}
logger.debug("Nothing!");
return null;
});
} }
private boolean isSameDirectoryName(String fileName, String worldName) { private Stream<String> getTimestampedEntries(String worldName, Path entry) throws IOException {
if (fileName.lastIndexOf('/') == fileName.length() - 1) { ZonedDateTime dateTime = FileNameDateTimeParser.getInstance().detectDateTime(entry);
fileName = fileName.substring(0, fileName.length() - 1); if (dateTime == null) {
// nothing available at this path
return Stream.of();
} }
return fileName.equalsIgnoreCase(worldName); 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<ArchiveDir> 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<Snapshot> listTimestampedEntries(Path fullPath, Path root, String worldName) throws IOException { private Stream<String> listWorldEntries(String worldName, Path directory) throws IOException {
logger.debug("Timestamp check in: {}", root); return SafeFiles.noLeakFileList(directory)
return Files.list(root) .map(IOFunction.unchecked(entry -> getWorldEntry(worldName, entry)))
.filter(candidate -> { .filter(Objects::nonNull);
ZonedDateTime date = FileNameDateTimeParser.getInstance().detectDateTime(candidate); }
return date != null;
}) private String getWorldEntry(String worldName, Path entry) throws IOException {
.flatMap(candidate -> { String fileName = SafeFiles.canonicalFileName(entry);
logger.debug("Timestamp trying: {}", candidate); if (fileName.equals(worldName) && Files.exists(entry.resolve("level.dat"))) {
// Try timestamped directory // world directory
if (Files.isDirectory(candidate)) { return worldName;
logger.debug("Timestamped directory"); }
try { if (fileName.startsWith(worldName + ".") && Files.isRegularFile(entry)) {
return listWorldEntries( Optional<ArchiveDir> asArchive = archiveNioSupport.tryOpenAsDir(entry);
fullPath.resolve(candidate.getFileName().toString()), candidate, worldName if (asArchive.isPresent()) {
); // world archive
} catch (IOException e) { asArchive.get().close();
throw new UncheckedIOException(e); return fileName;
} }
} }
// Otherwise archive, get it as a directory & unpack it return null;
try {
Optional<Path> 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);
}
});
} }
} }

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.function.IORunnable; import com.sk89q.worldedit.util.io.Closer;
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 IORunnable closeCallback; private final @Nullable Closer closeCallback;
public FolderSnapshot(SnapshotInfo info, Path folder, @Nullable IORunnable closeCallback) { public FolderSnapshot(SnapshotInfo info, Path folder, @Nullable Closer 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.run(); closeCallback.close();
} }
} }
} }

View File

@ -19,6 +19,8 @@
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;
@ -28,6 +30,7 @@ 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;
@ -67,19 +70,34 @@ 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 = db.getSnapshots(worldName).collect(toList()); List<Snapshot> snapshots;
assertTrue(1 >= snapshots.size(), try (Stream<Snapshot> snapshotStream = db.getSnapshots(worldName)) {
"Too many snapshots matched for " + worldName); snapshots = snapshotStream.collect(toList());
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) { Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) throws IOException {
assertNotNull(snapshot, "No snapshot for " + name); assertNotNull(snapshot, "No snapshot for " + name);
assertEquals(name, snapshot.getInfo().getDisplayName()); try {
assertEquals(name, snapshot.getInfo().getDisplayName());
} catch (Throwable t) {
Closer closer = Closer.create();
closer.register(snapshot);
throw closer.rethrowAndClose(t);
}
return snapshot; return snapshot;
} }
Path getRootOfArchive(Path archive) throws IOException { ArchiveDir 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,6 +21,7 @@ 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;
@ -29,7 +30,6 @@ 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,9 @@ 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_TAG;
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_POS; 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.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,16 +103,11 @@ 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);
Path rootOfArchive = context.getRootOfArchive(worldArchive); try (ArchiveDir rootOfArchive = context.getRootOfArchive(worldArchive)) {
try {
Files.setLastModifiedTime( Files.setLastModifiedTime(
rootOfArchive, rootOfArchive.getPath(),
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);
} }
@ -144,14 +140,9 @@ 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);
Path timestampedDir = context.getRootOfArchive(timestampedArchive); try (ArchiveDir timestampedDir = context.getRootOfArchive(timestampedArchive)) {
try { EntryMaker.WORLD_DIR.createEntry(timestampedDir.getPath(), WORLD_ALPHA);
EntryMaker.WORLD_DIR.createEntry(timestampedDir, WORLD_ALPHA); EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir.getPath(), WORLD_BETA);
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",
@ -261,16 +252,18 @@ enum FSSDTestType {
} }
}; };
private static List<DynamicTest> singleSnapTest(FSSDContext context, String name, 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, () -> {
Snapshot snapshot = context.requireSnapshot(name); try (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, () -> {
Snapshot snapshot = context.requireListsSnapshot(name); try (Snapshot snapshot = context.requireListsSnapshot(name)) {
assertValidSnapshot(time, snapshot); assertValidSnapshot(time, snapshot);
}
}) })
); );
} }

View File

@ -31,6 +31,7 @@ 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;
@ -76,6 +77,8 @@ 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();
@ -104,10 +107,17 @@ 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("worldedit-fs-snap-db"); return Files.createTempDirectory(TEMP_DIR, "db");
} }
private static void deleteTree(Path root) throws IOException { private static void deleteTree(Path root) throws IOException {
@ -175,7 +185,6 @@ 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);