纯 JS 的 Tarball API,无需 Node.js

在我的前一篇文章《在浏览器中用 JS 管理文件》中,我演示了我们可以通过 JS 在浏览器中访问文件系统,并且浏览器自身也存在域私有文件系统。今天,我将更深入地探索 JavaScript 的能力,展示它自身具备处理 tarball 文件的能力,而我们也不需要一个服务器运行时来实现。

然而,我也不会从非常底层的地方开始,因为处理 tarball 文件在底层是一件非常复杂的事。我已经创建了一个 Tarball API 的抽象,它在 JsExt 扩展库中,我们可以通过它来很容易地操作 tarball 文件。

这个 Tarball API 被设计为通用的,它并不是一个将文件存档为 .tar 文件的工具,或者一个从 .tar 文件中解压文件的工具。相反,一个 Tarball 实例表示一个 tarball 存档自身,我们可以往它里面增加新文件,移除旧文件,查看它包含哪些文件,和许多 Linux 系统中的存档管理器类似。

Tarball API 并不依赖任何服务器运行时所特有的 API,它只依赖现代的 Web API,主要是 ReadableStream API,所有的现代浏览器以及服务器运行时如 Node.js,Deno 和 Bun,以及一些边缘运行时,如 Cloudflare Workers 中都是可用的。因此,这个 API 可以被用于任何 JavaScript 环境中。

导入 Tarball API

要导入 Tarball API,我们可以使用下面的方式,取决于我们应用程序中的设置。

// In Node.js, Deno (JSR), Bun, Cloudflare Workers, Browsers (with bundler or import map)
import { Tarball } from "@ayonli/jsext/archive";

// Or in Deno with URL import
import { Tarball } from "https://ayonli.github.io/jsext/archive.ts";

// Or in the browser with URL import
import { Tarball } from "https://ayonli.github.io/jsext/esm/archive.js";
TypeScript

创建一个 Tarball 实例

有两种方法可以用来创建 Tarball 实例,一个是使用 new 关键字初始化一个空 tarball,另一种是通过 Tarball.load 方法来载入一个现有的 .tar 文件。

初始化一个空 tarball

const tarball = new Tarball();
TypeScript

载入一个 tar 文件

Tarball.load 方法接受一个 ReadableStream 实例,而不是一个文件地址或者 URL,这意味着它可以从任何地方载入 tar 文件。例如,从文件系统中:

// Load from the file system (even the browser's OPFS)
import { createReadableStream } from "@ayonli/jsext/fs";

const input = createReadableStream("/file/to/archive.tar");
const tarball = await Tarball.load(stream);
TypeScript

或者从一个 HTTP 响应中:

// From the response of fetch
const res = await fetch("https://example.com/file/to/archive.tar");

const tarball = await Tarball.load(res.body!);
TypeScript

我们也可以载入一个压缩的 .tar.gz 文件:

const res = await fetch("https://example.com/file/to/archive.tar.gz");

const tarball = await Tarball.load(res.body!, { gzip: true });
TypeScript

往 Tarball 中添加新文件

现在我们已经有一个 Tarball 实例,我们可以向它添加新的文件,即时这个实例是从一个已有文件的 tar 文件载入的。

const file = new File(["Hello, World!"], "hello.txt", { type: "text/plain" });

tarball.append(file);
TypeScript

我们也可以将文件夹或者一个包含文件夹路径的文件增加到 tarball 中。并且当我们增加包含文件夹路径的文件而文件夹不存在于 tarball 中时,这个文件夹将会被自动创建。例如,下面的代码将会追加一个 foo/bar.txt 到 tarball 中,并且会自动在追加文件前新增 foo 文件夹。

const file = new Blob(["This is some content"], { type: "text/plain" });

tarball.append(file, { relativePath: "foo/bar.txt" });
// Now the tarball will have both the `foo` directory and the `bar.txt` file in the directory.
TypeScript

从 tarball 中取回文件

我们也可以从 tarball 中取回之前存入的文件,例如:

import { readAsText } from "@ayonli/jsext/reader";

const entry = tarball.retrieve("hello.txt")!;
const content = await readAsText(entry.stream);

console.log(content); // Hello, World!
TypeScript

列出 tarball 中所有的条目

有两种方式可以用来显示 tarball 中的条目,一个是遍历 tarball 实例本身,而另一个则是使用 treeView 方法创建一个树状试图,我们可以浏览所有的条目以及它们的子条目,并以目录等级来排序。

遍历 tarball

for (const entry of tarball) {
    if (entry.kind === "directory")
        console.log(`Directory: ${entry.name}; Path: ${entry.relativePath}`);
    } else {
        console.log(`File: ${entry.name}; Path: ${entry.relativePath}; Size: ${entry.size}; Last-Modified: ${entry.mtime}`);
    }
}
TypeScript

以树状试图展示 tarball 内容

const tree = tarball.treeView();
console.log(tree);
TypeScript

将 tarball 保存为文件

和初始化过程类似,Tarball API 并不提供受限于文件系统的方法来保存为文件,而是,它提供了一个 stream 方法返回一个 ReadableStream,我们可以用管道方法将它存储到文件中:

// Save the tarball to the file system (even the browser's OPFS)
import { createWritableStream } from "@ayonli/jsext/fs";

const output = createWritableStream("/file/to/archive.tar");

await tarball.stream().pipeTo(output);
TypeScript

或者我们也可以将 tarball 上传到一个 URL 中:

const res = await fetch("https://example.com/path/to/archive.tar", {
    method: "PUT",
    body: tarball.stream(),
    headers: {
        "Content-Type": "application/x-tar",
    },
});
TypeScript

另外,我们也可以在保存之前压缩 tarball:

const res = await fetch("https://example.com/path/to/archive.tar.gz", {
    method: "PUT",
    body: tarball.stream({ gzip: true }),
    headers: {
        "Content-Type": "application/gzip",
    },
});
TypeScript

写在最后

Tarball API 也提供了其他方法,例如 remove 方法用于将文件从 tarball 中移除,replace 方法用于替换 tarball 中的条目。

Tarball 类之外,还有一个 tar 函数和一个 untar 函数,它们简化了在文件系统中处理 .tar 文件的过程,并且反映了 Unix/Linux 系统中 tar -ctar -x 命令的行为,在特定的场景中,它们显得更加有用。

如果你感兴趣,可以访问 JsExt 库的 GitHub 页面,这个库旨在成为 JavaScript 语言的扩展,它使用现代 Web 标准编写并能工作于几乎任何运行时中。谁知道呢,可能它其中的一些模块正好符合你的特殊需求。

它所突出的一些特性包括:

  • 各种用于处理内置数据类型但并不内置的函数
  • 各种用于扩展流程控制能力的函数
  • 多线程的 JavaScript,使用并行线程
  • 统一文件系统 API,同时适用于服务器与浏览器环境
  • 在 CLI 和 Web 应用中打开对话框
  • 使用相同的方式处理文件路径和 URL
  • 轻松地处理字节数组和可读流
  • 在任何运行时中创建、解压和预览归档文件
  • 以及更多其他特性…

Leave a comment