VFS - 虚拟文件系统基本操作方法的封装

接前一篇 VFS - 代码生成器预览功能实现 ,上一篇讲到了 mkdirs 封装创建目录的方法,接下来先处理前文中的BUG,然后再封装文件的基础方法。

文件的 BUG

在前一篇文章中,认为一个文件的 nametype 同时决定了唯一的一个文件,这个设计没有问题,但是经过在不同操作系统测试发现,同一个文件名只能在一个目录中出现一次,名字决定了唯一的一个文件,类型决定了可以对这个文件进行什么样的操作。并且默认情况下只有 Linux 对文件名区分大小写,Window 和 macOS 默认都不区分大小写,因为文件名的类型为 Path,我特地看了看不同操作系统下 Path 的比较方式,JDK 两类系统的源码实现:

Windows 的实现中最终比较 Path 时会转换为大写进行比较,Unix 实现不会进行转换。因为文件名使用的 Path 类型,所以直接比较 Path name 就支持了不同操作系统的不同实现。

由于 macOS 本身默认不区分大小写(和磁盘分区格式有关),但是 macOS 的 jdk 实现使用的 UnixPath,这就产生了一个 Java 的BUG:macOS 的代码上区分大小写,真正创建文件时又不区分大小写,会导致代码上多出的文件丢失

例如先创建一个 a.txt 文件,在创建一个 A.txt 文件时,代码中认为有两个文件,实际硬盘上只有一个 a.txt 文件,当两个文件依次写入内容时,第二个文件的内容会覆盖 a.txt 的内容。所以在真正写代码时,文件名不能区分大小写。

先修改下面两个基础方法:

@Override
public boolean equals(Object o) {
  if (this == o) {
    return true;
  }
  if (o == null || getClass() != o.getClass()) {
    return false;
  }
  VFSNode vfsNode = (VFSNode) o;
  return name.equals(vfsNode.name);
}

@Override
public int hashCode() {
  return Objects.hash(name);
}

由于只通过文件名判断文件是否已经存在,因此添加直接的子文件时增加判断已经存在的文件类型是否一致:

private void addChild(VFSNode child) {
  if (CollUtil.isEmpty(this.files)) {
    this.files = new ArrayList<>();
  }
  if (this.files.contains(child)) {
    VFSNode same = getChild(child.name);
    if (same.type != child.type) {
      throw new RuntimeException("已经存在类型为 "
          + same.type + " 的文件,无法添加 " + child.type + " 类型");
    }
  } else {
    this.files.add(child);
    child.parent = this;
  }
}

最后一个改动的地方就是添加子文件过程中,需要判断中间的文件是否为目录,如果不是目录也不允许往下添加子文件。

protected void addVFSNode(VFSNode node, Path relativePath) {
  int nameCount = relativePath.getNameCount();
  if (nameCount > 1) {
    Path name = relativePath.getName(0);
    VFSNode vfsNode = getChild(name);
    if (vfsNode == null) {
      vfsNode = new VFSNode(name, Type.DIR);
      addChild(vfsNode);
    }
    //增加判断节点类型
    if(vfsNode.isDirectory()) {
      vfsNode.addVFSNode(node, relativePath.subpath(1, nameCount));
    } else {
      throw new RuntimeException("无法向文件 " + vfsNode.name + " 下添加子文件");
    }
  } else if (nameCount == 1) {
    addChild(node);
  }
}

经过上面的修改后,VFS中的文件名和类型的规则和操作系统就一致了,为后面和操作系统上的文件系统交互打下了基础。

VFS 文件基本操作

VFSNode 中通过简单的 writeread 方法实现了虚拟文件的读写,而且文件还支持多次覆盖写入,并且记录文件写入内容的历史,在外层 VFS 中封装时,仍然是先要通过相对路径找到要写入的文件,如果文件不存在还要先创建该文件。找到要写入的虚拟文件后,调用 VFSNode 的写入方法就可以实现。在 VFSNode 中也提过 找到要操作的文件是其他操作的基础,这里还是先封装该方法:

/**
 * 查找指定类型节点
 *
 * @param relativePath 相对路径
 * @return
 */
protected VFSNode findVFSNode(Path relativePath) {
  return findVFSNode(relativePath, null, false);
}

/**
 * 查找指定类型节点
 *
 * @param relativePath 相对路径
 * @param type         文件类型
 * @return
 */
protected VFSNode findVFSNode(Path relativePath, Type type) {
  return findVFSNode(relativePath, type, false);
}

/**
 * 查找节点
 *
 * @param relativePath      相对路径
 * @param type              文件类型
 * @param createIfNotExists 如果不存在就创建,这种情况要么返回节点要么抛出异常
 * @return
 */
