Skip to content

Instantly share code, notes, and snippets.

@LamGC
Last active September 12, 2023 14:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LamGC/ebd1cdd74dd8532485d6762c029f276d to your computer and use it in GitHub Desktop.
Save LamGC/ebd1cdd74dd8532485d6762c029f276d to your computer and use it in GitHub Desktop.
通过 'jnr-fuse' 实现一个目录映射的虚拟文件系统

通过 'jnr-fuse' 实现一个目录映射的虚拟文件系统

前提概要

说实话,jnr-fuse 官方自带的示例真的有些难看懂,而且 ErrorCodes 的命名属实降智(不是很懂某软的命名缩写习惯)。
不过至少能用了不是吗?

指路:

jnr-fuse 项目中自带一个 HelloWorld 示例 HelloFuse 和一个内存盘实现示例 MemoryFS(千万不要真拿去用了,性能较为低下且没有持久化功能),
看这示例真的懵了半天,中途也是遇到了不少 bug,不过总之是把一个目录映射的示例做出来了,来分享一下经验。

如何初步组建一个 FileSystem?

请配合随附代码 SimpleFileSystem.java 阅读本文。

首选创建一个 SimpleFileSystem 类,实现 FuseStubFS 类,其实你也可以继承它的父类 AbstractFuseFS,但是真心不建议你直接去实现它所实现的接口 FuseFS,因为真要实现起来真不需要实现所有方法,只需要几个就好。
既然我们一开始先做个目录映射的虚拟文件系统,那么我们应该要有指定的根目录。
由于 FuseStubFSFuseFS 中所有需要实现的接口都实现了(包括必须的,对,就是这么难受),所以呢我们需要按需(必须)实现某些方法。
目前弄懂了一些满足读写所必须实现的部分方法:

  • create 创建文件
  • unlink 删除文件
  • open 打开文件(此时可以创建例如 FileChannel,不过不推荐,原因见已完成的示例代码中对应方法的注释)
  • release 关闭文件(如果打开了 Stream 或者 Channel,可以在此进行关闭)
  • read 读取文件数据
  • write 写入文件数据
  • getattr 获取文件、目录的属性
  • mkdir 创建目录
  • readdir 读取目录中的项目
  • statfs 获取文件系统的元数据;根据说明,Windows下必须实现,用于获取存储设备的信息(比如容量、块大小、已用量)
  • rename 重命名目录或文件
  • truncate 清空文件数据,某些程序(比如 Notepad++ 在写入前会清空)

通过实现这些方法,就能实现一个具有读写操作的 FileSystem 了,具体可查阅附上的,已完成的示例代码。

