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> {
|
||||
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.
|
||||
*/
|
||||
@ -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?
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -83,52 +85,12 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
private FileEntry createEntry(String path) {
|
||||
return new FileEntry(path, new ResourceLocation(namespace, subPath + "/" + path));
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
|
@ -12,25 +12,30 @@ import java.time.Instant;
|
||||
/**
|
||||
* 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
|
||||
|
@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
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;
|
||||
@ -488,9 +487,9 @@ public class FSAPI implements ILuaAPI {
|
||||
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));
|
||||
@ -499,8 +498,4 @@ public class FSAPI implements ILuaAPI {
|
||||
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.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 javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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";
|
||||
|
||||
/**
|
||||
@ -44,82 +40,38 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
|
||||
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
|
||||
.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
|
||||
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 getCachedSize(file);
|
||||
}
|
||||
|
||||
private long getCachedSize(T file) throws IOException {
|
||||
protected final long getSize(T file) throws IOException {
|
||||
if (file.size != -1) return file.size;
|
||||
if (file.isDirectory()) return file.size = 0;
|
||||
|
||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
||||
if (contents != null) return file.size = contents.length;
|
||||
|
||||
return file.size = getSize(file);
|
||||
return file.size = contents != null ? contents.length : getFileSize(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file.
|
||||
* <p>
|
||||
* This should only be called once per file, as the result is cached in {@link #getSize(String)}.
|
||||
* Get the size of the file by reading it (or its metadata) from disk.
|
||||
*
|
||||
* @param file The file to compute the size of. This will not be a directory.
|
||||
* @return The size of the file.
|
||||
* @throws IOException If the size could not be read.
|
||||
* @param file The file to get the size of.
|
||||
* @return The file's size.
|
||||
* @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
|
||||
public SeekableByteChannel openForRead(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
protected final SeekableByteChannel openForRead(T file) throws IOException {
|
||||
return new ArrayByteChannel(getContents(file));
|
||||
}
|
||||
|
||||
private byte[] getContents(T file) throws IOException {
|
||||
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);
|
||||
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.
|
||||
* @return The contents of the file.
|
||||
*/
|
||||
protected abstract byte[] getContents(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 abstract byte[] getFileContents(T file) throws IOException;
|
||||
|
||||
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
|
||||
long size = -1;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
protected boolean isDirectory() {
|
||||
return children != null;
|
||||
}
|
||||
|
||||
protected void list(List<String> contents) {
|
||||
if (children != null) contents.addAll(children.keySet());
|
||||
super(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
@ -42,7 +42,7 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
}
|
||||
|
||||
// Read in all the entries
|
||||
root = new FileEntry("");
|
||||
var root = this.root = new FileEntry("");
|
||||
var zipEntries = zip.entries();
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
var entry = zipEntries.nextElement();
|
||||
@ -51,41 +51,18 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
if (!entryPath.startsWith(subPath)) continue;
|
||||
|
||||
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
|
||||
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);
|
||||
return file.zipEntry.getSize();
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
try (var stream = zip.getInputStream(file.zipEntry)) {
|
||||
@ -97,9 +74,10 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
return new ZipEntryAttributes(file.zipEntry);
|
||||
protected BasicFileAttributes getAttributes(FileEntry file) throws IOException {
|
||||
return file.zipEntry == null ? super.getAttributes(file) : new FileAttributes(
|
||||
file.isDirectory(), getSize(file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -117,69 +95,13 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
|
||||
void setup(ZipEntry entry) {
|
||||
zipEntry = entry;
|
||||
size = entry.getSize();
|
||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ZipEntryAttributes implements BasicFileAttributes {
|
||||
private final ZipEntry entry;
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
ZipEntryAttributes(ZipEntry entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
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.Mount;
|
||||
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.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
|
||||
*/
|
||||
public class MemoryMount implements Mount {
|
||||
private final Map<String, byte[]> files = new HashMap<>();
|
||||
private final Set<String> directories = new HashSet<>();
|
||||
|
||||
public class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> {
|
||||
public MemoryMount() {
|
||||
directories.add("");
|
||||
}
|
||||
|
||||
@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);
|
||||
root = new FileEntry("");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@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