Skip to content

Commit 5c95693

Browse files
committed
WIP Handle world reading in one place and abstract WorldProviders to only reading files
1 parent e989652 commit 5c95693

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ca.bkaw.mch.world;
2+
3+
import ca.bkaw.mch.util.StringPath;
4+
import org.jetbrains.annotations.Nullable;
5+
6+
/**
7+
* A file name with optional file metadata.
8+
*
9+
* @param name The name of the file or directory.
10+
* @param path The path to the file relative to the provider's root.
11+
* @param metadata Metadata about the file or directory.
12+
*/
13+
public record FileInfo(String name, StringPath path, @Nullable Metadata metadata) {
14+
/**
15+
* Metadata about a file or directory.
16+
*
17+
* @param isFile {@code true} if a regular file.
18+
* @param isDirectory {@code true} if a directory.
19+
* @param fileSize The file size of the file. May be undefined for directories.
20+
* @param lastModified The last modification time of the file, measured in epoch milliseconds.
21+
*/
22+
public record Metadata(boolean isFile, boolean isDirectory, long fileSize, long lastModified) {
23+
}
24+
}

mch/src/main/java/ca/bkaw/mch/world/WorldProvider.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import ca.bkaw.mch.object.tree.Tree;
66
import ca.bkaw.mch.repository.MchRepository;
77
import ca.bkaw.mch.util.RandomAccessReader;
8+
import ca.bkaw.mch.util.StringPath;
89
import org.jetbrains.annotations.Nullable;
910

