最近在社区同学 @xprazak2 的帮助下为 Apache OpenDAL 增加了 IPFS 支持。
具体的来说是两个 Service:
其中,ipfs 基于 HTTP Gateway 提供了 read 和 list 支持,而 ipmfs 则基于 MFS 提供了完整的读写支持。
以访问 https://ipfs.io
为例:
let mut builder = ipfs::Builder::default();
builder.root("/ipfs/QmPpCt1aYGb9JWJRmXRUnmJtVgeFFTJGzWFYEEX7bo9zGJ");
builder.endpoint("https://ipfs.io");
let op: Operator = Operator::new(builder.build()?);
let content = op.object("normal_file").read().await?;
let meta = op.object("normal_file").metadata().await?;
通过 OpenDAL,用户可以使用统一的 API 访问存储在 IPFS 网络上的所有数据,同时这也意味着用户可以访问存储在 Filecoin,Ethereum 等去中心化项目中的数据,向实现 OpenDAL 的愿景 Access data freely, painless, and efficiently
再次前进一大步。
为什么要支持 IPFS?
OpenDAL 的愿景是 Access data freely, painless, and efficiently
,而我对 freely
的理解是可以访问任意存储服务,只要他对外暴露了公开的访问接口,包括:
- 对象存储服务:OpenDAL 不仅支持 S3 兼容接口,还努力的实现各个服务原生接口,让用户的应用不被锁定在 S3 API 上
- 文件存储服务:OpenDAL 支持了 HDFS 这样的分布式文件系统,azfile 等服务也在支持计划中
- SaaS 网盘服务:OpenDAL 有计划支持 Google Drive,Dropbox,OneDrive,iCloud 这样面向用户的 SaaS 网盘服务
自然,去中心化的存储服务也是 OpenDAL 支持的目标,包括但不限于 IPFS,Storj 等。
哪里有访问数据的需求,哪里就有 OpenDAL。
除了 OpenDAL 本身的愿景外,社区对 IPFS 支持也有着自己的期待。
OpenDAL 最大的用户 Databend 一直希望能够支持从 IPFS 直接读取数据:
COPY INTO mytable FROM "ipfs://QmPpCt1aYGb9JWJRmXRUnmJtVgeFFTJGzWFYEEX7bo9zGJ"
这样 Databend 就能够扩大自己的使用场景,为 Web3 的大数据分析提供更好的支持,而不需要类似 mars 这样的项目做中转。
如何支持 IPFS?
大体上来说,IPFS 支持通过三种方式来读写数据
- HTTP Gateway
- Kubo UnixFS RPC API
- Kubo MFS RPC API
HTTP Gateway
HTTP Gateway 是最简单的访问方式,只支持 GET 和 HEAD 请求,其中:
- GET 请求会返回该文件的内容
- 如果访问的目标是个目录,则会返回自动生成的 DirIndex 页面
- 作为例外,如果目录下存在
index.html
文件,则会返回对应的首页文件
- HEAD 请求与 GET 请求类似,区别是不会返回对应的内容
特别的,如果使用特定的 accept
header 请求,比如 accept: application/vnd.ipld.raw
,Gateway 会返回 raw block,开发者可以直接解析 ipfs block,而不是自动生成的 HTML 页面。
Kubo UnixFS RPC API
Kubo 之前叫做 go-ipfs
,是 IPFS 的 Go 实现。Kubo 提供了 Raw RPC API,用户可以实现:
/api/v0/add
:增加文件到 IPFS/api/v0/cat
:查看 IPFS 中的对象数据/api/v0/get
:下载 IPFS 中的文件/api/v0/ls
:列取 IPFS 中的目录
UnixFS 构建了一个全局的 immutable 文件系统,所有文件都通过 content hash 进行定位,因此无法实现删除等操作。
Kubo MFS RFC API
在 UnixFS 的基础上,Kubo 构建了一层 MFS(Mutable File System),对外暴露了一个可读写的文件系统,支持常用的文件系统操作:
/api/v0/files/ls
:列取目录/api/v0/files/mkdir
:创建文件夹/api/v0/files/read
:读取数据/api/v0/files/rm
:删除文件/api/v0/files/stat
:查看文件状态/api/v0/files/write
:写入文件
本质上是维护一个不断更新的 Root 节点,每次写入操作都会创建一个全新的 Root。
OpenDAL 的实现
@xprazak2 为 OpenDAL 提供了 IPMFS 的支持,使得 OpenDAL 可以在 ipfs daemon 的 MFS 上工作。但是这还不够,因为 MFS 是本地的 ipfs daemon 暴露出来的一层抽象,用户无法直接访问外部的数据。想要访问 IPFS 上已经存储的数据,需要使用 ipfs cp /ipfs/QmHash /path/to/dir
将对应的数据复制过来。为了能够让用户更自然的访问 IPFS 上的数据,OpenDAL 基于 IPFS HTTP Gateway 提供了一个新的服务,命名为 ipfs
。ipfs service 的实现与 http 大体是相似的,主要区别在于 ipfs 提供了列取目录的定义,OpenDAL 能够在此基础上实现 list 操作。
前面提到在 GET 的时候指定 accept: application/vnd.ipld.raw
可以获取对应的 raw block,魔法就在这里:
IPFS 存储的所有数据都通过 IPLD 进行了统一描述,其 Protobuf 定义如下:
message PBNode {
// refs to other objects
repeated PBLink Links = 2;
// opaque user data
optional bytes Data = 1;
}
message PBLink {
// binary CID (with no multibase prefix) of the target object
optional bytes Hash = 1;
// UTF-8 string name
optional string Name = 2;
// cumulative size of target object
optional uint64 Tsize = 3;
}
在 IPLD 的基础上,IPFS 定义了 UnixFS 的规范:
syntax = "proto2";
package unixfs.pb;
message Data {
enum DataType {
Raw = 0;
Directory = 1;
File = 2;
Metadata = 3;
Symlink = 4;
HAMTShard = 5;
}
required DataType Type = 1;
optional bytes Data = 2;
optional uint64 filesize = 3;
repeated uint64 blocksizes = 4;
optional uint64 hashType = 5;
optional uint64 fanout = 6;
}
message Metadata {
optional string MimeType = 1;
}
所以我们只需要获取到每个目录的 raw block 就能知道他下面包含的所有子对象。为了避免引入一大堆 IPFS,IPLD 相关的依赖,OpenDAL 自行实现了对应 protobuf 结构体的声明,通过 prost
进行了解析。
这里比较麻烦的地方在于
PBLink
内是没有类型概念的,所以 OpenDAL 无法通过PBLink
知道这个对象到底是一个文件还是一个目录,只能逐个去 HEAD 一下,由此带来了额外的开销。
未来工作
IPFS 是有多个复杂组件构建起来的大型协议,因此 OpenDAL 还有很多工作要做。
Pinning Services 支持
IPFS 的节点只会缓存自己感兴趣的数据,无用的数据会在过期后自动淘汰。如果想要让自己的数据长期保存,则需要使用 Pinning Services 将数据 pin
住,这样就能保证在自己的节点下线后仍能可以正常访问数据。
OpenDAL 计划在 ipmfs
中引入 pin 相关的配置项,在每次数据写入完成后调用 Pin API 来锁定数据,保证 OpenDAL 写入的数据在网络上持久化。
部分服务不支持返回 Raw Block 数据
OpenDAL ipfs
的 list 实现高度依赖 accept: application/vnd.ipld.raw
的行为,但是有些 HTTP Gateway 没有实现这一规范,比如 https://cloudflare-ipfs.com
:
curl -H "accept: application/vnd.ipld.raw" "https://cloudflare-ipfs.com/ipfs/bafybeiakou6e7hnx4ms2yangplzl6viapsoyo6phlee6bwrg4j2xt37m3q" | cat
即使手动指定了 accept
,cloudflare-ipfs.com
仍然会返回生成的 HTML Index。
考虑到用户体量,OpenDAL 后面计划支持解析 HTML 来获取文件列表。
部分服务对特殊字符的支持有问题
经过实际的测试,有些 Gateway 服务对特殊字符的支持不正确,对形如 special_file !@#$%^&*()_+-=;'><,?
的文件无法正确的进行 URL Unescape,由此无法通过 OpenDAL 的行为测试。这个是服务器端的 BUG,不是非常好绕过。一个可能的解决方案是 OpenDAL 去访问对应的 Hash 而不是子路径。
总结
总的来说,OpenDAL 已经成功的支持了 ipfs
和 ipmfs
两个服务,能够覆盖绝大多数 IPFS 的使用场景,欢迎大家来尝鲜和使用。同时 OpenDAL 在 Tracking issue of more underlying storage support 中记录和跟踪各个服务的访问情况,欢迎社区同学来实现或者增加新的服务~
OpenDAL: Access data freely, painless, and efficiently