1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-16 18:19:55 +00:00
CC-Tweaked/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java
Jonathan Coates 367773e173
Some refactoring of mounts
- Separate FileMount into separate FileMount and WritableFileMount
   classes. This separates the (relatively simple) read-only code from
   the (soon to be even more complex) read/write code.

   It also allows you to create read-only mounts which don't bother with
   filesystem accounting, which is nice.

 - Make openForWrite/openForAppend always return a SeekableFileHandle.
   Appendable files still cannot be seeked within, but that check is now
   done on the FS side.

 - Refactor the various mount tests to live in test contract interfaces,
   allowing us to reuse them between mounts.

 - Clean up our error handling a little better. (Most) file-specific code
   has been moved to FileMount, and ArchiveMount-derived classes now
   throw correct path-localised exceptions.
2022-12-09 22:02:31 +00:00

323 lines
11 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 javax.annotation.Nullable;
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.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set;
/**
* A {@link WritableFileMount} implementation which provides read-write access to a directory.
*/
public class WritableFileMount extends FileMount implements WritableMount {
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
private static final long MINIMUM_FILE_SIZE = 500;
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);
protected final File rootFile;
private final long capacity;
private long usedSpace;
public WritableFileMount(File rootFile, long capacity) {
super(rootFile.toPath());
this.rootFile = rootFile;
this.capacity = capacity + MINIMUM_FILE_SIZE;
usedSpace = created() ? measureUsedSpace(root) : MINIMUM_FILE_SIZE;
}
protected File resolveFile(String path) {
return new File(rootFile, path);
}
private void create() throws FileOperationException {
try {
Files.createDirectories(root);
} catch (IOException e) {
throw new FileOperationException("Access denied");
}
}
@Override
public long getRemainingSpace() {
return Math.max(capacity - usedSpace, 0);
}
@Override
public long getCapacity() {
return capacity - MINIMUM_FILE_SIZE;
}
@Override
public boolean isReadOnly(String path) {
var file = resolveFile(path);
while (true) {
if (file.exists()) return !file.canWrite();
if (file.equals(rootFile)) return false;
file = file.getParentFile();
}
}
@Override
public void makeDirectory(String path) throws IOException {
create();
var file = resolveFile(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 = resolveFile(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 FileOperationException {
var sourceFile = resolvePath(source);
var destFile = resolvePath(dest);
if (!Files.exists(sourceFile)) throw new FileOperationException(source, "No such file");
if (Files.exists(destFile)) throw new FileOperationException(dest, "File exists");
if (destFile.startsWith(sourceFile)) {
throw new FileOperationException(source, "Cannot move a directory inside itself");
}
try {
Files.move(sourceFile, destFile);
} catch (IOException e) {
throw remapException(source, e);
}
}
private @Nullable BasicFileAttributes tryGetAttributes(String path, Path resolved) throws FileOperationException {
try {
return Files.readAttributes(resolved, BasicFileAttributes.class);
} catch (NoSuchFileException ignored) {
return null;
} catch (IOException e) {
throw remapException(path, e);
}
}
@Override
public SeekableByteChannel openForWrite(String path) throws FileOperationException {
create();
var file = resolvePath(path);
var attributes = tryGetAttributes(path, file);
if (attributes == null) {
if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, "Out of space");
} else if (attributes.isDirectory()) {
throw new FileOperationException(path, "Cannot write to directory");
} else {
usedSpace -= Math.max(attributes.size(), MINIMUM_FILE_SIZE);
}
usedSpace += MINIMUM_FILE_SIZE;
try {
return new CountingChannel(Files.newByteChannel(file, WRITE_OPTIONS), MINIMUM_FILE_SIZE, true);
} catch (IOException e) {
throw remapException(path, e);
}
}
@Override
public SeekableByteChannel openForAppend(String path) throws IOException {
create();
var file = resolvePath(path);
var attributes = tryGetAttributes(path, file);
if (attributes == null) {
if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, "Out of space");
} else if (attributes.isDirectory()) {
throw new FileOperationException(path, "Cannot write to directory");
}
// Allowing seeking when appending is not recommended, so we use a separate channel.
try {
return new CountingChannel(
Files.newByteChannel(file, APPEND_OPTIONS),
Math.max(MINIMUM_FILE_SIZE - (attributes == null ? 0 : attributes.size()), 0),
false
);
} catch (IOException e) {
throw remapException(path, e);
}
}
private class CountingChannel implements SeekableByteChannel {
private final SeekableByteChannel channel;
private long ignoredBytesLeft;
private final boolean canSeek;
CountingChannel(SeekableByteChannel channel, long bytesToIgnore, boolean canSeek) {
this.channel = channel;
ignoredBytesLeft = bytesToIgnore;
this.canSeek = canSeek;
}
@Override
public int write(ByteBuffer b) throws IOException {
count(b.remaining());
return channel.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 channel.isOpen();
}
@Override
public void close() throws IOException {
channel.close();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (!isOpen()) throw new ClosedChannelException();
if (!canSeek) throw new UnsupportedOperationException("File does not support seeking");
if (newPosition < 0) {
throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
}
var delta = newPosition - channel.position();
if (delta < 0) {
ignoredBytesLeft -= delta;
} else {
count(delta);
}
return channel.position(newPosition);
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw new UnsupportedOperationException("File cannot be truncated");
}
@Override
public int read(ByteBuffer dst) throws ClosedChannelException {
if (!channel.isOpen()) throw new ClosedChannelException();
throw new NonReadableChannelException();
}
@Override
public long position() throws IOException {
return channel.position();
}
@Override
public long size() throws IOException {
return channel.size();
}
}
private static long measureUsedSpace(Path path) {
if (!Files.exists(path)) return 0;
class CountingVisitor 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;
}
}
try {
var visitor = new CountingVisitor();
Files.walkFileTree(path, visitor);
return visitor.size;
} catch (IOException e) {
LOG.error("Error computing file size for {}", path, e);
return 0;
}
}
}