CC-Tweaked/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java

375 lines
12 KiB
Java

/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import com.google.common.collect.Sets;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.WritableMount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class FileMount implements WritableMount {
private static final Logger LOG = LoggerFactory.getLogger(FileMount.class);
private static final long MINIMUM_FILE_SIZE = 500;
private static final Set<OpenOption> READ_OPTIONS = Collections.singleton(StandardOpenOption.READ);
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
private class WritableCountingChannel implements WritableByteChannel {
private final WritableByteChannel inner;
long ignoredBytesLeft;
WritableCountingChannel(WritableByteChannel inner, long bytesToIgnore) {
this.inner = inner;
ignoredBytesLeft = bytesToIgnore;
}
@Override
public int write(ByteBuffer b) throws IOException {
count(b.remaining());
return inner.write(b);
}
void count(long n) throws IOException {
ignoredBytesLeft -= n;
if (ignoredBytesLeft < 0) {
var newBytes = -ignoredBytesLeft;
ignoredBytesLeft = 0;
var bytesLeft = capacity - usedSpace;
if (newBytes > bytesLeft) throw new IOException("Out of space");
usedSpace += newBytes;
}
}
@Override
public boolean isOpen() {
return inner.isOpen();
}
@Override
public void close() throws IOException {
inner.close();
}
}
private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel {
private final SeekableByteChannel inner;
SeekableCountingChannel(SeekableByteChannel inner, long bytesToIgnore) {
super(inner, bytesToIgnore);
this.inner = inner;
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (!isOpen()) throw new ClosedChannelException();
if (newPosition < 0) {
throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
}
var delta = newPosition - inner.position();
if (delta < 0) {
ignoredBytesLeft -= delta;
} else {
count(delta);
}
return inner.position(newPosition);
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw new IOException("Not yet implemented");
}
@Override
public int read(ByteBuffer dst) throws ClosedChannelException {
if (!inner.isOpen()) throw new ClosedChannelException();
throw new NonReadableChannelException();
}
@Override
public long position() throws IOException {
return inner.position();
}
@Override
public long size() throws IOException {
return inner.size();
}
}
private final File rootPath;
private final long capacity;
private long usedSpace;
public FileMount(File rootPath, long capacity) {
this.rootPath = rootPath;
this.capacity = capacity + MINIMUM_FILE_SIZE;
usedSpace = created() ? measureUsedSpace(this.rootPath) : MINIMUM_FILE_SIZE;
}
// IMount implementation
@Override
public boolean exists(String path) {
if (!created()) return path.isEmpty();
var file = getRealPath(path);
return file.exists();
}
@Override
public boolean isDirectory(String path) {
if (!created()) return path.isEmpty();
var file = getRealPath(path);
return file.exists() && file.isDirectory();
}
@Override
public boolean isReadOnly(String path) throws IOException {
var file = getRealPath(path);
while (true) {
if (file.exists()) return !file.canWrite();
if (file.equals(rootPath)) return false;
file = file.getParentFile();
}
}
@Override
public void list(String path, List<String> contents) throws IOException {
if (!created()) {
if (!path.isEmpty()) throw new FileOperationException(path, "Not a directory");
return;
}
var file = getRealPath(path);
if (!file.exists() || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
var paths = file.list();
for (var subPath : paths) {
if (new File(file, subPath).exists()) contents.add(subPath);
}
}
@Override
public long getSize(String path) throws IOException {
if (!created()) {
if (path.isEmpty()) return 0;
} else {
var file = getRealPath(path);
if (file.exists()) return file.isDirectory() ? 0 : file.length();
}
throw new FileOperationException(path, "No such file");
}
@Override
public SeekableByteChannel openForRead(String path) throws IOException {
if (created()) {
var file = getRealPath(path);
if (file.exists() && !file.isDirectory()) return Files.newByteChannel(file.toPath(), READ_OPTIONS);
}
throw new FileOperationException(path, "No such file");
}
@Override
public BasicFileAttributes getAttributes(String path) throws IOException {
if (created()) {
var file = getRealPath(path);
if (file.exists()) return Files.readAttributes(file.toPath(), BasicFileAttributes.class);
}
throw new FileOperationException(path, "No such file");
}
// IWritableMount implementation
@Override
public void makeDirectory(String path) throws IOException {
create();
var file = getRealPath(path);
if (file.exists()) {
if (!file.isDirectory()) throw new FileOperationException(path, "File exists");
return;
}
var dirsToCreate = 1;
var parent = file.getParentFile();
while (!parent.exists()) {
++dirsToCreate;
parent = parent.getParentFile();
}
if (getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE) {
throw new FileOperationException(path, "Out of space");
}
if (file.mkdirs()) {
usedSpace += dirsToCreate * MINIMUM_FILE_SIZE;
} else {
throw new FileOperationException(path, "Access denied");
}
}
@Override
public void delete(String path) throws IOException {
if (path.isEmpty()) throw new FileOperationException(path, "Access denied");
if (created()) {
var file = getRealPath(path);
if (file.exists()) deleteRecursively(file);
}
}
private void deleteRecursively(File file) throws IOException {
// Empty directories first
if (file.isDirectory()) {
var children = file.list();
for (var aChildren : children) {
deleteRecursively(new File(file, aChildren));
}
}
// Then delete
var fileSize = file.isDirectory() ? 0 : file.length();
var success = file.delete();
if (success) {
usedSpace -= Math.max(MINIMUM_FILE_SIZE, fileSize);
} else {
throw new IOException("Access denied");
}
}
@Override
public void rename(String source, String dest) throws IOException {
var sourceFile = getRealPath(source);
var destFile = getRealPath(dest);
if (!sourceFile.exists()) throw new FileOperationException(source, "No such file");
if (destFile.exists()) throw new FileOperationException(dest, "File exists");
var sourcePath = sourceFile.toPath();
var destPath = destFile.toPath();
if (destPath.startsWith(sourcePath)) {
throw new FileOperationException(source, "Cannot move a directory inside itself");
}
Files.move(sourcePath, destPath);
}
@Override
public WritableByteChannel openForWrite(String path) throws IOException {
create();
var file = getRealPath(path);
if (file.exists() && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
if (file.exists()) {
usedSpace -= Math.max(file.length(), MINIMUM_FILE_SIZE);
} else if (getRemainingSpace() < MINIMUM_FILE_SIZE) {
throw new FileOperationException(path, "Out of space");
}
usedSpace += MINIMUM_FILE_SIZE;
return new SeekableCountingChannel(Files.newByteChannel(file.toPath(), WRITE_OPTIONS), MINIMUM_FILE_SIZE);
}
@Override
public WritableByteChannel openForAppend(String path) throws IOException {
if (!created()) {
throw new FileOperationException(path, "No such file");
}
var file = getRealPath(path);
if (!file.exists()) throw new FileOperationException(path, "No such file");
if (file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
// Allowing seeking when appending is not recommended, so we use a separate channel.
return new WritableCountingChannel(
Files.newByteChannel(file.toPath(), APPEND_OPTIONS),
Math.max(MINIMUM_FILE_SIZE - file.length(), 0)
);
}
@Override
public long getRemainingSpace() {
return Math.max(capacity - usedSpace, 0);
}
@Override
public long getCapacity() {
return capacity - MINIMUM_FILE_SIZE;
}
private File getRealPath(String path) {
return new File(rootPath, path);
}
private boolean created() {
return rootPath.exists();
}
private void create() throws IOException {
if (!rootPath.exists()) {
var success = rootPath.mkdirs();
if (!success) {
throw new IOException("Access denied");
}
}
}
private static class Visitor extends SimpleFileVisitor<Path> {
long size;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
size += MINIMUM_FILE_SIZE;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
size += Math.max(attrs.size(), MINIMUM_FILE_SIZE);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
LOG.error("Error computing file size for {}", file, exc);
return FileVisitResult.CONTINUE;
}
}
private static long measureUsedSpace(File file) {
if (!file.exists()) return 0;
try {
var visitor = new Visitor();
Files.walkFileTree(file.toPath(), visitor);
return visitor.size;
} catch (IOException e) {
LOG.error("Error computing file size for {}", file, e);
return 0;
}
}
}