文件系统的 “代理”

1,407 阅读3分钟
目标:给指定目录设置“反向代理”,使得其内容经过一个指定的 f() 进行变换。如果源目录的 file 更新了,f(file) 也要更新。

go-fuse unionfs

网络上的反向代理我们已经非常清楚了。在应用服务器之前架设一个反向代理的服务器。通过访问反向代理,我们可以添加了一些http header,把http转换成https,诸如此类的。

在文件系统上也可以做这样的事情吗?我们能不能提供一个文件目录,是另外一个目录的反向代理。在新目录上实现文件的增减,以及文件内容的变换?

答案是可行的。这个实现基于 hanwen/go-fuse 。它提供一个 golang 实现的用户态文件系统(FUSE)。利用其中的 unionfs,我们可以得到以下的行为。

$unionfs
Usage:
  unionfs MOUNTPOINT RW-DIRECTORY RO-DIRECTORY ...

其中 MOUNTPOINT 是我们最终访问的目录,RO目录是提供了原始文件的目录,而RW目录以COW(Copy-on-Write)的方式记录了我们在RO目录上的改动。

假设我们把ro目录和rw目录合并到了fuse目录

$unionfs fuse rw ro
  • RW/RO 内容合并:ro 下的文件为 [a]。rw 下的文件为 [b]。那么 fuse 下的文件为 [a,b]
  • RW/RO 文件同名:ro 下的文件为 [a]。rw 下的文件也为 [a]。那么 fuse 下的文件为 [a],其中 a 的内容是 rw 的。
  • 在 fuse 中新增:ro 下的文件为 []。rw 下的文件为 []。如果给 fuse 新增文件a,从 [] 变为 [a]。那么结果是 ro 下的文件为 [],而 rw 变为了 [a]。
  • 在 fuse 中更新:如果 a 同时存在于 ro, rw 中,更新 fuse 的 a,则等同于更新 rw 的。如果 a 只存在于 rw 中,更新 fuse 的 a,则等于更新 rw 的。如果 a 只存在于 ro 中,更新 fuse 的 a 不会更新 ro 的,而是会在 rw 中新增一个文件 a。
  • 在 fuse 中删除:如果 a 同时存在与 ro,rw 中,删除 fuse 中的 a,不仅仅是从 rw 中把 a 删除了,同时会在 rw/GOUNIONFS_DELETIONS/ 目录下记录 a 已经被删除了。如果 a 只存在于 rw 中,则仅仅是从 rw 里把 a 删除。如果 a 只存在于 ro 中,则只会在 rw/GOUNIONFS_DELETIONS/ 目录下记录 a 已经被删除了。
  • 在 ro 中新增:新增的文件立即可以在 fuse 中可见
  • 在 ro 中更新:如果没有被 rw 覆盖的话,更新也可以在 fuse 中可见
  • 在 ro 中删除:如果没有被 rw 覆盖的话,删除之后在 fuse 中也看不到了

这个和 overlayfs 的不同在于 fuse 合并了 rw 和 ro 之后,ro 的改动对于 fuse 仍然是可见的。

www.kernel.org/doc/Documen…


Changes to underlying filesystems
---------------------------------

Offline changes, when the overlay is not mounted, are allowed to either
the upper or the lower trees.

Changes to the underlying filesystems while part of a mounted overlay
filesystem are not allowed.  If the underlying filesystem is changed,
the behavior of the overlay is undefined, though it will not result in
a crash or deadlock.

似乎 unionfs 要比 overlayfs 要宽松很多。

lambdafs

基于 go-fuse 的 unionfs,我们可以实现一个 lambdafs。其原理就是对 ro 里的部分文件进行 f() 的映射,把结果写入到 rw 目录里。从而使得最终的 fuse 目录里,对 ro 的部分文件进行了一个 lambda 的转换。而且这个转换是 lazy 的,而且是可更新的。只有当我们 cat 了 fuse 里的文件,这个时候才会触发 lambda。对于那些没有读到的文件,则可以先不处理。

我们要应用的 lambda 是这样的

UpdateFile: func(filePath string) ([]byte, error) {
	if !strings.HasSuffix(filePath, ".php"){
		return nil, nil
	}
	content, err := ioutil.ReadFile(filePath)
	if err != nil {
		return nil, err
	}
	content = append(content, []byte("\nhello\n")...)
	return content, nil
}

对于 .php 结尾的文件,在文件的尾部添加 hello。

在 ro 中添加一个文件 test.php

$cat ro/test.php
abc

在 fuse 中 cat 同一个我呢见

$cat fuse/test.php
abc

hello

如果更新了 test.php

$cat ro/test.php
new file content

在 fuse 中再去 cat 则会发现更新后的文件内容

$cat fuse/test.php
new file content

hello

就这样的效果。这样的东西可以广泛应用于源代码加工的场景(es6转javascript,css转换,源代码注入调试信息等)。可以无缝地实现自动更新。同时可以做到延迟转换,提高性能。这个代码在 taowen/lambdafs