一个用TypeScript编写的WebDAV客户端,适用于NodeJS和浏览器
关于
WebDAV是一种广为人知、稳定且高度灵活的协议,用于通过API与远程文件系统进行交互。由于其广泛使用,许多文件托管服务如Nextcloud/ownCloud、Box和Yandex都将其作为主要接口的备选方案。
这个库提供了一个WebDAV客户端接口,使与支持WebDAV的服务交互变得简单。API返回promise并解析结果。它解析并准备目录内容请求以便于使用,同时还提供了获取文件统计信息和配额等方法。
这个库的目标不是严格遵循RFC或标准的WebDAV接口,而是提供一个易于使用的客户端API,用于在Node或浏览器中与大多数WebDAV服务进行交互。
支持的版本/环境
版本5正在积极开发中。版本4处于维护模式,将接收安全和稳定性相关的错误修复。早期版本已经废弃,不会再收到更新。
版本5将库升级为使用ESM(ECMAScript模块),因此你的环境必须符合以下格式之一才能使用这个库:
- NodeJS项目,在
package.json
中设置"type": "module"
(ESM模式) - 使用像Webpack这样能处理ESM的工具打包的Web项目
- React-native项目(通过直接导入或使用自动react-native入口)
如果你还没准备好升级,可以考虑使用这个库的版本4。
请求
这个库使用@buttercup/fetch
以跨平台的方式发起请求。如果存在,它会使用浏览器的原生fetch
,否则使用polyfill。在Node和其他环境中,它使用node-fetch
。
v5之前的版本使用Axios发起请求。
Node支持
支持表:
库主版本 | Node JS范围 |
---|---|
v5 | 14+ |
v4 | 10-18 |
v3 | 10-16 |
v2 | 6-14 |
v1 | 4-12 |
浏览器支持
从这个库的版本3开始支持浏览器环境。
如前所述,v5引入了ESM,这可能需要在为浏览器打包时进行额外配置。
尽管你可以选择自行转译这个库的默认入口点(NodeJS),但不建议这样做 - 请使用专门的web版本。
在版本4中,你必须为web版本使用不同的入口点,虽然在版本5中仍可以这样做,但你不再需要:
import { createClient } from "webdav/web";
// 或
import { createClient } from "webdav";
// 在支持的打包工具中两种方式都可以
版本3/4支持浏览器中的UMD风格模块,但在版本5中不再支持。版本5只提供一个可以导入到其他支持ESM的项目中的ESM兼容包。
**注意:**在浏览器中不支持流,所以createReadStream
和createWriteStream
只是存根。调用它们会抛出异常。
React-Native支持
自版本5.6.0
起,React-Native得到了更好的支持,使用了专门为该平台构建的版本。导入应该是自动的,但可以通过直接从/react-native
导入来强制使用:
import { createClient } from "webdav/react-native";
**注意:**由于某些原因,Metro构建系统无法正确解析这些入口,你可能需要为React Native自定义babel配置以正确导入这个库:
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
plugins: [
[
"module-resolver",
{
alias: {
// 将webdav客户端入口指向react native构建:
webdav: "webdav/dist/react-native"
},
extensions: [".tsx", ".ts", ".js", ".jsx", ".json"]
}
]
]
};
使用这个覆盖后,你可以简单地从webdav
导入。
类型
这个库为Node构建导出了Typescript类型。所有类型也可以直接从模块中导入:
import { AuthType, createClient } from "webdav";
const client = createClient("https://some-server.org", {
authType: AuthType.Digest,
username: "user",
password: "pass"
});
安装
使用npm简单地安装为依赖:
npm install webdav --save
使用
使用方法是通过调用工厂函数createClient
创建一个客户端适配器实例:
const { createClient } = require("webdav");
const client = createClient(
"https://webdav.example.com/marie123",
{
username: "marie",
password: "myS3curePa$$w0rd"
}
);
// 获取目录内容
const directoryItems = await client.getDirectoryContents("/");
// 输出一个类似这样的结构:
// [{
// filename: "/my-file.txt",
// basename: "my-file.txt",
// lastmod: "Mon, 10 Oct 2018 23:24:11 GMT",
// size: 371,
// type: "file"
// }]
认证和连接
如果没有提供authType
配置参数,WebDAV客户端会自动检测使用哪种认证方式,在AuthType.None
和AuthType.Password
之间选择。对于AuthType.Token
或AuthType.Digest
,你必须明确指定。
设置authType
将自动管理连接时的Authorization
头。
如果你不确定远程服务器是需要digest还是password认证,可以将authType
设置为AuthType.Auto
。
基本/无认证
如果服务器不需要认证,你可以不使用认证 - 只需在配置中避免传递username
和password
的值。
要使用基本认证,只需在配置中传递username
和password
。
这个库还允许通过设置httpAgent
和httpsAgent
属性来覆盖内置的HTTP和HTTPS代理。这些应该是node的http.Agent和https.Agent的实例。
OAuth令牌
要使用令牌进行认证,将令牌数据传递给token
字段并指定authType
:
createClient(
"https://address.com",
{
authType: AuthType.Token,
token: {
access_token: "2YotnFZFEjr1zCsicMWpAA",
token_type: "example",
expires_in: 3600,
refresh_token: "tGzv3JOkF0XG5Qx2TlKWIA",
example_parameter: "example_value"
}
}
);
你也可以自己提供HA1(详情见此)。这使你能够在用户登录时生成HA1并保存它,这样你就不需要保存密码本身。
createClient("https://address.com", {
authType: AuthType.Digest,
username: "someUser",
password: "",
ha1: "在此处填入您之前生成的ha1"
});
摘要认证
如果服务器需要基于摘要的认证,您可以通过 authType
配置参数启用此功能,并提供 username
和 password
:
createClient(
"https://address.com",
{
authType: AuthType.Digest,
username: "someUser",
password: "myS3curePa$$w0rd"
}
);
客户端配置
createClient
方法接受一个 WebDAV 服务 URL 和一个配置选项参数。
可用的配置选项如下:
选项 | 默认值 | 描述 |
---|---|---|
authType | null | 要使用的认证类型。如果未提供,默认会根据是否提供了 username 和 password 来尝试检测。 |
contactHref | 此 URL | 用于 LOCK 的联系 URL。 |
headers | {} | 提供给所有请求的附加头部。这里提供的头部会被方法特定的头部覆盖,包括 Authorization 。 |
httpAgent | 无 | HTTP 代理实例。仅在 Node 中可用。参见 http.Agent。 |
httpsAgent | 无 | HTTPS 代理实例。仅在 Node 中可用。参见 https.Agent。 |
password | 无 | 用于认证的密码。 |
token | 无 | 用于认证的令牌对象。 |
username | 无 | 用于认证的用户名。 |
withCredentials | 无 | 请求的凭证包含设置。 |
客户端方法
WebDAVClient
接口类型包含了 WebDAV 客户端实例的所有方法和签名。
copyFile
将文件从一个位置复制到另一个位置。
await client.copyFile(
"/images/test.jpg",
"/public/img/test.jpg"
);
(filename: string, destination: string, options?: WebDAVMethodOptions) => Promise<void>
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 源文件名。 |
destination | 是 | 目标文件名。 |
options | 否 | 方法选项。 |
createDirectory
创建新目录。
await client.createDirectory("/data/system/storage");
(path: string, options?: CreateDirectoryOptions) => Promise<void>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要创建的路径。 |
options | 否 | 创建目录选项。 |
options.recursive | 否 | 如果目录不存在,则递归创建。 |
options
扩展自 方法选项。
递归创建
递归目录创建在请求方面是昂贵的。会发出多个 stat
请求(总数等于已存在路径的深度加1)以检测路径的哪些部分已经存在,直到找到一个不存在的段 - 然后只请求 创建 方法。
例如,递归调用创建路径 /a/b/c/d/e
,其中 /a/b
已经存在,将会导致 3 个 stat
请求(针对 /a
、/a/b
和 /a/b/c
)和 3 个 createDirectory
请求(针对 /a/b/c
、/a/b/c/d
和 /a/b/c/d/e
)。
createReadStream
同步创建远程文件的可读流。
注意,虽然流会立即返回,但连接和获取文件仍在后台异步执行。在流开始接收数据之前会有一些延迟。
client
.createReadStream("/video.mp4")
.pipe(fs.createWriteStream("~/video.mp4"));
如果您只想流式传输文件的一部分,可以在选项参数中指定 range
:
client
.createReadStream(
"/video.mp4",
{ range: { start: 0, end: 1024 } }
).pipe(fs.createWriteStream("~/video.mp4"));
(filename: string, options?: CreateReadStreamOptions) => Stream.Readable
参数 | 是否必需 | 描述 |
---|---|---|
callback | 否 | 用于触发请求响应的回调。 |
filename | 是 | 要流式传输的远程文件。 |
options | 否 | 读取流选项。 |
options.range | 否 | 流范围配置。 |
options.range.start | 是 | 流的起始字节位置。 |
options.range.end | 否 | 流的结束字节位置。 |
options
扩展自 方法选项。
createWriteStream
创建针对远程文件的写入流。
注意,虽然流会立即返回,但连接和写入远程文件仍在后台异步执行。在流开始传输数据之前会有一些延迟。
fs
.createReadStream("~/Music/song.mp3")
.pipe(client.createWriteStream("/music/song.mp3"));
(filename: string, options?: CreateWriteStreamOptions, callback?: CreateWriteStreamCallback) => Stream.Writable
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要流式写入的远程文件。 |
options | 否 | 写入流选项。 |
options.overwrite | 否 | 如果远程文件已存在,是否覆盖。默认为 true 。 |
callback | 否 | 连接建立并开始流式传输后触发的回调。回调会收到请求的响应。 |
options
扩展自 方法选项。
customRequest
可以通过调用 customRequest
向附加的主机发出自定义请求。自定义请求提供了客户端内部使用的样板认证和其他请求选项。
const resp: Response = await this.client.customRequest("/alrighty.jpg", {
method: "PROPFIND",
headers: {
Accept: "text/plain",
Depth: "0"
}
});
const result: DAVResult = await parseXML(await resp.text());
const stat: FileStat = parseStat(result, "/alrighty.jpg", false);
(path: string, requestOptions: RequestOptionsCustom) => Promise<Response>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要发出自定义请求的路径。 |
requestOptions | 是 | 请求选项 - 必需的参数如 url 、method 等 - 请参考 RequestOptionsCustom 类型定义。 |
请求选项参数 不 扩展 方法选项,因为诸如 headers
之类的内容已经可以指定。
deleteFile
删除远程文件。
await client.deleteFile("/tmp.dat");
(filename: string, options?: WebDAVMethodOptions) => Promise<void>
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要删除的文件。 |
options | 否 | 方法选项。 |
exists
检查文件或目录是否存在。
if (await client.exists("/some/path") === false) {
await client.createDirectory("/some/path");
}
(path: string, options?: WebDAVMethodOptions) => Promise<boolean>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要检查的远程路径。 |
options | 否 | 方法选项。 |
getDirectoryContents
获取远程目录的内容。返回 项目统计 数组。
// 获取当前目录内容:
const contents = await client.getDirectoryContents("/");
// 获取所有内容:
const contents = await client.getDirectoryContents("/", { deep: true });
文件可以使用 glob
选项进行通配(使用 minimatch
处理)。使用通配模式时,建议获取 deep
内容:
const images = await client.getDirectoryContents("/", { deep: true, glob: "/**/*.{png,jpg,gif}" });
(path: string, options?: GetDirectoryContentsOptions) => Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要获取内容的路径。 |
options | 否 | 配置选项。 |
options.deep | 否 | 获取深层结果(递归)。默认为 false 。 |
options.details | 否 | 获取详细结果(项目统计信息,头部)。默认为 false 。 |
options.glob | 否 | 用于匹配文件名的通配符字符串。默认不设置。 |
options
扩展自 方法选项。
getFileContents
获取远程文件的内容。默认返回二进制内容(Buffer
):
const buff: Buffer = await client.getFileContents("/package.zip");
如果传输的文件较大,建议使用流。
也可以获取文本文件:
const str: string = await client.getFileContents("/config.json", { format: "text" });
(filename: string, options?: GetFileContentsOptions) => Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>>
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要获取内容的文件。 |
options | 否 | 配置选项。 |
options.details | 否 | 获取详细结果(附加头部)。默认为 false 。 |
options.format | 否 | 是获取二进制("binary")数据还是文本("text")。默认为 "binary"。 |
options
扩展自 方法选项。
getFileDownloadLink
生成可下载文件的公共链接。此方法是同步的。在 URL 中暴露身份验证详细信息。
并非所有服务器都支持此功能。只有基本身份验证和未经身份验证的连接支持此方法。
const downloadLink: string = client.getFileDownloadLink("/image.png");
(filename: string) => string
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要生成下载链接的远程文件。 |
getFileUploadLink
生成文件上传的 URL。此方法是同步的。在 URL 中暴露身份验证详细信息。
const uploadLink: string = client.getFileUploadLink("/image.png");
(filename: string) => string
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要生成上传链接的远程文件。 |
getQuota
获取当前账户的配额信息:
const quota: DiskQuota = await client.getQuota();
// {
// "used": 1938743,
// "available": "unlimited"
// }
(options?: GetQuotaOptions) => Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>>
参数 | 是否必需 | 描述 |
---|---|---|
options | 否 | 配置选项。 |
options.details | 否 | 返回详细结果(头部等)。默认为 false 。 |
options.path | 否 | 用于发出配额请求的路径。 |
options
扩展自 方法选项。
lock
锁定远程资源(使用写入锁)。
const lock = await client.lock("/file.doc");
// 稍后
await client.unlock("/file.doc", lock.token);
(path: string, options?: LockOptions) => Promise<LockResponse>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要锁定的路径。 |
options | 否 | 配置选项。 |
options.timeout | 否 | WebDAV 锁请求超时。参见 WebDAV Timeout 头部文档。 |
options.refreshToken | 否 | 应刷新的先前有效锁定令牌。 |
options
扩展自 方法选项。
moveFile
将文件移动到另一个位置。
await client.moveFile("/file1.png", "/file2.png");
(filename: string, destinationFilename: string, options?: WebDAVMethodOptions) => Promise<void>
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要移动的文件。 |
destinationFilename | 是 | 目标文件名。 |
options | 否 | 方法选项。 |
putFileContents
将数据写入远程文件。当文件未写入时返回 false
(例如 { overwrite: false }
且文件已存在),否则返回 true
。
// 写入缓冲区:
await client.putFileContents("/my/file.jpg", imageBuffer, { overwrite: false });
// 写入文本文件:
await client.putFileContents("/my/file.txt", str);
(filename: string, data: string | BufferLike | Stream.Readable, options?: PutFileContentsOptions) => Promise<boolean>
参数 | 是否必需 | 描述 |
---|---|---|
filename | 是 | 要写入的文件。 |
data | 是 | 要写入的数据。可以是字符串、缓冲区或可读流。 |
options | 否 | 配置选项。 |
options.contentLength | 否 | 数据内容长度覆盖。可以是布尔值(true (默认)= 计算,false = 不设置)或表示文件确切字节长度的数字。 |
options.overwrite | 否 | 如果远程文件存在,是否覆盖。默认为 true 。 |
options
扩展自 方法选项。
partialUpdateFileContents
使用部分更新来更新远程文件。此方法适用于更新文件而无需下载和重新上传整个文件。
请注意,此方法未标准化,可能并非所有服务器都支持。 要使用此功能,必须满足以下条件之一:
- WebDav 由 Apache 提供服务,并使用 mod_dav 模块
- 服务器支持 Sabredav PartialUpdate 扩展 (https://sabre.io/dav/http-patch/)
(filePath: string, start: number, end: number, data: string | BufferLike | Stream.Readable, options?: WebDAVMethodOptions)=> Promise<void>
参数 | 是否必需 | 描述 |
---|---|---|
filePath | 是 | 要更新的文件。 |
start | 是 | 起始字节位置。(包含) |
end | 是 | 结束字节位置。(包含) |
data | 是 | 要写入的数据。可以是字符串、缓冲区或可读流。 |
options | 否 | 配置选项。 |
search
根据 rfc5323 执行 WebDAV 搜索。
const searchRequest = `
<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:f="http://example.com/foo">
<f:natural-language-query>
查找上周更改的文件
</f:natural-language-query>
</d:searchrequest>
`
const result: SearchResult = await client.search("/some-collection", { data: searchRequest });
(path: string, options?: SearchOptions) => Promise<SearchResult | ResponseDataDetailed<SearchResult>>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 执行搜索的远程路径。 |
options | 否 | 配置选项。 |
options.details | 否 | 返回详细结果(头部等)。默认为 false 。 |
options 继承自方法选项。 |
stat
获取文件或目录的 stat 对象。返回一个项目统计信息。
const stat: FileStat = await client.stat("/some/file.tar.gz");
(path: string, options?: StatOptions) => Promise<FileStat | ResponseDataDetailed<FileStat>>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要获取统计信息的远程路径。 |
options | 否 | 配置选项。 |
options.details | 否 | 返回详细结果(包括头部等)。默认为 false 。 |
options
继承自方法选项。
unlock
使用令牌解锁被锁定的资源。
await client.unlock("/file.doc", lock.token);
(path: string, token:string, options?: WebDAVMethodOptions) => Promise<void>
参数 | 是否必需 | 描述 |
---|---|---|
path | 是 | 要解锁的远程路径。 |
token | 是 | 之前锁定请求中获得的令牌字符串。 |
options | 否 | 配置选项。 |
options
继承自方法选项。
自定义属性
对于像 stat
这样在底层使用 PROPFIND
方法的请求,可以为该方法提供自定义请求体,以便服务器可能响应额外或不同的数据。可以通过在方法选项中设置 data
属性来覆盖请求体。
方法选项
大多数 WebDAV 方法都继承自 WebDAVMethodOptions
,它允许设置自定义头部等内容。
选项 | 是否必需 | 描述 |
---|---|---|
data | 否 | 可选的请求体/数据值。这会覆盖原始请求体(如果适用)。 |
headers | 否 | 可选的头部对象,应用于请求。这些头部会覆盖所有其他头部,请谨慎使用。 |
signal | 否 | AbortSignal 实例,用于中止请求。 |
常见数据结构
项目统计信息
项目统计信息是描述文件或目录的对象。它们类似于以下结构:
{
"filename": "/test",
"basename": "test",
"lastmod": "Tue, 05 Apr 2016 14:39:18 GMT",
"size": 0,
"type": "directory",
"etag": null
}
或:
{
"filename": "/image.jpg",
"basename": "image.jpg",
"lastmod": "Sun, 13 Mar 2016 04:23:32 GMT",
"size": 42497,
"type": "file",
"mime": "image/jpeg",
"etag": "33a728c7f288ede1fecc90ac6a10e062"
}
属性:
属性名 | 类型 | 出现情况 | 描述 |
---|---|---|---|
filename | 字符串 | 始终 | 远程项目的文件路径 |
basename | 字符串 | 始终 | 远程项目的基本文件名,不包含路径 |
lastmod | 字符串 | 始终 | 项目的最后修改日期 |
size | 数字 | 始终 | 文件大小 - 目录为 0 |
type | 字符串 | 始终 | 项目类型 - "file" 或 "directory" |
mime | 字符串 | 仅文件 | MIME 类型 - 仅适用于文件项目 |
etag | 字符串 / null | 支持时 | 文件的 ETag |
props | 对象 | details: true | 包含服务器返回的所有项目属性的 Props 对象 |
详细响应
返回结果的请求,如 getDirectoryContents
、getFileContents
、getQuota
、search
和 stat
,可以配置为返回更详细的信息,例如响应头部。在它们的选项参数中传递 { details: true }
以接收如下对象:
属性 | 类型 | 描述 |
---|---|---|
data | * | 该过程返回的数据。类型与不使用 { details: true } 调用时返回的类型相同 |
headers | 对象 | 响应头部。 |
status | 数字 | 数字状态码。 |
statusText | 字符串 | 状态文本。 |
CORS
CORS 是浏览器采用的一种安全执行技术,用于确保请求在预期的上下文中执行和接收。如果目标服务器在从浏览器发出请求时不返回 CORS 头部,它可能会与此库冲突。处理这个问题是你的责任。
众所周知,Nextcloud 服务器默认不返回友好的 CORS 头部,这使得在浏览器环境中使用此库变得不可能。当然,你可以强制添加 CORS 头部(Apache 或 Nginx 配置),但请自行承担风险。