1011
import java.io.IOException;
@@ -19,6 +20,15 @@
1920
* provider object should be closed.
2021
*/
2122
public interface WorldProvider extends AutoCloseable {
23+
24+
List<FileInfo> list(StringPath path);
25+
@Nullable
26+
FileInfo.Metadata stat(StringPath path);
27+
RandomAccessReader openFile(StringPath path, long estimatedSize);
28+
byte[] readFile(StringPath path, long estimatedSize);
29+
30+
// Old
31+
2232
/**
2333
* Get the dimensions that this world has.
2434
*
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package ca.bkaw.mch.world;
2+
3+
import ca.bkaw.mch.object.ObjectStorageTypes;
4+
import ca.bkaw.mch.object.Reference20;
5+
import ca.bkaw.mch.object.blob.Blob;
6+
import ca.bkaw.mch.object.dimension.Dimension;
7+
import ca.bkaw.mch.object.tree.Tree;
8+
import ca.bkaw.mch.repository.MchRepository;
9+
import ca.bkaw.mch.util.RandomAccessReader;
10+
import ca.bkaw.mch.util.StringPath;
11+
import ca.bkaw.mch.util.Util;
12+
import ca.bkaw.mch.world.sftp.OutputStreamFileDest;
13+
import net.schmizz.sshj.sftp.RemoteResourceInfo;
14+
import org.jetbrains.annotations.NotNull;
15+
import org.jetbrains.annotations.Nullable;
16+
17+
import java.io.ByteArrayOutputStream;
18+
import java.io.IOException;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.function.Predicate;
23+
24+
public class WorldReader {
25+
private final WorldProvider provider;
26+
27+
public WorldReader(WorldProvider provider) {
28+
this.provider = provider;
29+
}
30+
31+
@NotNull
32+
private FileInfo.Metadata metadata(FileInfo fileInfo) {
33+
if (fileInfo.metadata() != null) {
34+
return fileInfo.metadata();
35+
}
36+
// Some providers do not include metadata when listing directories. We need
37+
// to stat to get the metadata. We know the file exists, so it's safe to
38+
// assume not null.
39+
return Objects.requireNonNull(
40+
this.provider.stat(fileInfo.path()),
41+
"File not found. Was the file deleted?"
42+
);
43+
}
44+
45+
public List<String> getDimensions() {
46+
List<FileInfo> directories = this.provider.list(StringPath.root());
47+
48+
List<String> dimensions = new ArrayList<>(3);
49+
for (FileInfo fileInfo : directories) {
50+
String dimension = switch (fileInfo.name()) {
51+
case "region" -> Dimension.OVERWORLD;
52+
case Util.NETHER_FOLDER -> Dimension.NETHER;
53+
case Util.THE_END_FOLDER -> Dimension.THE_END;
54+
default -> null;
55+
};
56+
if (dimension != null && metadata(fileInfo).isDirectory()) {
57+
dimensions.add(dimension);
58+
}
59+
}
60+
// TODO custom dimensions
61+
return dimensions;
62+
}
63+
64+
private StringPath getDimensionPath(String dimension) {
65+
return switch (dimension) {
66+
case Dimension.OVERWORLD -> StringPath.root();
67+
case Dimension.NETHER -> StringPath.of(Util.NETHER_FOLDER);
68+
case Dimension.THE_END -> StringPath.of(Util.THE_END_FOLDER);
69+
default -> StringPath.of( "dimensions/" + dimension.replace(':', '/'));
70+
};
71+
}
72+
73+
public List<RegionFileInfo> getRegionFiles(String dimension) throws IOException {
74+
StringPath path = getDimensionPath(dimension).resolve("region");
75+
if (this.provider.stat(path) == null) {
76+
return List.of();
77+
}
78+
List<RegionFileInfo> regionFiles = new ArrayList<>();
79+
80+
for (FileInfo fileInfo : this.provider.list(path)) {
81+
String fileName = fileInfo.name();
82+
if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) {
83+
continue;
84+
}
85+
FileInfo.Metadata metadata = metadata(fileInfo);
86+
87+
regionFiles.add(new RegionFileInfo(
88+
fileName, metadata.lastModified(), metadata.fileSize()
89+
));
90+
}
91+
return regionFiles;
92+
}
93+
94+
public RandomAccessReader openRegionFile(String dimension, String regionFileName, long estimatedSize) throws IOException {
95+
StringPath path = this.getDimensionPath(dimension).resolve("region").resolve(regionFileName);
96+
return this.provider.openFile(path, estimatedSize);
97+
}
98+
99+
public Reference20<Tree> trackDirectoryTree(String dimension, MchRepository repository, Predicate<String> predicate, @Nullable Tree currentTree) throws IOException {
100+
return this.trackDirectoryTreePath(this.getDimensionPath(dimension), repository, predicate, currentTree);
101+
}
102+
103+
public Reference20<Tree> trackDirectoryTreePath(StringPath path, MchRepository repository, Predicate<String> predicate, @Nullable Tree currentTree) throws IOException {
104+
Tree tree = new Tree();
105+
for (FileInfo file : this.provider.list(path)) {
106+
String name = file.name();
107+
if (!predicate.test(name)) {
108+
continue;
109+
}
110+
// TODO repository-wide "mchignore"
111+
if (name.contains("ledger.sqlite")) {
112+
continue;
113+
}
114+
FileInfo.Metadata metadata = metadata(file);
115+
if (metadata.isDirectory()) {
116+
// Track subdirectories
117+
Reference20<Tree> currentSubTreeReference = currentTree != null ? currentTree.getSubTrees().get(name) : null;
118+
Tree currentSubTree = currentSubTreeReference != null ? currentSubTreeReference.resolve(repository) : null;
119+
Reference20<Tree> subDirectoryReference = trackDirectoryTreePath(file.path(), repository, str -> true, currentSubTree);
120+
tree.addSubTree(name, subDirectoryReference);
121+
} else if (metadata.isFile()) {
122+
// Track files
123+
Tree.BlobReference currentBlobReference = currentTree != null ? currentTree.getFiles().get(name) : null;
124+
long lastModified = metadata.lastModified();
125+
if (currentBlobReference == null || currentBlobReference.lastModified() != lastModified) {
126+
// The file has changed since last commit. Save it anew.
127+
byte[] bytes = this.provider.readFile(file.path(), metadata.fileSize());
128+
Blob blob = new Blob(bytes);
129+
Reference20<Blob> blobReference = ObjectStorageTypes.BLOB.save(blob, repository);
130+
tree.addFile(name, new Tree.BlobReference(blobReference, lastModified));
131+
} else {
132+
// The file has not changed since last commit. Reuse the reference.
133+
tree.addFile(name, currentBlobReference);
134+
}
135+
}
136+
}
137+
138+
// Save the tree
139+
return ObjectStorageTypes.TREE.save(tree, repository);
140+
}
141+
142+
}

0 commit comments

Comments
 (0)