diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IOFunction.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IOFunction.java new file mode 100644 index 000000000..dd97df39b --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/function/IOFunction.java @@ -0,0 +1,44 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.function; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Function; + +/** + * I/O function type. + */ +@FunctionalInterface +public interface IOFunction { + + static Function unchecked(IOFunction function) { + return param -> { + try { + return function.apply(param); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } + + R apply(T param) throws IOException; + +} 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 @@ @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/ArchiveDir.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveDir.java new file mode 100644 index 000000000..b0e48502f --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/ArchiveDir.java @@ -0,0 +1,33 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.io.file; + +import java.io.Closeable; +import java.nio.file.Path; + +/** + * Represents an archive opened as a directory. This must be closed after work on the Path is + * done. + */ +public interface ArchiveDir extends Closeable { + + Path getPath(); + +} 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 70c9474bf..5705689ee 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 @@ -35,6 +35,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/SafeFiles.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/SafeFiles.java new file mode 100644 index 000000000..e790f0d30 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/SafeFiles.java @@ -0,0 +1,69 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.io.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SafeFiles { + + /** + * A version of {@link Files#list(Path)} that won't leak resources. + * + *

+ * Instead, it immediately consumes the entire listing into a {@link List} and + * calls {@link List#stream()}. + *

+ * + * @param dir the directory to list + * @return an I/O-resource-free stream of the files in the directory + * @throws IOException if an I/O error occurs + */ + public static Stream noLeakFileList(Path dir) throws IOException { + try (Stream stream = Files.list(dir)) { + return stream.collect(Collectors.toList()).stream(); + } + } + + /** + * {@link Path#getFileName()} includes a slash sometimes for some reason. + * This will get rid of it. + * + * @param path the path to get the file name for + * @return the file name of the given path + */ + public static String canonicalFileName(Path path) { + return dropSlash(path.getFileName().toString()); + } + + private static String dropSlash(String name) { + if (name.isEmpty() || name.codePointBefore(name.length()) != '/') { + return name; + } + return name.substring(0, name.length() - 1); + } + + private SafeFiles() { + } +} 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 @@ 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 @@ private TrueVfsArchiveNioSupport() { } @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 @@ private ZipArchiveNioSupport() { } @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 b37995c34..9fceb1dd4 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 @@ 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 @@ */ 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 FileSystemSnapshotDatabase(Path root, 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. - 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 Optional getSnapshot(URI name) throws IOException { 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 @@ private Optional tryRegularFileSnapshot(Path fullPath, Path realPath) iterator = MorePaths.iterPaths(relative).iterator(); } if (!iterator.hasNext()) { + closer.close(); return Optional.empty(); } Path relativeNext = iterator.next(); @@ -164,18 +173,17 @@ private Optional tryRegularFileSnapshot(Path fullPath, Path realPath) // 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,110 +199,97 @@ public Stream getSnapshots(String worldName) throws IOException { /* There are a few possible snapshot formats we accept: - a world directory, identified by /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 + formats inside of it! - 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. - - a directory with the world name, but no level.dat - - inside must be timestamped directory/archive, with the world inside that 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 @@ 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 @@ private static Object getRegionFolder(Path folder) throws IOException { 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 CompoundTag getChunkTag(BlockVector3 position) throws DataException, IOEx @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.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 @@ Snapshot requireListsSnapshot(String name) throws IOException { 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..259f10650 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 @@ 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 java.io.File; import java.io.IOException; import java.net.URI; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; @@ -38,8 +38,8 @@ import java.util.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 +102,11 @@ List getTests(FSSDContext context) throws IOException { 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 +139,9 @@ List getTests(FSSDContext context) throws IOException { 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 +251,18 @@ List getTests(FSSDContext context) throws IOException { } }; - 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 9f9e33859..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 @@ -33,7 +33,6 @@ import com.sk89q.worldedit.world.storage.McRegionReader; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.Test; @@ -62,7 +61,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; -@Disabled("Fails on Windows due to file handle issues.") @DisplayName("A FS Snapshot Database") class FileSystemSnapshotDatabaseTest {