com.sk89q.worldedit.world.snapshot.experimental.fs.FileSystemSnapshotDatabase Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of FastAsyncWorldEdit-Core Show documentation
Show all versions of FastAsyncWorldEdit-Core Show documentation
Blazingly fast Minecraft world manipulation for artists, builders and everyone else.
/*
* 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 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.sk89q.worldedit.world.snapshot.experimental.fs;
import com.google.common.collect.ImmutableList;
import com.google.common.net.UrlEscapers;
import com.sk89q.worldedit.util.function.IOFunction;
import com.sk89q.worldedit.util.function.IORunnable;
import com.sk89q.worldedit.util.io.Closer;
import com.sk89q.worldedit.util.io.file.ArchiveDir;
import com.sk89q.worldedit.util.io.file.ArchiveNioSupport;
import com.sk89q.worldedit.util.io.file.MorePaths;
import com.sk89q.worldedit.util.io.file.SafeFiles;
import com.sk89q.worldedit.util.time.FileNameDateTimeParser;
import com.sk89q.worldedit.util.time.ModificationDateTimeParser;
import com.sk89q.worldedit.util.time.SnapshotDateTimeParser;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotDatabase;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
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.stream.Stream;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Implements a snapshot database based on a filesystem.
*/
public class FileSystemSnapshotDatabase implements SnapshotDatabase {
private static final String SCHEME = "snapfs";
private static final List DATE_TIME_PARSERS =
new ImmutableList.Builder()
.add(FileNameDateTimeParser.getInstance())
.addAll(ServiceLoader.load(SnapshotDateTimeParser.class))
.add(ModificationDateTimeParser.getInstance())
.build();
public static ZonedDateTime tryParseDate(Path path) {
return tryParseDateInternal(path)
.orElseThrow(() -> new IllegalStateException("Could not detect date of " + path));
}
private static Optional tryParseDateInternal(Path path) {
return DATE_TIME_PARSERS.stream()
.map(parser -> parser.detectDateTime(path))
.filter(Objects::nonNull)
.findFirst();
}
public static URI createUri(String name) {
return URI.create(SCHEME + ":" + UrlEscapers.urlFragmentEscaper().escape(name));
}
public static FileSystemSnapshotDatabase maybeCreate(
Path root,
ArchiveNioSupport archiveNioSupport
) throws IOException {
Files.createDirectories(root);
return new FileSystemSnapshotDatabase(root, archiveNioSupport);
}
private final Path root;
private final ArchiveNioSupport archiveNioSupport;
public FileSystemSnapshotDatabase(Path root, ArchiveNioSupport archiveNioSupport) {
checkArgument(Files.isDirectory(root), "Database root is not a directory");
try {
this.root = root.toRealPath();
} catch (IOException e) {
throw new RuntimeException("Failed to resolve snapshot database path", e);
}
this.archiveNioSupport = archiveNioSupport;
}
/*
* 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 idPath, Path ioPath, @Nullable Closer closeCallback) {
return new FolderSnapshot(
createSnapshotInfo(idPath, ioPath), ioPath, closeCallback
);
}
public Path getRoot() {
return root;
}
@Override
public String getScheme() {
return SCHEME;
}
@Override
public Optional getSnapshot(URI name) throws IOException {
if (!name.getScheme().equals(SCHEME)) {
return Optional.empty();
}
return getSnapshot(name.getSchemeSpecificPart());
}
private Optional getSnapshot(String id) throws IOException {
Path rawResolved = root.resolve(id);
// Catch trickery with paths:
Path ioPath = rawResolved.normalize();
if (!ioPath.startsWith(root)) {
return Optional.empty();
}
Path idPath = root.relativize(ioPath);
Optional result = tryRegularFileSnapshot(idPath);
if (result.isPresent()) {
return result;
}
if (!Files.isDirectory(ioPath)) {
return Optional.empty();
}
return Optional.of(createSnapshot(idPath, ioPath, null));
}
private Optional tryRegularFileSnapshot(Path idPath) throws IOException {
Closer closer = Closer.create();
Path root = this.root;
Path relative = idPath;
Iterator iterator = null;
try {
while (true) {
if (iterator == null) {
iterator = MorePaths.iterPaths(relative).iterator();
}
if (!iterator.hasNext()) {
closer.close();
return Optional.empty();
}
Path relativeNext = iterator.next();
Path next = root.resolve(relativeNext);
if (!Files.isRegularFile(next)) {
// This will never be it.
continue;
}
Optional newRootOpt = archiveNioSupport.tryOpenAsDir(next);
if (newRootOpt.isPresent()) {
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(idPath, relative, closer));
}
// Otherwise, we may have more archives to open.
// Keep searching!
}
}
} catch (Throwable t) {
throw closer.rethrowAndClose(t);
}
}
@Override
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.
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 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 listTimestampedEntries(String worldName, Path directory) throws IOException {
return SafeFiles.noLeakFileList(directory)
.flatMap(IOFunction.unchecked(entry -> getTimestampedEntries(worldName, entry)));
}
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();
}
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 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;
}
}