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 338a7a0b0..a0dc747e4 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,6 +20,7 @@ package com.sk89q.worldedit.util.function; import java.io.IOException; +import java.io.UncheckedIOException; /** * I/O runnable type. @@ -27,6 +28,16 @@ import java.io.IOException; @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 605aaaf04..1c435f7b2 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 @@ -34,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/ArchiveNioSupports.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveNioSupports.java index ae80431a5..e47f00a1f 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 e3b3527ee..1c3205ff9 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,6 +22,7 @@ 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; @@ -45,15 +46,28 @@ 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(); } - TPath root = new TPath(archive).getFileSystem().getPath("/"); - return Optional.of(ArchiveNioSupports.skipRootSameName( + TFileSystem fileSystem = new TPath(archive).getFileSystem(); + TPath root = fileSystem.getPath("/"); + Path realRoot = 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 069965a5c..8fa41d994 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,17 +37,28 @@ 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() ); - return Optional.of(ArchiveNioSupports.skipRootSameName( + Path root = 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 6b45a0c29..64febdd66 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,34 +21,31 @@ 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; @@ -58,8 +55,6 @@ 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 = @@ -102,15 +97,24 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { this.archiveNioSupport = archiveNioSupport; } - 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); + /* + * 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 Snapshot createSnapshot(Path fullPath, Path realPath, @Nullable IORunnable closeCallback) { + private Snapshot createSnapshot(Path idPath, Path ioPath, @Nullable Closer closeCallback) { 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)) { return Optional.empty(); } - // drop the / in the path to make it absolute - Path rawResolved = root.resolve(name.getSchemeSpecificPart()); + return getSnapshot(name.getSchemeSpecificPart()); + } + + private Optional getSnapshot(String id) throws IOException { + Path rawResolved = root.resolve(id); // Catch trickery with paths: - Path realPath = rawResolved.normalize(); - if (!realPath.startsWith(root)) { + Path ioPath = rawResolved.normalize(); + if (!ioPath.startsWith(root)) { return Optional.empty(); } - Optional result = tryRegularFileSnapshot(root.relativize(realPath), realPath); + Path idPath = root.relativize(ioPath); + Optional result = tryRegularFileSnapshot(idPath); if (result.isPresent()) { return result; } - if (!Files.isDirectory(realPath)) { + if (!Files.isDirectory(ioPath)) { return Optional.empty(); } - return Optional.of(createSnapshot(root.relativize(realPath), realPath, null)); + return Optional.of(createSnapshot(idPath, ioPath, null)); } - private Optional tryRegularFileSnapshot(Path fullPath, Path realPath) throws IOException { + private Optional tryRegularFileSnapshot(Path idPath) throws IOException { Closer closer = Closer.create(); Path root = this.root; - Path relative = root.relativize(realPath); + Path relative = idPath; Iterator iterator = null; try { while (true) { @@ -156,6 +164,7 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { iterator = MorePaths.iterPaths(relative).iterator(); } if (!iterator.hasNext()) { + closer.close(); return Optional.empty(); } Path relativeNext = iterator.next(); @@ -164,18 +173,17 @@ public class FileSystemSnapshotDatabase implements SnapshotDatabase { // This will never be it. continue; } - Optional newRootOpt = archiveNioSupport.tryOpenAsDir(next); + Optional newRootOpt = archiveNioSupport.tryOpenAsDir(next); if (newRootOpt.isPresent()) { - root = newRootOpt.get(); - if (root.getFileSystem() != FileSystems.getDefault()) { - closer.register(root.getFileSystem()); - } + ArchiveDir archiveDir = newRootOpt.get(); + root = archiveDir.getPath(); + closer.register(archiveDir); // 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(fullPath, relative, closer::close)); + return Optional.of(createSnapshot(idPath, relative, closer)); } // Otherwise, we may have more archives to open. // Keep searching! @@ -191,119 +199,96 @@ 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 Stream.of( - listWorldEntries(Paths.get(""), root, worldName), - listTimestampedEntries(Paths.get(""), root, worldName) - ).flatMap(Function.identity()); + return SafeFiles.noLeakFileList(root) + .flatMap(IOFunction.unchecked(entry -> { + String worldEntry = getWorldEntry(worldName, entry); + 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 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); - } - } - // 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 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); + 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(); } - 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 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 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); - } - }); + 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; } } 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 b14c61882..f753d2507 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.function.IORunnable; +import com.sk89q.worldedit.util.io.Closer; 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 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 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.run(); + closeCallback.close(); } } } 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 0c34a2a71..d63ff3936 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,6 +19,8 @@ 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; @@ -28,6 +30,7 @@ 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; @@ -67,19 +70,34 @@ class FSSDContext { String worldName = Paths.get(name).getFileName().toString(); // Without an extension worldName = worldName.split("\\.")[0]; - 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)); + 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); + } } - Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) { + Snapshot requireSnapshot(String name, @Nullable Snapshot snapshot) throws IOException { 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; } - Path getRootOfArchive(Path archive) throws IOException { + ArchiveDir 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 d470afcd8..25c5c4317 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,6 +21,7 @@ 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; @@ -29,7 +30,6 @@ 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,9 @@ 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_TAG; + 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_TWO; import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.WORLD_ALPHA; @@ -102,16 +103,11 @@ enum FSSDTestType { List getTests(FSSDContext context) throws IOException { Path worldArchive = EntryMaker.WORLD_ARCHIVE .createEntry(context.db.getRoot(), WORLD_ALPHA); - Path rootOfArchive = context.getRootOfArchive(worldArchive); - try { + try (ArchiveDir rootOfArchive = context.getRootOfArchive(worldArchive)) { Files.setLastModifiedTime( - rootOfArchive, + rootOfArchive.getPath(), FileTime.from(TIME_ONE.toInstant()) ); - } finally { - if (rootOfArchive.getFileSystem() != FileSystems.getDefault()) { - rootOfArchive.getFileSystem().close(); - } } return singleSnapTest(context, WORLD_ALPHA + ".zip", TIME_ONE); } @@ -144,14 +140,9 @@ enum FSSDTestType { Path root = context.db.getRoot(); Path timestampedArchive = EntryMaker.TIMESTAMPED_ARCHIVE .createEntry(root, TIME_ONE); - 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(); - } + try (ArchiveDir timestampedDir = context.getRootOfArchive(timestampedArchive)) { + EntryMaker.WORLD_DIR.createEntry(timestampedDir.getPath(), WORLD_ALPHA); + EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir.getPath(), WORLD_BETA); } return ImmutableList.of( dynamicContainer("world dir", @@ -261,16 +252,18 @@ enum FSSDTestType { } }; - private static List singleSnapTest(FSSDContext context, String name, + List singleSnapTest(FSSDContext context, String name, ZonedDateTime time) { return ImmutableList.of( dynamicTest("return a valid snapshot for " + name, () -> { - Snapshot snapshot = context.requireSnapshot(name); - assertValidSnapshot(time, snapshot); + try (Snapshot snapshot = context.requireSnapshot(name)) { + assertValidSnapshot(time, snapshot); + } }), dynamicTest("list a valid snapshot for " + name, () -> { - Snapshot snapshot = context.requireListsSnapshot(name); - assertValidSnapshot(time, snapshot); + try (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 cb93f294a..b79c88675 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,6 +31,7 @@ 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; @@ -76,6 +77,8 @@ 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(); @@ -104,10 +107,17 @@ 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("worldedit-fs-snap-db"); + return Files.createTempDirectory(TEMP_DIR, "db"); } private static void deleteTree(Path root) throws IOException { @@ -175,7 +185,6 @@ 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);