package peergos.server.fuse; import jnr.ffi.Pointer; import jnr.ffi.types.*; import peergos.shared.user.UserContext; import peergos.shared.user.fs.*; import peergos.shared.util.Serialize; import ru.serce.jnrfuse.ErrorCodes; import ru.serce.jnrfuse.FuseFillDir; import ru.serce.jnrfuse.FuseStubFS; import ru.serce.jnrfuse.struct.*; import java.nio.file.Path; import java.nio.file.Paths; import java.time.*; import java.util.*; import java.util.function.*; /** * Nice FUSE API doc @ * https://www.cs.hmc.edu/~geoff/classes/hmc.cs135.201001/homework/fuse/fuse_doc.html * also * https://github.com/libfuse/libfuse/blob/master/include/fuse.h */ public class PeergosFS extends FuseStubFS implements AutoCloseable { protected static class PeergosStat { public final FileTreeNode treeNode; public final FileProperties properties; public PeergosStat(FileTreeNode treeNode, FileProperties properties) { this.treeNode = treeNode; this.properties = properties; } } private final UserContext context; protected volatile boolean isClosed; public PeergosFS(UserContext context) { this.context = context; } @Override public void close() throws Exception { ensureNotClosed(); this.isClosed = true; } private void ensureNotClosed() { if (isClosed) throw new IllegalStateException(this +" is closed"); } protected int annotateAttributes(String fullPath, PeergosStat peergosStat, FileStat fileStat) { try { FileTreeNode fileTreeNode = peergosStat.treeNode; FileProperties fileProperties = peergosStat.properties; int mode = fileTreeNode.isDirectory() ? FileStat.S_IFDIR | 0755 : FileStat.S_IFREG | 0644; fileStat.st_mode.set(mode); fileStat.st_size.set(fileProperties.size); Instant instant = fileProperties.modified.toInstant(ZonedDateTime.now().getOffset()); long epochSecond = instant.getEpochSecond(); long nanoSeconds = instant.getNano(); fileStat.st_mtim.tv_sec.set(epochSecond); fileStat.st_mtim.tv_nsec.set(nanoSeconds); fileStat.st_atim.tv_nsec.set(epochSecond); fileStat.st_atim.tv_nsec.set(nanoSeconds); return 0; } catch (Throwable t) { t.printStackTrace(); return 1; } } @Override public int getattr(String s, FileStat fileStat) { ensureNotClosed(); int aDefault = -ErrorCodes.ENOENT(); return applyIfPresent(s, (peergosStat) -> annotateAttributes(s, peergosStat, fileStat), aDefault); } @Override public int readlink(String s, Pointer pointer, @size_t long l) { throw new IllegalStateException("Unimplemented"); } @Override public int mknod(String s, @mode_t long l, @dev_t long l1) { throw new IllegalStateException("Unimplemented"); } private Optional<FilePointer> mkdir(String name, FileTreeNode node) { boolean isSystemFolder = false; try { return Optional.of(node.mkdir(name, context.network, isSystemFolder, context.crypto.random).get()); } catch (Exception ioe) { ioe.printStackTrace(); return Optional.empty(); } } @Override public int mkdir(String s, @mode_t long l) { ensureNotClosed(); Optional<PeergosStat> current = getByPath(s); if (current.isPresent()) return 1; Path path = Paths.get(s); String parentPath = path.getParent().toString(); Optional<PeergosStat> parentOpt = getByPath(parentPath); String name = path.getFileName().toString(); if (! parentOpt.isPresent()) return 1; PeergosStat parent = parentOpt.get(); return mkdir(name, parent.treeNode).isPresent() ? 0 : 1; } @Override public int unlink(String s) { ensureNotClosed(); try { Path requested = Paths.get(s); Optional<FileTreeNode> file = context.getByPath(s).get(); if (!file.isPresent()) return 1; Optional<FileTreeNode> parent = context.getByPath(requested.getParent().toString()).get();; if (!parent.isPresent()) return 1; boolean removed = file.get().remove(context.network, parent.get()).get(); return removed ? 0 : 1; } catch (Exception ioe) { ioe.printStackTrace(); return 1; } } @Override public int rmdir(String s) { ensureNotClosed(); Path dir = Paths.get(s); return applyIfPresent(s, (stat) -> applyIfPresent(dir.getParent().toString(), parentStat -> rmdir(stat, parentStat))); } @Override public int symlink(String s, String s1) { return unimp(); } private int rename(PeergosStat source, PeergosStat sourceParent, String sourcePath, String name) { ensureNotClosed(); try { Path requested = Paths.get(name); Optional<FileTreeNode> newParent = context.getByPath(requested.getParent().toString()).get();; if (!newParent.isPresent()) return 1; FileTreeNode parent = sourceParent.treeNode; source.treeNode.rename(requested.getFileName().toString(), context.network, parent); // TODO clean up on error conditions if (!parent.equals(newParent.get())) { Path renamedInPlacePath = Paths.get(sourcePath).getParent().resolve(requested.getFileName().toString()); Optional<FileTreeNode> renamedOriginal = context.getByPath(renamedInPlacePath.toString()).get();; if (!renamedOriginal.isPresent()) return 1; renamedOriginal.get().copyTo(newParent.get(), context.network, context.crypto.random).get(); boolean removed = source.treeNode.remove(context.network, parent).get(); if (!removed) return 1; } return 0; } catch (Exception ioe) { ioe.printStackTrace(); return 1; } } @Override public int rename(String s, String s1) { ensureNotClosed(); Path source = Paths.get(s); return applyIfPresent(s, (stat) -> applyIfPresent(source.getParent().toString(), parentStat -> rename(stat, parentStat, s, s1))); } @Override public int link(String s, String s1) { return unimp(); } @Override public int chmod(String s, @mode_t long l) { return unimp(); } @Override public int chown(String s, @uid_t long l, @gid_t long l1) { return unimp(); } @Override public int truncate(String s, @off_t long l) { ensureNotClosed(); //TODO return 0; } @Override public int open(String s, FuseFileInfo fuseFileInfo) { ensureNotClosed(); debug("OPEN %s", s); return 0; } @Override public int read(String s, Pointer pointer, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) { ensureNotClosed(); debug("READ %s, size %d offset %d ", s, size, offset); return applyIfPresent(s, (stat) -> read(stat, pointer, size, offset)); } @Override public int write(String s, Pointer pointer, @size_t long size, @off_t long offset, FuseFileInfo fuseFileInfo) { ensureNotClosed(); debug("WRITE %s, size %d offset %d ", s, size, offset); Path path = Paths.get(s); String parentPath = path.getParent().toString(); String name = path.getFileName().toString(); return applyIfPresent(parentPath, (parent) -> write(parent, name, pointer, size, offset), -ErrorCodes.ENOENT()); } @Override public int statfs(String s, Statvfs statvfs) { ensureNotClosed(); statvfs.f_bsize.set(128*1024L); // return 0; return unimp(); } @Override public int flush(String s, FuseFileInfo fuseFileInfo) { ensureNotClosed(); return 0; } // @Override // public int release(String s, FuseFileInfo fuseFileInfo) { // return 0; // } // @Override // public int fsync(String s, int i, FuseFileInfo fuseFileInfo) { // return unimp(); // } // @Override // public int setxattr(String s, String s1, Pointer pointer, @size_t long l, int i) { // return unimp(); // } // @Override // public int getxattr(String s, String s1, Pointer pointer, @size_t long l) { // return 0; // } // @Override // public int listxattr(String s, Pointer pointer, @size_t long l) { // return unimp(); // } @Override public int removexattr(String s, String s1) { return unimp(); } @Override public int opendir(String s, FuseFileInfo fuseFileInfo) { ensureNotClosed(); return 0; } @Override public int readdir(String s, Pointer pointer, FuseFillDir fuseFillDir, @off_t long l, FuseFileInfo fuseFileInfo) { ensureNotClosed(); return applyIfPresent(s, (stat) ->readdir(stat, fuseFillDir, pointer)); } @Override public int releasedir(String s, FuseFileInfo fuseFileInfo) { ensureNotClosed(); return 0; } @Override public int fsyncdir(String s, FuseFileInfo fuseFileInfo) { ensureNotClosed(); return 0; } @Override public Pointer init(Pointer pointer) { ensureNotClosed(); return pointer; } @Override public void destroy(Pointer pointer) { ensureNotClosed(); } @Override public int access(String s, int mask) { ensureNotClosed(); debug("ACCESS %s, mask %d", s, mask); return 0; } @Override public int create(String s, @mode_t long l, FuseFileInfo fuseFileInfo) { ensureNotClosed(); Path path = Paths.get(s); String parentPath = path.getParent().toString(); String name = path.getFileName().toString(); byte[] emptyData = new byte[0]; return applyIfPresent(parentPath, (stat) -> write(stat, name, emptyData, 0, 0)); } @Override public int ftruncate(String s, @off_t long l, FuseFileInfo fuseFileInfo) { ensureNotClosed(); Path path = Paths.get(s); String parentPath = path.getParent().toString(); return applyIfBothPresent(parentPath, s, (parent, file) -> truncate(parent, file, l)); } @Override public int fgetattr(String s, FileStat fileStat, FuseFileInfo fuseFileInfo) { ensureNotClosed(); return getattr(s, fileStat); } @Override public int lock(String s, FuseFileInfo fuseFileInfo, int i, Flock flock) { System.out.println("LOCK: "+s); ensureNotClosed(); return 0; } // @Override public int utimens(String s, Timespec[] timespecs) { ensureNotClosed(); int aDefault = -ErrorCodes.ENOENT(); Optional<PeergosStat> parentOpt = getParentByPath(s); if (! parentOpt.isPresent()) return aDefault; return applyIfPresent(s, (stat) -> { Timespec access = timespecs[0], modified = timespecs[1]; long epochSeconds = modified.tv_sec.longValue(); Instant instant = Instant.ofEpochSecond(epochSeconds); LocalDateTime lastModified = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); FileProperties updated = stat.properties.withModified(lastModified); /* debug("utimens %s, with %s, %d, %s, updated %s", s, lastModified.toString(), epochSeconds, modified.toString(), updated.toString()); */ try { boolean isUpdated = stat.treeNode.setProperties(updated, context.network, parentOpt.get().treeNode).get(); return isUpdated ? 0 : 1; } catch (Exception ex) { ex.printStackTrace(); return 1; } }, aDefault); } @Override public int bmap(String s, @size_t long l, long l1) { return unimp(); } @Override public int ioctl(String s, int i, Pointer pointer, FuseFileInfo fuseFileInfo, @u_int32_t long l, Pointer pointer1) { return unimp(); } @Override public int poll(String s, FuseFileInfo fuseFileInfo, FusePollhandle fusePollhandle, Pointer pointer) { return unimp(); } // @Override // public int write_buf(String s, FuseBufvec fuseBufvec, @off_t long l, FuseFileInfo fuseFileInfo) { // // return write(); // } // @Override // public int read_buf(String s, Pointer pointer, @size_t long l, @off_t long l1, FuseFileInfo fuseFileInfo) { // return 0; // } @Override public int flock(String s, FuseFileInfo fuseFileInfo, int i) { return unimp(); } @Override public int fallocate(String s, int i, @off_t long l, @off_t long l1, FuseFileInfo fuseFileInfo) { return unimp(); } private int unimp() { IllegalStateException ex = new IllegalStateException("Unimlemented!"); ex.printStackTrace(); throw ex; } protected Optional<PeergosStat> getByPath(String path) { try { Optional<FileTreeNode> opt = context.getByPath(path).get(); ; if (!opt.isPresent()) return Optional.empty(); FileTreeNode treeNode = opt.get(); FileProperties fileProperties = treeNode.getFileProperties(); return Optional.of(new PeergosStat(treeNode, fileProperties)); } catch (Exception e) { throw new RuntimeException(e); } } private Optional<PeergosStat> getParentByPath(String path) { String parentPath = Paths.get(path).getParent().toString(); return getByPath(parentPath); } protected int applyIf(String path, boolean isPresent, Function<PeergosStat, Integer> func, int _default) { Optional<PeergosStat> byPath = getByPath(path); if (byPath.isPresent() && isPresent) return func.apply(byPath.get()); return _default; } protected int applyIfPresent(String path, Function<PeergosStat, Integer> func) { int aDefault = 1; return applyIfPresent(path, func, aDefault); } protected int applyIfPresent(String path, Function<PeergosStat, Integer> func, int _default) { boolean isPresent = true; return applyIf(path, isPresent, func, _default); } private int applyIfBothPresent(String parentPath, String filePath, BiFunction<PeergosStat, PeergosStat, Integer> func) { int aDefault = 1; return applyIfPresent(parentPath, parentStat -> applyIfPresent(filePath, fileStat -> func.apply(parentStat, fileStat)), aDefault); } private int rmdir(PeergosStat stat, PeergosStat parentStat) { FileTreeNode treeNode = stat.treeNode; try { Boolean removed = treeNode.remove(context.network, parentStat.treeNode).get(); return 0; } catch (Exception ioe) { ioe.printStackTrace(); return 1; } } private int readdir(PeergosStat stat, FuseFillDir fuseFillDir, Pointer pointer) { try { Set<FileTreeNode> children = stat.treeNode.getChildren(context.network).get(); children.stream() .map(e -> e.getFileProperties().name) .forEach(e -> fuseFillDir.apply(pointer, e, null, 0)); return 0; } catch (Exception e) { e.printStackTrace(); return 1; } } protected Optional<byte[]> read(PeergosStat stat, long requestedSize, long offset) { long actualSize = stat.properties.size; if (offset > actualSize) { Optional.empty(); } long size = Math.min(actualSize - offset, requestedSize); byte[] data = new byte[(int) size]; if (data.length == 0) return Optional.of(data); try (AsyncReader asyncReader = stat.treeNode.getInputStream(context.network, context.crypto.random, actualSize, (l) -> {}).get()){ AsyncReader seeked = asyncReader.seek((int) (offset >> 32), (int) offset).get(); // N.B. Fuse seems to assume that a file must be an integral number of disk sectors, // so need to tolerate EOFs up end of last sector (4KiB) if (offset + size > actualSize + 4096) return Optional.empty(); int sizeToRead = offset + size >= actualSize ? (int) (actualSize - offset) : (int) size; int read = seeked.readIntoArray(data, 0, sizeToRead).get(); return Optional.of(data); } catch (Exception ioe) { ioe.printStackTrace(); return Optional.empty(); } } public int read(PeergosStat stat, Pointer pointer, long requestedSize, long offset) { Optional<byte[]> dataOpt = read(stat, requestedSize, offset); if (! dataOpt.isPresent()) return 1; byte[] data = dataOpt.get(); for (int i = 0; i < data.length; i++) { pointer.putByte(i, data[i]); } return data.length; } private byte[] getData(Pointer pointer, int size) { if (Integer.MAX_VALUE < size) { throw new IllegalStateException("Cannot write more than " + Integer.MAX_VALUE + " bytes"); } byte[] toWrite = new byte[size]; pointer.get(0, toWrite, 0, size); return toWrite; } public int truncate(PeergosStat parent, PeergosStat file, long size) { debug("TRUNCATE file %s, size %d", file.properties.name, size); try { if (size > Integer.MAX_VALUE) throw new IllegalStateException("Trying to truncate/extend to > 4GiB! "+ size); byte[] original = new byte[(int)file.properties.size]; Serialize.readFullArray(file.treeNode.getInputStream(context.network, context.crypto.random, l -> {}).get(), original); // TODO do this smarter by only writing the chunk containing the new endpoint, and deleting all following chunks // or extending with 0s byte[] truncated = Arrays.copyOfRange(original, 0, (int)size); file.treeNode.remove(context.network, parent.treeNode); boolean b = parent.treeNode.uploadFile(file.properties.name, new AsyncReader.ArrayBacked(truncated), truncated.length, context.network, context.crypto.random, l -> {}, context.fragmenter()).get(); return b ? (int) size : 1; } catch (Throwable t) { t.printStackTrace(); return 1; } } public int write(PeergosStat parent, String name, byte[] toWrite, long size, long offset) { try { long updatedLength = size + offset; if (Integer.MAX_VALUE < updatedLength) { throw new IllegalStateException("Cannot write more than " + Integer.MAX_VALUE + " bytes"); } boolean b = parent.treeNode.uploadFileSection(name, new AsyncReader.ArrayBacked(toWrite), offset, offset + size, context.network, context.crypto.random, l -> {}, context.fragmenter()).get(); return b ? (int) size : 1; } catch (Throwable t) { t.printStackTrace(); return 1; } } public int write(PeergosStat parent, String name, Pointer pointer, long size, long offset) { byte[] data = getData(pointer, (int) size); return write(parent, name, data, size, offset); } /** * JNR doesn't play nicely with debugger at all => debugging like it's 1990 */ private void debug(String template, Object... obj) { String msg = String.format(template, obj); System.out.println(msg); } }