# 虚拟文件系统(VFS) Deft 的虚拟文件系统层(VFS)位于 `fs/` 命名空间之后,让普通文件操作可以依据路径的前导路径段路由到不同后端——另一个目录、通过 ssh 连接的远程主机。它仿照 TCL 的 VFS,并适配了 Deft 的约定:挂载以数据形式声明(在预置文件中,与所有其他项目级配置一样),调用处仍使用普通的 Deft 字符串。
[vfs/mount "prod" %{ :kind :ssh :host "10.0.0.5" :root "/srv/app" }]

[fs/read  "prod/etc/hostname"]     # read /srv/app/etc/hostname on 10.0.0.5
[fs/write "prod/config.toml" "..."]
[fs/list  "prod"]                  # bare mount name = the mount root
[fs/read  "/etc/hostname"]         # no mount named → native, as always
无需学习新的路径语法:第一个路径段与挂载名匹配的路径会路由到该挂载的驱动;其他所有路径的行为与以往完全一致。 ## 声明挂载 ### 在预置文件中 `.deft/prelude.dft` 中的 `:mounts` 映射是声明挂载的惯用位置。值类型决定挂载的含义: - 字符串值 —— 经典的导入挂载:`import "<name>/..."` 通过该目录解析。绝不会被 `fs/` 拦截。 - 映射或 `%Local{}`/`%Ssh{}` 值 —— VFS 挂载:该名称下的 `fs/*` 路径会路由到其驱动。绝不是导入挂载。
%Prelude{
    :mounts %{
        :scripts "scripts"                                   # import mount
        :docs    %Local{ :root "/usr/share/doc" }            # VFS: local dir
        :prod    %{ :kind :ssh :host "10.0.0.5" :root "/srv/app"
                    :user "root" :port 22 } } }
预置文件中的挂载会在启动时写入进程全局表,因此每个应用线程都能看到它们——包括跳过预置文件加载的子运行时。 ### 在运行时
[vfs/mount "prod" %{ :kind :ssh :host "10.0.0.5" :root "/srv/app" }]
[vfs/unmount "prod"]
[vfs/list]        # → %{ :prod %{ :kind "ssh" :host ... :root ... } }
`vfs/mount` 在 `"vfs"` 发布/订阅主题上发布消息(`:data` 下的 `%{ :name :value :old }`),与 `config/*` 的响应式模式一致。运行时挂载还会写入共享配置注册表的 `vfs.mount.<name>`,供可见性工具使用。 ## 挂载规格 | 字段 | 本地 | ssh | | |---|---|---|---| | `:kind` | `:local` | `:ssh` | 必填(或使用 `%Local{}`/`%Ssh{}`) | | `:root` | 本地锚定目录 | 远程锚定目录 | 本地挂载必填 | | `:host` | — | 主机名或 `user@host` | ssh 必填 | | `:user` | — | ssh 用户 | 可选 | | `:port` | — | 端口(默认 22) | 可选 | | `:identity` | — | 密钥文件 | 可选 | | `:jump` | — | 跳板主机(ssh `-J`) | 可选 | | `:options` | — | 额外的 `-o` 选项,`@{"Opt=val"}` | 可选 | ## 路径语义 - 最长前缀优先:存在 `deft` 和 `deft/ui` 两个挂载时,路径 `deft/ui/button.dft` 路由到 `deft/ui`;`deft/other/x` 路由到 `deft`。 - 整段匹配:`products/x` 不会匹配名为 `prod` 的挂载。 - 挂载即 chroot:`..` 无法逃逸——`prod/../../etc` 会解析到挂载根内部。 - 裸挂载名即根:`fs/list "prod"` 列出挂载根本身。 - 判定函数保持一致:`fs/absolute?` 和 `fs/abs` 将挂载锚定的路径视为完全限定路径(`fs/abs` 为恒等操作);`fs/dir`、`fs/base`、`fs/ext`、`fs/join` 照常对虚拟字符串进行操作。 ## 哪些会路由,哪些不会 所有整文件操作的 `fs/*` 动词都会通过挂载路由:`fs/read`、`fs/write`、`fs/append`、`fs/read-lines`、`fs/write-lines`、`fs/stat`、`fs/list`、`fs/readdir`、`fs/glob`、`fs/exists?`、`fs/file?`、`fs/dir?`、`fs/mkdir`、`fs/remove`、`fs/rename`(同一挂载内)、`fs/copy`(也可跨挂载——它由读 + 写组合而成)。 挂载上会干净地返回 `%Err`(v1): - `fs/watch` —— inotify 仅限本地;远程监视需要轮询。 - `fs/link` —— 符号链接无法映射到远程语义。 - 带 `:recurse` 的 `fs/glob` —— 尚无驱动遍历实现。 - 跨两个不同挂载的 `fs/rename`。 按设计仅限宿主(永不虚拟化):`fs/tempdir`、`fs/home`、`fs/tmp-dir`、`fs/tmp-path`、`fs/local-path`、`fs/local-dir`。`sqlite/*` 命名空间通过其自带的引擎读取,不经过挂载路由——如果需要打开远程数据库,请先将远程文件实体化到本地临时路径。 ## ssh 后端 `:ssh` 后端会调用系统 `ssh` 客户端,每次操作启动一个进程。你已了解的 ssh 用法全都自动适用:密钥与代理认证、`~/.ssh/config` 别名、`known_hosts`、跳板主机、`ControlMaster` 连接复用。 默认的 `-o BatchMode=yes -o ConnectTimeout=10` 在 `:options` 之前应用,因此挂载可以覆盖其中任意一项(ssh 以最后一个 `-o` 为准)。BatchMode 意味着仅使用密钥或代理——交互式密码提示会快速失败而不是挂起脚本,这对无头运行时而言是正确行为。 远程端只需要一个 POSIX shell,以及 GNU 或 BusyBox 的 `stat` 和 `ls`(任何典型的 Linux 或 BSD 用户空间即可)。文件内容通过 `cat` 的标准输入/输出传输——不使用临时文件——远程路径经过 shell 引号处理,因此空格和元字符都是安全的。 由于每次操作都会启动一个进程,单次调用延迟为 ssh 往返量级。对于配置文件、日志和部署脚本来说足够;计划在同一接口之后升级为持久 sftp 会话。 ## 沙箱说明 ssh 挂载需要子进程和网络访问,而 Landlock 沙箱(`DEFT_SANDBOX=1`)会拒绝这些请求。当在沙箱会话中声明 ssh 挂载时,预置文件加载器会发出警告。 ## 示例 ### 部署脚本
[vfs/mount "prod" %{ :kind :ssh :host "prod.example" :root "/srv/app" }]

def local-hash [unwrap [fs/read "build/SHA"]]
def remote-hash [unwrap [fs/read "prod/SHA"]]
if ($local-hash != $remote-hash) {
    [fs/write "prod/SHA" $local-hash]
    [fs/copy "build/app.tar.gz" "prod/app.tar.gz"]
}
### 以短名称读取文档目录
[vfs/mount "docs" %Local{ :root "/usr/share/doc" }]
[fs/glob "docs" "*.md"]
## 另见 - 预置文件 —— `:mounts`,声明式挂载所在之处 - 配置 —— `vfs/*` 响应式所遵循的注册表模式 - 沙箱 —— 与 Landlock 的相互影响 - 参考:`vfs/mount`、`vfs/unmount`、`vfs/list`