package net.lamgc.example.fuse;
import jnr.ffi.Platform;
import jnr.ffi.Pointer;
import jnr.ffi.types.mode_t;
import jnr.ffi.types.off_t;
import jnr.ffi.types.size_t;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.serce.jnrfuse.ErrorCodes;
import ru.serce.jnrfuse.FuseFillDir;
import ru.serce.jnrfuse.FuseStubFS;
import ru.serce.jnrfuse.struct.FileStat;
import ru.serce.jnrfuse.struct.FuseFileInfo;
import ru.serce.jnrfuse.struct.Statvfs;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;
import static jnr.ffi.Platform.OS.WINDOWS;
public class SimpleFileSystem extends FuseStubFS {
private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName() + "@" + this.hashCode());
private final File rootDirectory;
private final Map<File, FileChannel> openMap = new HashMap<>();
private final Map<FileChannel, OpenOption> channelStatus = new HashMap<>();
public SimpleFileSystem(File rootDirectory) {
this.rootDirectory = rootDirectory;
}
@Override
public int create(String path, @mode_t long mode, FuseFileInfo fi) {
log.info("正在创建文件, filePath: {}, mode: {}", path, mode);
if (getPath(path).exists()) {
// 文件存在的情况下返回 EEXIST(文件已存在) 错误码
return -ErrorCodes.EEXIST();
}
// 获取父级 File 并检查其是否为目录
// 其实这里应该还要检查是否在映射的目录下的,不过假设请求没问题,就忽略了
File parent = getParentPath(path);
if (parent.isDirectory()) {
try {
// 当没有问题且不需要返回什么的时候就返回 0,代表操作成功
return getPath(path).createNewFile() ? 0 : -ErrorCodes.EIO();
} catch (IOException e) {
log.error("创建文件失败", e);
// 当读写发生错误时的万能代码(如果能在 ErrorCodes 里找到对应代码就最好不过,没找到可使用 EIO 代替)
return -ErrorCodes.EIO();
}
}
// 未找到指定文件或目录则返回 ENOENT(No such file or directory) 错误码
return -ErrorCodes.ENOENT();
}
@Override
public int getattr(String path, FileStat stat) {
log.info("正在获取属性, path: {}", path);
File p = getPath(path);
if (p.exists()) {
if(p.isDirectory()) {
// FileStat.S_IFDIR 为目录类型,还有更多类型可翻阅 FileStat 代码(明明写了注释却不写 Javadoc 格式真的难受 :( )
// 0777 指定了访问权限(Windows 似乎可以参考 Linux 的那套)
stat.st_mode.set(FileStat.S_IFDIR | 0777);
} else if(p.isFile()) {
stat.st_mode.set(FileStat.S_IFREG | 0777);
stat.st_size.set(p.length());
}
// 不是很明白用来做什么,但必须设置
stat.st_uid.set(getContext().uid.get());
stat.st_gid.set(getContext().gid.get());
return 0;
}
return -ErrorCodes.ENOENT();
}
private File getParentPath(String path) {
return getPath(path).getParentFile();
}
private File getPath(String path) {
return new File(rootDirectory, path);
}
@Override
public int mkdir(String path, @mode_t long mode) {
log.info("正在创建目录, path: {}, mode: {}", path, mode);
if (getPath(path).exists()) {
// 如果已存在
return -ErrorCodes.EEXIST();
}
File parent = getParentPath(path);
if (parent.isDirectory()) {
// 如果成功返回 0,否则返回 EIO
return getPath(path).mkdir() ? 0 : -ErrorCodes.EIO();
}
return -ErrorCodes.ENOENT();
}
@Override
public int read(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) {
log.info("正在读取文件, path: {}, size: {}, offset: {}", path, size, offset);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (p.isDirectory()) {
return -ErrorCodes.EISDIR();
}
try {
// 这里对 FileChannel 做了缓存,不做缓存的情况下速度慢到极点(1MB/s),但是万万不可在 open 里创建( FileChannel 分了打开方式)
FileChannel fileInput = openMap.get(p);
// 检查打开类型是否为 Read,不是就换掉。
if(fileInput == null || !channelStatus.get(fileInput).equals(StandardOpenOption.READ)) {
release(path, fi);
FileChannel newChannel = new FileInputStream(p).getChannel();
openMap.put(p, newChannel);
channelStatus.put(newChannel, StandardOpenOption.READ);
fileInput = newChannel;
}
int bytesToRead = (int) Math.min(p.length() - offset, size);
ByteBuffer bytesRead = ByteBuffer.allocate(bytesToRead);
synchronized (this) {
fileInput.position((int) offset);
fileInput.read(bytesRead);
buf.put(0, bytesRead.array(), 0, bytesToRead);
}
// 注意,这里需要返回读取的字节数!
return bytesToRead;
} catch (IOException e) {
e.printStackTrace();
return -ErrorCodes.EIO();
}
}
@Override
public int readdir(String path, Pointer buf, FuseFillDir filter, @off_t long offset, FuseFileInfo fi) {
log.info("正在读取目录列表, path: {}, offset: {}", path, offset);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (!p.isDirectory()) {
return -ErrorCodes.ENOTDIR();
}
// 似乎是必须加入的?照顾一下 Linux 的似乎。
filter.apply(buf, ".", null, 0);
filter.apply(buf, "..", null, 0);
File[] files = p.listFiles();
if(files != null) {
for (File file : files) {
// 创建一个新的文件元数据结构对象
FileStat fileStat = FileStat.of(buf);
// 注意:传递给 getattr 的路径需要是子项路径
if(getattr(appendPath(path, file.getName()), fileStat) != 0) {
// 如果无法读取指定子项的属性,就跳过
continue;
}
// 加入一个项目,参数 1 只需要传递方法传入的 buf 即可,参数 4 也只需要传递 0 就好了
// 参数 2 传入子项文件名(不含全路径),参数 3 传入通过 getattr 获取的文件元数据结构即可
filter.apply(buf, file.getName(), fileStat, 0);
}
}
// 本方法只需要返回 0 代表成功
return 0;
}
@Override
public int statfs(String path, Statvfs stbuf) {
log.info("正在获取文件系统属性, path: {}", path);
// 仅限 Windows
if (Platform.getNativePlatform().getOS() == WINDOWS) {
// 官方注释:
// statfs needs to be implemented on Windows in order to allow for copying
// data from other devices because winfsp calculates the volume size based
// on the statvfs call.
// see https://github.com/billziss-gh/winfsp/blob/14e6b402fe3360fdebcc78868de8df27622b565f/src/dll/fuse/fuse_intf.c#L654
if ("/".equals(path)) {
// 设定块数量
stbuf.f_blocks.set(1024 * 1024); // total data blocks in file system
// 设定块大小
stbuf.f_frsize.set(1024); // fs block size
// 剩余可用块
stbuf.f_bfree.set(1024 * 1024); // free blocks in fs
}
}
// 虽然调用父类方法,但实际上也只是返回 0 而已
return super.statfs(path, stbuf);
}
@Override
public int rename(String path, String newName) {
log.info("正在更改文件名, path: {}, newName: {}", path, newName);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
File newParent = getParentPath(newName);
if (newParent == null) {
return -ErrorCodes.ENOENT();
}
if (!(newParent.isDirectory())) {
return -ErrorCodes.ENOTDIR();
}
return p.renameTo(getPath(newName)) ? 0 : -ErrorCodes.EIO();
}
@Override
public int rmdir(String path) {
log.info("正在删除目录, path: {}", path);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (!p.isDirectory()) {
return -ErrorCodes.ENOTDIR();
}
String[] list = p.list();
if(list != null && list.length != 0) {
// 这里用于检查目录是否为空,如果不为空则返回 ENOTEMPTY(Directory not empty)
return -ErrorCodes.ENOTEMPTY();
}
return p.delete() ? 0 : -ErrorCodes.EIO();
}
@Override
public int truncate(String path, long offset) {
log.info("正在清空文件数据, path: {}, offset: {}", path, offset);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (!p.isFile()) {
return -ErrorCodes.EISDIR();
}
try (FileChannel fileInput = new FileInputStream(p).getChannel()) {
fileInput.position(offset);
fileInput.truncate(0);
return 0;
} catch (IOException e) {
log.error("清空文件数据时发生异常", e);
return -ErrorCodes.EIO();
}
}
@Override
public int unlink(String path) {
log.info("正在删除文件, path: {}", path);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
return p.delete() ? 0 : -ErrorCodes.EIO();
}
@Override
public int open(String path, FuseFileInfo fi) {
log.info("正在打开文件, path: {}", path);
// 建议该方法仅检查是否存在和是否为文件即可,不适宜打开 FileChannel
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (!p.isFile()) {
return -ErrorCodes.EISDIR();
}
return 0;
}
@Override
public int release(String path, FuseFileInfo fi) {
log.info("正在释放文件, path: {}", path);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (!p.isFile()) {
return -ErrorCodes.EISDIR();
}
// 这里将打开的 Channel 关闭即可,如果没开,就不管它
if(openMap.containsKey(p)) {
try {
openMap.get(p).close();
} catch (IOException e) {
log.error("释放文件时发生异常", e);
return -ErrorCodes.EIO();
} finally {
openMap.remove(p);
}
}
return 0;
}
@Override
public int write(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) {
log.info("正在写入文件, path: {}, size: {}, offset: {}", path, size, offset);
File p = getPath(path);
if (!p.exists()) {
return -ErrorCodes.ENOENT();
}
if (!p.isFile()) {
return -ErrorCodes.EISDIR();
}
try {
// 同 read 方法一样
FileChannel fileOutput = openMap.get(p);
if(fileOutput == null || !channelStatus.get(fileOutput).equals(StandardOpenOption.WRITE)) {
release(path, null);
FileChannel newChannel = new FileOutputStream(p).getChannel();
openMap.put(p, newChannel);
channelStatus.put(newChannel, StandardOpenOption.WRITE);
fileOutput = newChannel;
}
int maxWriteIndex = (int) (offset + size);
byte[] bytesToWrite = new byte[(int) size];
synchronized (this) {
if (maxWriteIndex > p.length()) {
fileOutput.truncate(maxWriteIndex);
}
buf.get(0, bytesToWrite, 0, (int) size);
fileOutput.position((int) offset);
fileOutput.write(ByteBuffer.wrap(bytesToWrite));
}
return (int) size;
} catch (IOException e) {
e.printStackTrace();
return -ErrorCodes.EIO();
}
}
public static String appendPath(String path, String appendStr) {
String trimPath = path.trim();
String pathSeparator = File.separator;
if(trimPath.endsWith("/") || trimPath.endsWith("\\")) {
pathSeparator = "";
}
return path + pathSeparator + appendStr;
}
}
@LamGC
Copy link
Author

LamGC commented Jul 25, 2020

不瞒你们说,刚开始我是想写完整的步骤的,然后,然后困了。

@wxiaoguang
Copy link

ErrorCodes 这个纯粹是为了延续 posix-style 才这样写的吧……

话说 java 真的适合用来写 fuse 吗?好奇ing

(路过参观一下)

@LamGC
Copy link
Author

LamGC commented Sep 12, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment