mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-24 17:10:31 +00:00
Hoist some ArchiveMount logic into a new superclass
- 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.
This commit is contained in:
parent
ae71eb3cae
commit
0d6c6e7ae7
@ -29,8 +29,6 @@ import java.util.Map;
|
|||||||
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);
|
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.
|
* Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes.
|
||||||
*/
|
*/
|
||||||
@ -68,7 +66,11 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
|||||||
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
|
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
|
||||||
|
|
||||||
var localPath = FileSystem.toLocal(file.getPath(), subPath);
|
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;
|
hasAny = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,52 +85,12 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void create(FileEntry lastEntry, String path) {
|
private FileEntry createEntry(String path) {
|
||||||
var lastIndex = 0;
|
return new FileEntry(path, new ResourceLocation(namespace, subPath + "/" + path));
|
||||||
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
|
@Override
|
||||||
public long getSize(FileEntry file) {
|
public byte[] getFileContents(FileEntry file) throws IOException {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getContents(FileEntry file) throws IOException {
|
|
||||||
var resource = manager.getResource(file.identifier).orElse(null);
|
var resource = manager.getResource(file.identifier).orElse(null);
|
||||||
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||||
|
|
||||||
|
@ -12,25 +12,30 @@ import java.time.Instant;
|
|||||||
/**
|
/**
|
||||||
* A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
|
* 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 isDirectory Whether this filesystem entry is a directory.
|
||||||
* @param size The size of the file.
|
* @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);
|
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public FileTime lastModifiedTime() {
|
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
|
||||||
return EPOCH;
|
* {@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
|
@Override
|
||||||
public FileTime lastAccessTime() {
|
public FileTime lastAccessTime() {
|
||||||
return EPOCH;
|
return lastModifiedTime();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime creationTime() {
|
|
||||||
return EPOCH;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemException;
|
|||||||
import dan200.computercraft.core.metrics.Metrics;
|
import dan200.computercraft.core.metrics.Metrics;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.nio.file.attribute.FileTime;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@ -488,9 +487,9 @@ public class FSAPI implements ILuaAPI {
|
|||||||
try (var ignored = environment.time(Metrics.FS_OPS)) {
|
try (var ignored = environment.time(Metrics.FS_OPS)) {
|
||||||
var attributes = getFileSystem().getAttributes(path);
|
var attributes = getFileSystem().getAttributes(path);
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("modification", getFileTime(attributes.lastModifiedTime()));
|
result.put("modification", attributes.lastModifiedTime().toMillis());
|
||||||
result.put("modified", getFileTime(attributes.lastModifiedTime()));
|
result.put("modified", attributes.lastModifiedTime().toMillis());
|
||||||
result.put("created", getFileTime(attributes.creationTime()));
|
result.put("created", attributes.creationTime().toMillis());
|
||||||
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
||||||
result.put("isDir", attributes.isDirectory());
|
result.put("isDir", attributes.isDirectory());
|
||||||
result.put("isReadOnly", getFileSystem().isReadOnly(path));
|
result.put("isReadOnly", getFileSystem().isReadOnly(path));
|
||||||
@ -499,8 +498,4 @@ public class FSAPI implements ILuaAPI {
|
|||||||
throw new LuaException(e.getMessage());
|
throw new LuaException(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long getFileTime(@Nullable FileTime time) {
|
|
||||||
return time == null ? 0 : time.toMillis();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,25 +6,21 @@ package dan200.computercraft.core.filesystem;
|
|||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
|
||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
|
||||||
import dan200.computercraft.api.filesystem.Mount;
|
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract mount based on some archive of files, such as a Zip or Minecraft's resources.
|
* An abstract mount based on some archive of files, such as a Zip or Minecraft's resources.
|
||||||
|
* <p>
|
||||||
|
* We assume that we cannot create {@link SeekableByteChannel}s directly from the archive, and so maintain a (shared)
|
||||||
|
* cache of recently read files and their contents.
|
||||||
*
|
*
|
||||||
* @param <T> The type of file.
|
* @param <T> The type of file.
|
||||||
*/
|
*/
|
||||||
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implements Mount {
|
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends AbstractInMemoryMount<T> {
|
||||||
protected static final String NO_SUCH_FILE = "No such file";
|
protected static final String NO_SUCH_FILE = "No such file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,82 +40,38 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
|
|||||||
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
|
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@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
|
@Override
|
||||||
public final boolean exists(String path) {
|
protected final long getSize(T file) throws IOException {
|
||||||
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 getCachedSize(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long getCachedSize(T file) throws IOException {
|
|
||||||
if (file.size != -1) return file.size;
|
if (file.size != -1) return file.size;
|
||||||
if (file.isDirectory()) return file.size = 0;
|
if (file.isDirectory()) return file.size = 0;
|
||||||
|
|
||||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
var contents = CONTENTS_CACHE.getIfPresent(file);
|
||||||
if (contents != null) return file.size = contents.length;
|
return file.size = contents != null ? contents.length : getFileSize(file);
|
||||||
|
|
||||||
return file.size = getSize(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of a file.
|
* Get the size of the file by reading it (or its metadata) from disk.
|
||||||
* <p>
|
|
||||||
* This should only be called once per file, as the result is cached in {@link #getSize(String)}.
|
|
||||||
*
|
*
|
||||||
* @param file The file to compute the size of. This will not be a directory.
|
* @param file The file to get the size of.
|
||||||
* @return The size of the file.
|
* @return The file's size.
|
||||||
* @throws IOException If the size could not be read.
|
* @throws IOException If the size could not be computed.
|
||||||
*/
|
*/
|
||||||
protected abstract long getSize(T file) throws IOException;
|
protected long getFileSize(T file) throws IOException {
|
||||||
|
return getContents(file).length;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SeekableByteChannel openForRead(String path) throws IOException {
|
protected final SeekableByteChannel openForRead(T file) throws IOException {
|
||||||
var file = get(path);
|
return new ArrayByteChannel(getContents(file));
|
||||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
}
|
||||||
|
|
||||||
|
private byte[] getContents(T file) throws IOException {
|
||||||
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
||||||
if (cachedContents != null) return new ArrayByteChannel(cachedContents);
|
if (cachedContents != null) return cachedContents;
|
||||||
|
|
||||||
var contents = getContents(file);
|
var contents = getFileContents(file);
|
||||||
CONTENTS_CACHE.put(file, contents);
|
CONTENTS_CACHE.put(file, contents);
|
||||||
return new ArrayByteChannel(contents);
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,44 +80,13 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
|
|||||||
* @param file The file to read into memory. This will not be a directory.
|
* @param file The file to read into memory. This will not be a directory.
|
||||||
* @return The contents of the file.
|
* @return The contents of the file.
|
||||||
*/
|
*/
|
||||||
protected abstract byte[] getContents(T file) throws IOException;
|
protected abstract byte[] getFileContents(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. This will not be a directory.
|
|
||||||
* @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(), getCachedSize(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class FileEntry<T extends ArchiveMount.FileEntry<T>> {
|
|
||||||
public final String path;
|
|
||||||
@Nullable
|
|
||||||
public Map<String, T> children;
|
|
||||||
|
|
||||||
|
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
|
||||||
long size = -1;
|
long size = -1;
|
||||||
|
|
||||||
protected FileEntry(String path) {
|
protected FileEntry(String path) {
|
||||||
this.path = path;
|
super(path);
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDirectory() {
|
|
||||||
return children != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void list(List<String> contents) {
|
|
||||||
if (children != null) contents.addAll(children.keySet());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
package dan200.computercraft.core.filesystem;
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||||
import dan200.computercraft.core.util.Nullability;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
@ -42,7 +42,7 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read in all the entries
|
// Read in all the entries
|
||||||
root = new FileEntry("");
|
var root = this.root = new FileEntry("");
|
||||||
var zipEntries = zip.entries();
|
var zipEntries = zip.entries();
|
||||||
while (zipEntries.hasMoreElements()) {
|
while (zipEntries.hasMoreElements()) {
|
||||||
var entry = zipEntries.nextElement();
|
var entry = zipEntries.nextElement();
|
||||||
@ -51,41 +51,18 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
if (!entryPath.startsWith(subPath)) continue;
|
if (!entryPath.startsWith(subPath)) continue;
|
||||||
|
|
||||||
var localPath = FileSystem.toLocal(entryPath, subPath);
|
var localPath = FileSystem.toLocal(entryPath, subPath);
|
||||||
create(entry, localPath);
|
getOrCreateChild(root, localPath, FileEntry::new).setup(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void create(ZipEntry entry, String localPath) {
|
|
||||||
var lastEntry = Nullability.assertNonNull(root);
|
|
||||||
|
|
||||||
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 = new FileEntry(localPath.substring(0, nextIndex)));
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEntry = nextEntry;
|
|
||||||
lastIndex = nextIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEntry.setup(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getSize(FileEntry file) throws FileOperationException {
|
protected long getFileSize(FileEntry file) throws FileOperationException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||||
return file.zipEntry.getSize();
|
return file.zipEntry.getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] getContents(FileEntry file) throws FileOperationException {
|
protected byte[] getFileContents(FileEntry file) throws FileOperationException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||||
|
|
||||||
try (var stream = zip.getInputStream(file.zipEntry)) {
|
try (var stream = zip.getInputStream(file.zipEntry)) {
|
||||||
@ -97,9 +74,10 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BasicFileAttributes getAttributes(FileEntry file) throws FileOperationException {
|
protected BasicFileAttributes getAttributes(FileEntry file) throws IOException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
return file.zipEntry == null ? super.getAttributes(file) : new FileAttributes(
|
||||||
return new ZipEntryAttributes(file.zipEntry);
|
file.isDirectory(), getSize(file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -117,69 +95,13 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
|
|
||||||
void setup(ZipEntry entry) {
|
void setup(ZipEntry entry) {
|
||||||
zipEntry = entry;
|
zipEntry = entry;
|
||||||
size = entry.getSize();
|
|
||||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ZipEntryAttributes implements BasicFileAttributes {
|
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||||
private final ZipEntry entry;
|
|
||||||
|
|
||||||
ZipEntryAttributes(ZipEntry entry) {
|
private static FileTime orEpoch(@Nullable FileTime time) {
|
||||||
this.entry = entry;
|
return time == null ? EPOCH : time;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime lastModifiedTime() {
|
|
||||||
return orEpoch(entry.getLastModifiedTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime lastAccessTime() {
|
|
||||||
return orEpoch(entry.getLastAccessTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileTime creationTime() {
|
|
||||||
var time = entry.getCreationTime();
|
|
||||||
return time == null ? lastModifiedTime() : time;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRegularFile() {
|
|
||||||
return !entry.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory() {
|
|
||||||
return entry.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSymbolicLink() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOther() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() {
|
|
||||||
return entry.getSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Object fileKey() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
|
||||||
|
|
||||||
private static FileTime orEpoch(@Nullable FileTime time) {
|
|
||||||
return time == null ? EPOCH : time;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,53 +7,44 @@ package dan200.computercraft.test.core.filesystem;
|
|||||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||||
import dan200.computercraft.api.filesystem.Mount;
|
import dan200.computercraft.api.filesystem.Mount;
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||||
|
import dan200.computercraft.core.filesystem.AbstractInMemoryMount;
|
||||||
|
import dan200.computercraft.core.util.Nullability;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
|
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
|
||||||
*/
|
*/
|
||||||
public class MemoryMount implements Mount {
|
public class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> {
|
||||||
private final Map<String, byte[]> files = new HashMap<>();
|
|
||||||
private final Set<String> directories = new HashSet<>();
|
|
||||||
|
|
||||||
public MemoryMount() {
|
public MemoryMount() {
|
||||||
directories.add("");
|
root = new FileEntry("");
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists(String path) {
|
|
||||||
return files.containsKey(path) || directories.contains(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory(String path) {
|
|
||||||
return directories.contains(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void list(String path, List<String> files) {
|
|
||||||
for (var file : this.files.keySet()) {
|
|
||||||
if (file.startsWith(path)) files.add(file.substring(path.length() + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getSize(String path) {
|
|
||||||
throw new RuntimeException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekableByteChannel openForRead(String path) throws FileOperationException {
|
|
||||||
var file = files.get(path);
|
|
||||||
if (file == null) throw new FileOperationException(path, "File not found");
|
|
||||||
return new ArrayByteChannel(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemoryMount addFile(String file, String contents) {
|
public MemoryMount addFile(String file, String contents) {
|
||||||
files.put(file, contents.getBytes(StandardCharsets.UTF_8));
|
getOrCreateChild(Nullability.assertNonNull(root), file, FileEntry::new).contents = contents.getBytes(StandardCharsets.UTF_8);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getSize(FileEntry file) {
|
||||||
|
return file.contents == null ? 0 : file.contents.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SeekableByteChannel openForRead(FileEntry file) throws IOException {
|
||||||
|
if (file.contents == null) throw new FileOperationException(file.path, "File is a directory");
|
||||||
|
return new ArrayByteChannel(file.contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
|
||||||
|
@Nullable
|
||||||
|
byte[] contents;
|
||||||
|
|
||||||
|
protected FileEntry(String path) {
|
||||||
|
super(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user