protected VFSNode findVFSNode(Path relativePath, Type type, boolean createIfNotExists) {
  //检查相对路径是否合法(不能超出根路径范围)
  checkRelativePath(relativePath);
  //根据相对路径查找节点
  VFSNode node = getVFSNode(relativePath);
  //当节点存在、指定了类型、并且类型不一致时
  if (node != null && type != null && node.type != type) {
    //如果需要创建就抛出异常
    if (createIfNotExists) {
      throw new RuntimeException("已经存在类型为 " + node.type
          + " 的文件,无法创建 " + type + "类型的同名文件");
    }
    //不需要创建就因为类型不一致返回null
    return null;
  }
  //文件不存在并且需要创建时
  if (node == null && createIfNotExists) {
    //新建节点
    node = new VFSNode(relativePath.getFileName(), type);
    //根据相对路径添加节点
    addVFSNode(node, relativePath);
  }
  return node;
}

下面开始基于这个基础方法开始实现文件的基本操作,先看删除文件。

VFS 文件操作 - 删除

mkdirs 方法类似,有三种形式参数的方法,删除文件时,查找到对应的 VFSNode 调用对象上的 delete 方法断绝和父级的关系即可。

/**
 * 删除指定文件
 *
 * @param file 文件
 * @return true 删除成功,false 文件不存在
 */
public boolean delelte(File file) {
  return delelte(relativize(file));
}

/**
 * 删除指定文件
 *
 * @param relativePath 相对路径
 * @return true 删除成功,false 文件不存在
 */
public boolean delelte(String relativePath) {
  return delelte(toPath(relativePath));
}

/**
 * 删除相对路径的文件
 *
 * @param relativePath 相对路径
 * @return true 删除成功,false 文件不存在
 */
public boolean delelte(Path relativePath) {
  VFSNode vfsNode = findVFSNode(relativePath);
  if (vfsNode != null) {
    vfsNode.delete();
    return true;
  }
  return false;
}

删除非常简单,下面的写入文件也很简单。

VFS 文件操作 - 写文件

为了支持更多类型的文件,VFSNode 中的文件内容使用的 byte[] 字节数组类型,因此首先提供写入 bytes[] 的方法:

/**
 * 写入文件内容
 *
 * @param file  文件
 * @param bytes 内容
 */
public void write(File file, byte[] bytes) {
  write(relativize(file), bytes);
}

/**
 * 写入文件内容
 *
 * @param relativePath 相对路径
 * @param bytes        内容
 */
public void write(String relativePath, byte[] bytes) {
  write(toPath(relativePath), this.bytes);
}

/**
 * 写入相对文件内容
 *
 * @param relativePath 相对路径
 * @param bytes        文件内容
 */
public void write(Path relativePath, byte[] bytes) {
  //获取文件并写入数据,获取时指定为 FILE 类型,如果文件不存在就创建
  findVFSNode(relativePath, Type.FILE, true).write(bytes);
}

byte[] 类型的内容更通用,String 类型的文件比 byte[] 更常用。

/**
 * 写入文件内容
 *
 * @param file    文件
 * @param content 内容
 */
public void write(File file, String content) {
  write(relativize(file), content);
}

/**
 * 写入文件内容
 *
 * @param relativePath 相对路径
 * @param content      内容
 */
public void write(String relativePath, String content) {
  write(toPath(relativePath), content);
}

/**
 * 写入相对文件内容
 *
 * @param relativePath 相对路径
 * @param content      文件内容
 */
public void write(Path relativePath, String content) {
  findVFSNode(relativePath, Type.FILE, true).write(content.getBytes(StandardCharsets.UTF_8));
}

基本操作功能测试

VFS vfs = VFS.of("/");
vfs.mkdirs("/a");
vfs.mkdirs("/a/b");
vfs.mkdirs("/a/c");
vfs.mkdirs("/a/c");
vfs.mkdirs("/a/d/e.txt");
vfs.write("/a/help.txt", "帮助文档");
vfs.delelte("/a/c");
System.out.println(vfs.print());

输出的文件结构如下:

/
└── a
    ├── b
    ├── d
    │   └── e.txt
    └── help.txt

又是未完,待续…

又写了很长时间还没写完,一个是想表达的内容比较多,写的太长怕一次读不完,另外更主要的原因是:写文章的时候要换一个角度对代码进行重新理解并展示给读者,重新理解的过程也涉及到了代码的重构,因此写文章的同时也在写代码。

虽然未完,但是后续要写的整体内容基本上已经明确了,后续还有一篇,主要是加载指定的目录到VFS中,将VFS的内容写入到指定的目录中。除了目录外,为了便于使用专门增加了 ZIP 文件的支持,这次 ZIP 导入导出的功能让我又重新认识了 Java 的 ZIP,下一篇文章在详细介绍。

由于代码还在变,恐怕最后一篇文章写出前不适合再发源码,等最后再重新给所有在博客、微信留邮箱的各位读者朋友发送一遍代码。

上一篇:一位码农决定去当rapper


下一篇:IDEA 提示Cannot resolve symbol