Mirror von
https://github.com/IntellectualSites/FastAsyncWorldEdit.git
synchronisiert 2024-12-26 11:00:04 +01:00
Properly close all files when dealing with archives (#1274)
* Properly close all files when dealing with archives * Move file utils to SafeFiles class * Licenses (cherry picked from commit a600266d41151eec4f2239cf90e202bb99fa3a8b)
Dieser Commit ist enthalten in:
Ursprung
8d1efcfb21
Commit
374ad992a2
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* WorldEdit, a Minecraft world manipulation toolkit
|
||||||
|
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||||
|
* Copyright (C) WorldEdit team and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.util.function;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I/O function type.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface IOFunction<T, R> {
|
||||||
|
|
||||||
|
static <T, R> Function<T, R> unchecked(IOFunction<T, R> function) {
|
||||||
|
return param -> {
|
||||||
|
try {
|
||||||
|
return function.apply(param);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
R apply(T param) throws IOException;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* WorldEdit, a Minecraft world manipulation toolkit
|
||||||
|
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||||
|
* Copyright (C) WorldEdit team and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.util.io.file;
|
||||||
|
|
||||||
|
import java.io.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();
|
||||||
|
|
||||||
|
}
|
@ -35,6 +35,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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* WorldEdit, a Minecraft world manipulation toolkit
|
||||||
|
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||||
|
* Copyright (C) WorldEdit team and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.util.io.file;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Instead, it immediately consumes the entire listing into a {@link List} and
|
||||||
|
* calls {@link List#stream()}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Path> noLeakFileList(Path dir) throws IOException {
|
||||||
|
try (Stream<Path> 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() {
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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,110 +199,97 @@ 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
|
||||||
|
- 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 <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.
|
||||||
- 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,
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* WorldEdit, a Minecraft world manipulation toolkit
|
||||||
|
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||||
|
* Copyright (C) WorldEdit team and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.world.snapshot.experimental.fs;
|
||||||
|
|
||||||
|
import com.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;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
|
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;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context class for using a {@link FileSystemSnapshotDatabase}.
|
||||||
|
*/
|
||||||
|
class FSSDContext {
|
||||||
|
|
||||||
|
final ArchiveNioSupport archiveNioSupport;
|
||||||
|
final FileSystemSnapshotDatabase db;
|
||||||
|
|
||||||
|
FSSDContext(ArchiveNioSupport archiveNioSupport, Path root) {
|
||||||
|
this.archiveNioSupport = archiveNioSupport;
|
||||||
|
this.db = new FileSystemSnapshotDatabase(root, archiveNioSupport);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path(String first, String... more) {
|
||||||
|
Path p = db.getRoot().resolve(Paths.get(first, more));
|
||||||
|
checkArgument(p.startsWith(db.getRoot()), "Escaping root!");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI nameUri(String name) {
|
||||||
|
return FileSystemSnapshotDatabase.createUri(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot requireSnapshot(String name) throws IOException {
|
||||||
|
return requireSnapshot(name, db.getSnapshot(nameUri(name)).orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot requireListsSnapshot(String name) throws IOException {
|
||||||
|
// World name is the last element of the path
|
||||||
|
String worldName = Paths.get(name).getFileName().toString();
|
||||||
|
// Without an extension
|
||||||
|
worldName = worldName.split("\\.")[0];
|
||||||
|
List<Snapshot> snapshots;
|
||||||
|
try (Stream<Snapshot> 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) throws IOException {
|
||||||
|
assertNotNull(snapshot, "No snapshot for " + name);
|
||||||
|
try {
|
||||||
|
assertEquals(name, snapshot.getInfo().getDisplayName());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Closer closer = Closer.create();
|
||||||
|
closer.register(snapshot);
|
||||||
|
throw closer.rethrowAndClose(t);
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArchiveDir getRootOfArchive(Path archive) throws IOException {
|
||||||
|
return archiveNioSupport.tryOpenAsDir(archive)
|
||||||
|
.orElseThrow(() -> new AssertionError("No archive opener for " + archive));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,291 @@
|
|||||||
|
/*
|
||||||
|
* WorldEdit, a Minecraft world manipulation toolkit
|
||||||
|
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||||
|
* Copyright (C) WorldEdit team and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.world.snapshot.experimental.fs;
|
||||||
|
|
||||||
|
import com.google.common.collect.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;
|
||||||
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_POS;
|
||||||
|
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.CHUNK_TAG;
|
||||||
|
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.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;
|
||||||
|
import static com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabaseTest.WORLD_BETA;
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
|
||||||
|
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||||
|
|
||||||
|
enum FSSDTestType {
|
||||||
|
EMPTY {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
dynamicTest("return an empty stream from getSnapshots(worldName)",
|
||||||
|
() -> context.db.getSnapshots(WORLD_ALPHA)),
|
||||||
|
dynamicTest("return an empty optional from getSnapshot(name)",
|
||||||
|
() -> context.db.getSnapshot(context.nameUri(WORLD_ALPHA)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WORLD_ONLY_DIR {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path worldFolder = EntryMaker.WORLD_DIR.createEntry(context.db.getRoot(), WORLD_ALPHA);
|
||||||
|
Files.setLastModifiedTime(worldFolder, FileTime.from(TIME_ONE.toInstant()));
|
||||||
|
return singleSnapTest(context, WORLD_ALPHA, TIME_ONE);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WORLD_ONLY_DIM_DIR {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) throws IOException {
|
||||||
|
int dim = ThreadLocalRandom.current().nextInt();
|
||||||
|
Path worldFolder = EntryMaker.WORLD_DIM_DIR
|
||||||
|
.createEntry(context.db.getRoot(), new EntryMaker.DimInfo(WORLD_ALPHA, dim));
|
||||||
|
Files.setLastModifiedTime(worldFolder, FileTime.from(TIME_ONE.toInstant()));
|
||||||
|
return singleSnapTest(context, WORLD_ALPHA, TIME_ONE);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WORLD_ONLY_NO_REGION_DIR {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path worldFolder = EntryMaker.WORLD_NO_REGION_DIR
|
||||||
|
.createEntry(context.db.getRoot(), WORLD_ALPHA);
|
||||||
|
Files.setLastModifiedTime(worldFolder, FileTime.from(TIME_ONE.toInstant()));
|
||||||
|
return singleSnapTest(context, WORLD_ALPHA, TIME_ONE);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WORLD_LEGACY_DIR {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path worldFolder = EntryMaker.WORLD_LEGACY_DIR
|
||||||
|
.createEntry(context.db.getRoot(), WORLD_ALPHA);
|
||||||
|
Files.setLastModifiedTime(worldFolder, FileTime.from(TIME_ONE.toInstant()));
|
||||||
|
return singleSnapTest(context, WORLD_ALPHA, TIME_ONE);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WORLD_ONLY_ARCHIVE {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path worldArchive = EntryMaker.WORLD_ARCHIVE
|
||||||
|
.createEntry(context.db.getRoot(), WORLD_ALPHA);
|
||||||
|
try (ArchiveDir rootOfArchive = context.getRootOfArchive(worldArchive)) {
|
||||||
|
Files.setLastModifiedTime(
|
||||||
|
rootOfArchive.getPath(),
|
||||||
|
FileTime.from(TIME_ONE.toInstant())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return singleSnapTest(context, WORLD_ALPHA + ".zip", TIME_ONE);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TIMESTAMPED_DIR {
|
||||||
|
@Override
|
||||||
|
List<? extends DynamicNode> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path root = context.db.getRoot();
|
||||||
|
Path timestampedDir = EntryMaker.TIMESTAMPED_DIR
|
||||||
|
.createEntry(root, TIME_ONE);
|
||||||
|
EntryMaker.WORLD_DIR.createEntry(timestampedDir, WORLD_ALPHA);
|
||||||
|
EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir, WORLD_BETA);
|
||||||
|
return ImmutableList.of(
|
||||||
|
dynamicContainer("world dir",
|
||||||
|
singleSnapTest(context,
|
||||||
|
root.relativize(timestampedDir) + File.separator + WORLD_ALPHA,
|
||||||
|
TIME_ONE)
|
||||||
|
),
|
||||||
|
dynamicContainer("world archive",
|
||||||
|
singleSnapTest(context,
|
||||||
|
root.relativize(timestampedDir) + File.separator + WORLD_BETA + ".zip",
|
||||||
|
TIME_ONE)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TIMESTAMPED_ARCHIVE {
|
||||||
|
@Override
|
||||||
|
List<? extends DynamicNode> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path root = context.db.getRoot();
|
||||||
|
Path timestampedArchive = EntryMaker.TIMESTAMPED_ARCHIVE
|
||||||
|
.createEntry(root, TIME_ONE);
|
||||||
|
try (ArchiveDir timestampedDir = context.getRootOfArchive(timestampedArchive)) {
|
||||||
|
EntryMaker.WORLD_DIR.createEntry(timestampedDir.getPath(), WORLD_ALPHA);
|
||||||
|
EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDir.getPath(), WORLD_BETA);
|
||||||
|
}
|
||||||
|
return ImmutableList.of(
|
||||||
|
dynamicContainer("world dir",
|
||||||
|
singleSnapTest(context,
|
||||||
|
root.relativize(timestampedArchive) + File.separator + WORLD_ALPHA,
|
||||||
|
TIME_ONE)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WORLD_DIR_TIMESTAMPED_DIR {
|
||||||
|
@Override
|
||||||
|
List<? extends DynamicNode> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path root = context.db.getRoot();
|
||||||
|
Path timestampedDirA = EntryMaker.TIMESTAMPED_DIR
|
||||||
|
.createEntry(root.resolve(WORLD_ALPHA), TIME_ONE);
|
||||||
|
Path timestampedDirB = EntryMaker.TIMESTAMPED_DIR
|
||||||
|
.createEntry(root.resolve(WORLD_BETA), TIME_ONE);
|
||||||
|
EntryMaker.WORLD_DIR.createEntry(timestampedDirA, WORLD_ALPHA);
|
||||||
|
EntryMaker.WORLD_ARCHIVE.createEntry(timestampedDirB, WORLD_BETA);
|
||||||
|
return ImmutableList.of(
|
||||||
|
dynamicContainer("world dir",
|
||||||
|
singleSnapTest(context,
|
||||||
|
root.relativize(timestampedDirA) + File.separator + WORLD_ALPHA,
|
||||||
|
TIME_ONE)
|
||||||
|
),
|
||||||
|
dynamicContainer("world archive",
|
||||||
|
singleSnapTest(context,
|
||||||
|
root.relativize(timestampedDirB) + File.separator + WORLD_BETA + ".zip",
|
||||||
|
TIME_ONE)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TWO_TIMESTAMPED_WORLD_DIR {
|
||||||
|
@Override
|
||||||
|
List<DynamicTest> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path root = context.db.getRoot();
|
||||||
|
Path timestampedDirA = EntryMaker.TIMESTAMPED_DIR
|
||||||
|
.createEntry(root, TIME_ONE);
|
||||||
|
EntryMaker.WORLD_DIR.createEntry(timestampedDirA, WORLD_ALPHA);
|
||||||
|
Path timestampedDirB = EntryMaker.TIMESTAMPED_DIR
|
||||||
|
.createEntry(root, TIME_TWO);
|
||||||
|
EntryMaker.WORLD_DIR.createEntry(timestampedDirB, WORLD_ALPHA);
|
||||||
|
return ImmutableList.of(
|
||||||
|
dynamicTest("list both snapshots in order (newest first)", () -> {
|
||||||
|
List<Snapshot> snapshots = context.db
|
||||||
|
.getSnapshotsNewestFirst(WORLD_ALPHA).collect(toList());
|
||||||
|
assertEquals(2, snapshots.size());
|
||||||
|
assertValidSnapshot(TIME_ONE, snapshots.get(0));
|
||||||
|
assertValidSnapshot(TIME_TWO, snapshots.get(1));
|
||||||
|
}),
|
||||||
|
dynamicTest("list both snapshots in order (oldest first)", () -> {
|
||||||
|
List<Snapshot> snapshots = context.db
|
||||||
|
.getSnapshotsOldestFirst(WORLD_ALPHA).collect(toList());
|
||||||
|
assertEquals(2, snapshots.size());
|
||||||
|
assertValidSnapshot(TIME_TWO, snapshots.get(0));
|
||||||
|
assertValidSnapshot(TIME_ONE, snapshots.get(1));
|
||||||
|
}),
|
||||||
|
dynamicTest("list only 1 if getting AFTER 2", () -> {
|
||||||
|
List<Snapshot> snapshots = context.db
|
||||||
|
.getSnapshotsAfter(WORLD_ALPHA, TIME_TWO).collect(toList());
|
||||||
|
assertEquals(1, snapshots.size());
|
||||||
|
assertValidSnapshot(TIME_ONE, snapshots.get(0));
|
||||||
|
}),
|
||||||
|
dynamicTest("list only 2 if getting BEFORE 1", () -> {
|
||||||
|
List<Snapshot> snapshots = context.db
|
||||||
|
.getSnapshotsBefore(WORLD_ALPHA, TIME_ONE).collect(toList());
|
||||||
|
assertEquals(1, snapshots.size());
|
||||||
|
assertValidSnapshot(TIME_TWO, snapshots.get(0));
|
||||||
|
}),
|
||||||
|
dynamicTest("list both if AFTER time before 2", () -> {
|
||||||
|
List<Snapshot> snapshots = context.db
|
||||||
|
.getSnapshotsAfter(WORLD_ALPHA, TIME_TWO.minusSeconds(1))
|
||||||
|
.collect(toList());
|
||||||
|
assertEquals(2, snapshots.size());
|
||||||
|
// sorted newest first
|
||||||
|
assertValidSnapshot(TIME_ONE, snapshots.get(0));
|
||||||
|
assertValidSnapshot(TIME_TWO, snapshots.get(1));
|
||||||
|
}),
|
||||||
|
dynamicTest("list both if BEFORE time after 1", () -> {
|
||||||
|
List<Snapshot> snapshots = context.db
|
||||||
|
.getSnapshotsBefore(WORLD_ALPHA, TIME_ONE.plusSeconds(1))
|
||||||
|
.collect(toList());
|
||||||
|
assertEquals(2, snapshots.size());
|
||||||
|
// sorted oldest first
|
||||||
|
assertValidSnapshot(TIME_TWO, snapshots.get(0));
|
||||||
|
assertValidSnapshot(TIME_ONE, snapshots.get(1));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TWO_WORLD_ONLY_DIR {
|
||||||
|
@Override
|
||||||
|
List<? extends DynamicNode> getTests(FSSDContext context) throws IOException {
|
||||||
|
Path worldFolderA = EntryMaker.WORLD_DIR
|
||||||
|
.createEntry(context.db.getRoot(), WORLD_ALPHA);
|
||||||
|
Files.setLastModifiedTime(worldFolderA, FileTime.from(TIME_ONE.toInstant()));
|
||||||
|
Path worldFolderB = EntryMaker.WORLD_DIR
|
||||||
|
.createEntry(context.db.getRoot(), WORLD_BETA);
|
||||||
|
Files.setLastModifiedTime(worldFolderB, FileTime.from(TIME_TWO.toInstant()));
|
||||||
|
return Stream.of(
|
||||||
|
singleSnapTest(context, WORLD_ALPHA, TIME_ONE),
|
||||||
|
singleSnapTest(context, WORLD_BETA, TIME_TWO)
|
||||||
|
).flatMap(List::stream).collect(toList());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<DynamicTest> singleSnapTest(FSSDContext context, String name,
|
||||||
|
ZonedDateTime time) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
dynamicTest("return a valid snapshot for " + name, () -> {
|
||||||
|
try (Snapshot snapshot = context.requireSnapshot(name)) {
|
||||||
|
assertValidSnapshot(time, snapshot);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
dynamicTest("list a valid snapshot for " + name, () -> {
|
||||||
|
try (Snapshot snapshot = context.requireListsSnapshot(name)) {
|
||||||
|
assertValidSnapshot(time, snapshot);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertValidSnapshot(ZonedDateTime time,
|
||||||
|
Snapshot snapshot) throws IOException, DataException {
|
||||||
|
assertEquals(time, snapshot.getInfo().getDateTime());
|
||||||
|
// MCA file
|
||||||
|
assertEquals(CHUNK_TAG.toString(), snapshot.getChunkTag(CHUNK_POS).toString());
|
||||||
|
// MCR file
|
||||||
|
BlockVector3 offsetChunkPos = CHUNK_POS.add(32, 0, 32);
|
||||||
|
assertEquals(CHUNK_TAG.toString(), snapshot.getChunkTag(offsetChunkPos).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract List<? extends DynamicNode> getTests(FSSDContext context) throws IOException;
|
||||||
|
|
||||||
|
Stream<DynamicNode> getNamedTests(FSSDContext context) throws IOException {
|
||||||
|
return Stream.of(dynamicContainer(
|
||||||
|
name(),
|
||||||
|
URI.create("method:" + getClass().getName() +
|
||||||
|
"#getTests(" + FSSDContext.class.getName() + ")"),
|
||||||
|
getTests(context).stream()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* WorldEdit, a Minecraft world manipulation toolkit
|
||||||
|
* Copyright (C) sk89q <http://www.sk89q.com>
|
||||||
|
* Copyright (C) WorldEdit team and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sk89q.worldedit.world.snapshot.experimental.fs;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import com.sk89q.jnbt.CompoundTag;
|
||||||
|
import com.sk89q.worldedit.math.BlockVector2;
|
||||||
|
import com.sk89q.worldedit.math.BlockVector3;
|
||||||
|
import com.sk89q.worldedit.util.io.file.ArchiveNioSupport;
|
||||||
|
import com.sk89q.worldedit.util.io.file.ArchiveNioSupports;
|
||||||
|
import com.sk89q.worldedit.util.io.file.TrueVfsArchiveNioSupport;
|
||||||
|
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;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestFactory;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
|
||||||
|
|
||||||
|
@DisplayName("A FS Snapshot Database")
|
||||||
|
class FileSystemSnapshotDatabaseTest {
|
||||||
|
|
||||||
|
static byte[] REGION_DATA;
|
||||||
|
static byte[] CHUNK_DATA;
|
||||||
|
static CompoundTag CHUNK_TAG;
|
||||||
|
static BlockVector3 CHUNK_POS;
|
||||||
|
static final String WORLD_ALPHA = "World Alpha";
|
||||||
|
static final String WORLD_BETA = "World Beta";
|
||||||
|
|
||||||
|
static final DateTimeFormatter FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH_mm_ss");
|
||||||
|
static final ZonedDateTime TIME_ONE = Instant.parse("2018-01-01T12:00:00.00Z")
|
||||||
|
.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();
|
||||||
|
GZIPInputStream gzIn = new GZIPInputStream(in)) {
|
||||||
|
REGION_DATA = ByteStreams.toByteArray(gzIn);
|
||||||
|
}
|
||||||
|
McRegionReader reader = new McRegionReader(new ByteArrayInputStream(REGION_DATA));
|
||||||
|
try {
|
||||||
|
// Find the single chunk
|
||||||
|
BlockVector2 chunkPos = IntStream.range(0, 32).mapToObj(
|
||||||
|
x -> IntStream.range(0, 32).filter(z -> reader.hasChunk(x, z))
|
||||||
|
.mapToObj(z -> BlockVector2.at(x, z))
|
||||||
|
).flatMap(Function.identity())
|
||||||
|
.findAny()
|
||||||
|
.orElseThrow(() -> new AssertionError("No chunk in region file."));
|
||||||
|
ByteArrayOutputStream cap = new ByteArrayOutputStream();
|
||||||
|
try (InputStream in = reader.getChunkInputStream(chunkPos);
|
||||||
|
GZIPOutputStream gzOut = new GZIPOutputStream(cap)) {
|
||||||
|
ByteStreams.copy(in, gzOut);
|
||||||
|
}
|
||||||
|
CHUNK_DATA = cap.toByteArray();
|
||||||
|
CHUNK_TAG = ChunkStoreHelper.readCompoundTag(() -> new GZIPInputStream(
|
||||||
|
new ByteArrayInputStream(CHUNK_DATA)
|
||||||
|
));
|
||||||
|
CHUNK_POS = chunkPos.toBlockVector3();
|
||||||
|
} finally {
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMP_DIR = Files.createTempDirectory("worldedit-fs-snap-dbs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void afterAll() throws IOException {
|
||||||
|
deleteTree(TEMP_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path newTempDb() throws IOException {
|
||||||
|
return Files.createTempDirectory(TEMP_DIR, "db");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteTree(Path root) throws IOException {
|
||||||
|
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
Files.deleteIfExists(dir);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("makes the root directory absolute if needed")
|
||||||
|
@Test
|
||||||
|
void rootIsAbsoluteDir() throws IOException {
|
||||||
|
Path root = newTempDb();
|
||||||
|
try {
|
||||||
|
Path relative = root.getFileSystem().getPath("relative");
|
||||||
|
Files.createDirectories(relative);
|
||||||
|
FileSystemSnapshotDatabase db2 = new FileSystemSnapshotDatabase(relative,
|
||||||
|
ArchiveNioSupports.combined());
|
||||||
|
assertEquals(root.getFileSystem().getPath(".").toRealPath()
|
||||||
|
.resolve(relative), db2.getRoot());
|
||||||
|
Path absolute = root.resolve("absolute");
|
||||||
|
Files.createDirectories(absolute);
|
||||||
|
FileSystemSnapshotDatabase db3 = new FileSystemSnapshotDatabase(absolute,
|
||||||
|
ArchiveNioSupports.combined());
|
||||||
|
assertEquals(absolute, db3.getRoot());
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("with a specific NIO support:")
|
||||||
|
@TestFactory
|
||||||
|
Stream<DynamicNode> withSpecificNioSupport() {
|
||||||
|
return Stream.of(
|
||||||
|
ZipArchiveNioSupport.getInstance(), TrueVfsArchiveNioSupport.getInstance()
|
||||||
|
)
|
||||||
|
.map(nioSupport -> {
|
||||||
|
Stream<? extends DynamicNode> nodes = Stream.of(FSSDTestType.values())
|
||||||
|
.flatMap(type -> {
|
||||||
|
try {
|
||||||
|
return getTests(nioSupport, type);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return dynamicContainer(
|
||||||
|
nioSupport.getClass().getSimpleName() + ", can, for format:",
|
||||||
|
nodes
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<? extends DynamicNode> getTests(ArchiveNioSupport nioSupport,
|
||||||
|
FSSDTestType type) throws IOException {
|
||||||
|
Path root = newTempDb();
|
||||||
|
try {
|
||||||
|
Path dbRoot = root.resolve("snapshots");
|
||||||
|
Files.createDirectories(dbRoot);
|
||||||
|
return type.getNamedTests(new FSSDContext(nioSupport, dbRoot));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
deleteTree(root);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren