Skip to content

Commit

Permalink
Hoist some ArchiveMount logic into a new superclass
Browse files Browse the repository at this point in the history
 - Add AbstractInMemoryMount, which contains all of ArchiveMount's file
   tree logic, but not the caching functionality.

 - Convert MemoryMount to inherit from AbstractInMemoryMount.

 - Add a helper method to add a file to an AbstractInMemoryMount, and
   use that within {Resource,Jar}Mount.

There's definitely more work to be done here - it might be nice to split
FileEntry into separate Directory and File interfaces, or at least make
them slightly more immutable, but that's definitely a future job.
  • Loading branch information
SquidDev committed Sep 22, 2023
1 parent ae71eb3 commit 0d6c6e7
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 287 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);

private static final byte[] TEMP_BUFFER = new byte[8192];

/**
* Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes.
*/
Expand Down Expand Up @@ -68,7 +66,11 @@ private void load(ResourceManager manager) {
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?

var localPath = FileSystem.toLocal(file.getPath(), subPath);
create(newRoot, localPath);
try {
getOrCreateChild(newRoot, localPath, this::createEntry);
} catch (ResourceLocationException e) {
LOG.warn("Cannot create resource location for {} ({})", localPath, e.getMessage());
}
hasAny = true;
}

Expand All @@ -83,52 +85,12 @@ private void load(ResourceManager manager) {
}
}

private void create(FileEntry lastEntry, String path) {
var lastIndex = 0;
while (lastIndex < path.length()) {
var nextIndex = path.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = path.length();

var part = path.substring(lastIndex, nextIndex);
if (lastEntry.children == null) lastEntry.children = new HashMap<>();

var nextEntry = lastEntry.children.get(part);
if (nextEntry == null) {
ResourceLocation childPath;
try {
childPath = new ResourceLocation(namespace, subPath + "/" + path);
} catch (ResourceLocationException e) {
LOG.warn("Cannot create resource location for {} ({})", part, e.getMessage());
return;
}
lastEntry.children.put(part, nextEntry = new FileEntry(path, childPath));
}

lastEntry = nextEntry;
lastIndex = nextIndex + 1;
}
}

@Override
public long getSize(FileEntry file) {
var resource = manager.getResource(file.identifier).orElse(null);
if (resource == null) return 0;

try (var stream = resource.open()) {
int total = 0, read = 0;
do {
total += read;
read = stream.read(TEMP_BUFFER);
} while (read > 0);

return total;
} catch (IOException e) {
return 0;
}
private FileEntry createEntry(String path) {
return new FileEntry(path, new ResourceLocation(namespace, subPath + "/" + path));
}

@Override
public byte[] getContents(FileEntry file) throws IOException {
public byte[] getFileContents(FileEntry file) throws IOException {
var resource = manager.getResource(file.identifier).orElse(null);
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@
/**
* A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
*
* @param isDirectory Whether this filesystem entry is a directory.
* @param size The size of the file.
* @param isDirectory Whether this filesystem entry is a directory.
* @param size The size of the file.
* @param creationTime The time the file was created.
* @param lastModifiedTime The time the file was last modified.
*/
public record FileAttributes(boolean isDirectory, long size) implements BasicFileAttributes {
public record FileAttributes(
boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
) implements BasicFileAttributes {
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);

@Override
public FileTime lastModifiedTime() {
return EPOCH;
/**
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
* {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch.
*
* @param isDirectory Whether the filesystem entry is a directory.
* @param size The size of the file.
*/
public FileAttributes(boolean isDirectory, long size) {
this(isDirectory, size, EPOCH, EPOCH);
}

@Override
public FileTime lastAccessTime() {
return EPOCH;
}

@Override
public FileTime creationTime() {
return EPOCH;
return lastModifiedTime();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import dan200.computercraft.core.metrics.Metrics;

import javax.annotation.Nullable;
import java.nio.file.attribute.FileTime;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
Expand Down Expand Up @@ -488,9 +487,9 @@ public final Map<String, Object> attributes(String path) throws LuaException {
try (var ignored = environment.time(Metrics.FS_OPS)) {
var attributes = getFileSystem().getAttributes(path);
Map<String, Object> result = new HashMap<>();
result.put("modification", getFileTime(attributes.lastModifiedTime()));
result.put("modified", getFileTime(attributes.lastModifiedTime()));
result.put("created", getFileTime(attributes.creationTime()));
result.put("modification", attributes.lastModifiedTime().toMillis());
result.put("modified", attributes.lastModifiedTime().toMillis());
result.put("created", attributes.creationTime().toMillis());
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
result.put("isDir", attributes.isDirectory());
result.put("isReadOnly", getFileSystem().isReadOnly(path));
Expand All @@ -499,8 +498,4 @@ public final Map<String, Object> attributes(String path) throws LuaException {
throw new LuaException(e.getMessage());
}
}

private static long getFileTime(@Nullable FileTime time) {
return time == null ? 0 : time.toMillis();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0

package dan200.computercraft.core.filesystem;

import dan200.computercraft.api.filesystem.FileAttributes;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.Mount;

import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
* An abstract mount which stores its file tree in memory.
*
* @param <T> The type of file.
*/
public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.FileEntry<T>> implements Mount {
protected static final String NO_SUCH_FILE = "No such file";

@Nullable
protected T root;

private @Nullable T get(String path) {
var lastEntry = root;
var lastIndex = 0;

while (lastEntry != null && lastIndex < path.length()) {
var nextIndex = path.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = path.length();

lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
lastIndex = nextIndex + 1;
}

return lastEntry;
}

@Override
public final boolean exists(String path) {
return get(path) != null;
}

@Override
public final boolean isDirectory(String path) {
var file = get(path);
return file != null && file.isDirectory();
}

@Override
public final void list(String path, List<String> contents) throws IOException {
var file = get(path);
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");

file.list(contents);
}

@Override
public final long getSize(String path) throws IOException {
var file = get(path);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
return getSize(file);
}

/**
* Get the size of a file.
*
* @param file The file to get the size of.
* @return The size of the file. This should be 0 for directories, and equal to {@code openForRead(_).size()} for files.
* @throws IOException If the size could not be read.
*/
protected abstract long getSize(T file) throws IOException;

@Override
public final SeekableByteChannel openForRead(String path) throws IOException {
var file = get(path);
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
return openForRead(file);
}

/**
* Open a file for reading.
*
* @param file The file to read. This will not be a directory.
* @return The channel for this file.
*/
protected abstract SeekableByteChannel openForRead(T file) throws IOException;

@Override
public final BasicFileAttributes getAttributes(String path) throws IOException {
var file = get(path);
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
return getAttributes(file);
}

/**
* Get all attributes of the file.
*
* @param file The file to compute attributes for.
* @return The file's attributes.
* @throws IOException If the attributes could not be read.
*/
protected BasicFileAttributes getAttributes(T file) throws IOException {
return new FileAttributes(file.isDirectory(), getSize(file));
}

protected T getOrCreateChild(T lastEntry, String localPath, Function<String, T> factory) {
var lastIndex = 0;
while (lastIndex < localPath.length()) {
var nextIndex = localPath.indexOf('/', lastIndex);
if (nextIndex < 0) nextIndex = localPath.length();

var part = localPath.substring(lastIndex, nextIndex);
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);

var nextEntry = lastEntry.children.get(part);
if (nextEntry == null || !nextEntry.isDirectory()) {
lastEntry.children.put(part, nextEntry = factory.apply(localPath.substring(0, nextIndex)));
}

lastEntry = nextEntry;
lastIndex = nextIndex + 1;
}

return lastEntry;
}

protected static class FileEntry<T extends FileEntry<T>> {
public final String path;
@Nullable
public Map<String, T> children;

protected FileEntry(String path) {
this.path = path;
}

public boolean isDirectory() {
return children != null;
}

protected void list(List<String> contents) {
if (children != null) contents.addAll(children.keySet());
}
}
}
Loading

0 comments on commit 0d6c6e7

Please sign in to comment.