|
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; |
|
} |
|
|
|
} |
不瞒你们说,刚开始我是想写完整的步骤的,然后,然后困了。