最近在社区同学 @xprazak2 的帮助下为 Apache OpenDAL 增加了 IPFS 支持。

具体的来说是两个 Service:

  • ipfs: IPFS HTTP Gateway 支持
  • ipmfs: IPFS Mutable File System 支持

其中,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 支持的目标,包括但不限于 IPFSStorj 等。

哪里有访问数据的需求,哪里就有 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

即使手动指定了 acceptcloudflare-ipfs.com 仍然会返回生成的 HTML Index。

考虑到用户体量,OpenDAL 后面计划支持解析 HTML 来获取文件列表。

部分服务对特殊字符的支持有问题

经过实际的测试,有些 Gateway 服务对特殊字符的支持不正确,对形如 special_file !@#$%^&*()_+-=;'><,? 的文件无法正确的进行 URL Unescape,由此无法通过 OpenDAL 的行为测试。这个是服务器端的 BUG,不是非常好绕过。一个可能的解决方案是 OpenDAL 去访问对应的 Hash 而不是子路径。

总结

总的来说,OpenDAL 已经成功的支持了 ipfsipmfs 两个服务,能够覆盖绝大多数 IPFS 的使用场景,欢迎大家来尝鲜和使用。同时 OpenDAL 在 Tracking issue of more underlying storage support 中记录和跟踪各个服务的访问情况,欢迎社区同学来实现或者增加新的服务~

OpenDAL: Access data freely, painless, and efficiently