Sign in

    webdav

    MoonBit WebDAV 客户端:文件上传/下载(流式+进度)、目录、移动/复制、PROPFIND/PROPPATCH。

    webdav
    http-client
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    10 days ago
    Downloads
    63

    #q2316367743/webdav

    CI

    MoonBit WebDAV 客户端:文件上传 / 下载(全量字节、读取流、本地文件路径,流式 + 进度 + Range 断点续传)、目录、移动 / 复制、锁、PROPFIND / PROPPATCH。基于 moonhttp 与 Milky2018/xml 构建,仅 native 后端。

    #安装

    moon add q2316367743/webdav

    #快速上手

    async fn main { let client = @webdav.create_webdav_client({ base_url: "http://127.0.0.1:8080/dav/", username: Some("user"), password: Some("pass"), timeout_ms: None, }) // 递归建目录(mkdir -p 语义:逐级 MKCOL,父级 / 本级已存在都跳过) client.mkdir_all(path="/demo/相册/2024") // 上传 · 全量字节(进度按 64 KiB 分块节奏回调;返回值带 ETag 等元数据) let put = client.put_file( path="/demo/hello.txt", data=@utf8.encode("你好,WebDAV!"), content_type="text/plain; charset=utf-8", conditions=@webdav.Conditions::new(if_none_match="*"), // 仅当不存在时创建 on_progress=fn(p) { println("上传 (\{p.loaded}/\{p.total})") }, ) println("已创建:\{put.created},ETag:\{put.etag}") // 上传 · 本地文件(内部边读边发,内容不整份读进内存) ignore( client.put_file_from_path( path="/demo/big.bin", local_path="/Users/me/big.bin", content_type="application/octet-stream", on_progress=fn(p) { println("上传 big.bin \{p.loaded}/\{p.total}") }, ), ) // 上传 · 任意读取流(@fs.File / @io.pipe() 读端 / MemoryReader 都能接; // 传 content_length 走定长,不传走 chunked) // client.put_file_stream(path="/demo/stream.bin", reader=some_reader) // 列目录 let items = client.list(path="/demo") for item in items { println("\{if item.is_dir { "[目录]" } else { "[文件]" }} \{item.name}") } // 流式下载 · 分块回调(内存消费) client.download_file( path="/demo/hello.txt", on_chunk=fn(chunk) { /* 逐块处理,如拼 Buffer / 算哈希 */ }, on_progress=fn(p) { println("下载 \{p.loaded} 字节") }, ) // 流式下载 · 直接写本地文件(写盘是 async,客户端内部完成) client.download_to_path(path="/demo/big.bin", local_path="/Users/me/big.copy") // 断点续传:中断残留的本地文件从断点继续(只拉剩余部分); // 带 ETag 发 If-Range,服务端文件已变时自动回退全文重写 client.download_to_path_resume( path="/demo/big.bin", local_path="/Users/me/big.copy", etag=put.etag.unwrap_or(""), ) // 区间下载:只取前 8 字节(服务端支持时回 206) let head = client.get_file(path="/demo/hello.txt", range=@webdav.ByteRange::from(end=7)) }

    #功能一览

    类别方法
    上传put_file(全量 Bytes)、put_file_stream(&@io.Reader,可选长度)、put_file_from_path(本地文件路径);三者都流式发送、都回 PutResult
    下载get_file(全量到内存,回 GetResult)、download_file(同步分块回调)、download_to_path(流式写本地文件);三者都支持 range? : ByteRange 区间下载(服务端接受时 206,GetResult.content_range 携带区间与总长)
    断点续传download_to_path_resume:以本地残留为起点发 Range: bytes=N-,206 追加 / 200 回退重写 / 416 且已下完视为成功;etag? 发 If-Range 防服务端文件已变,进度为绝对值,成功返回 = 文件完整
    目录mkdir(单级)、mkdir_all(逐级 mkdir -p,已存在跳过)、list(可指定 Depth)、list_recursive(整树)、stat(属性)、exists(HEAD,不支持时自动回退 PROPFIND)
    移动 / 复制move_path、copy_path(覆盖语义 + Depth)
    自定义属性set_properties / get_properties / get_properties_with_status(PROPPATCH / 按名 PROPFIND,保留 404 状态)
    能力探测options(DAV / Allow / Server → ServerCapabilities)
    锁lock(独占 / 共享写锁)、refresh_lock(续期)、unlock、get_locks(lockdiscovery 锁发现)、cleanup_lock_null(lock-null 空资源收尾);令牌传给 Conditions::new(lock_token=…) 后写操作自动带 If: (<令牌>),if_lists 支持多令牌 / Not / [etag] / 带标签列表
    协议控制所有方法的 conditions?(If-Match / If-None-Match / lock_token / if_lists)与 signal?(AbortController 取消);WebdavConfig::retry_on_423 可选开启「423 自动补令牌重试一次」

    统一错误为 WebdavError(detail() 区分网络 / XML 解析 / 非期望状态码 / 207 部分失败 / 已取消 / 本地文件六类,to_string() 为中文人读描述); 所有方法为 async ... raise WebdavError。

    #已知限制

    • mkdir 单级创建(父目录不存在得 409);递归用 mkdir_all:逐级 MKCOL, 405(已存在)跳过,409 先 PROPFIND Depth:0 复核再决定跳过或抛出, 中途失败不回滚已建层级。
    • 锁做独占与共享写锁、完整 If(多令牌 / Not / [etag] / 带标签列表)、 lock-null 清理与可选的 423 自动补令牌重试;Depth: 1 的 LOCK 按 RFC 4918 §9.10.3 本地拦 400,不做 5xx / 断线退避重试,锁注册表只是进程内缓存(不跨进程); 锁发现依赖服务端实现 lockdiscovery(hacdias/webdav 只回空元素)。
    • 路径形态的传输只覆盖单文件;目录镜像 / 同步、push 型(Writer)上传仍是路线图。
    • Range / 断点续传只做单区间(无多区间 / multipart),区间端点与进度受 Int32 上限约束(>2GB 资源不支持);If-Range 仅支持 ETag;不带 etag 的裸续传在 服务端文件被整体替换时可能拼出损坏内容。详见 docs/06-range-resume.md 的已知限制。
    • 更多缺口与 P1 / P2 路线图见 docs/03-gap-analysis.md。

    #文档

    #开发

    bash src/main/run_tests.sh # 一键测试:起 WebDAV 服务端 → moon test → 真实全流程 → 关闭并汇总 moon test # 仅单元 / 黑盒测试(Mock 传输层,不触网) moon run src/main # 单独跑真实全流程演示(环境变量见 src/main/moon.pkg 头注释) moon info && moon fmt

    CI(.github/workflows/ci.yml)在 push / PR 时跑检查 → 构建 → 单元测试 → 门禁, 并另起 job 用 run_tests.sh 跑真实服务端全流程;发布走 .github/workflows/publish.yml:改 moon.mod 的 version → 等 CI 绿 → 建 vX.Y.Z 的 Release → 自动发布到 mooncakes.io。 配置与注意事项见 docs/04-ci-cd.md。

    AbortController

    取消原语再导出:三个入口与七个快捷方法的 signal? 参数类型是 AbortSignal, 而取消之后调用方还要拿它查状态(aborted / reason),叫停则要用 AbortController。信号不是配置字段,所以这里直接从定义它的 abort 包再导出。

    AbortSignal

    取消原语再导出:三个入口与七个快捷方法的 signal? 参数类型是 AbortSignal, 而取消之后调用方还要拿它查状态(aborted / reason),叫停则要用 AbortController。信号不是配置字段,所以这里直接从定义它的 abort 包再导出。

    ActiveLock

    一条活动锁(RFC 4918 §14.11 activelock)。

    ByteRange

    HTTP 字节区间(RFC 7233):字节从 0 计,end 为含端。 只能经 ByteRange::from / ByteRange::suffix 构造,保证取值合法: 后缀形式(取资源末尾 N 字节)以负数 start = -N 存储。

    Conditions

    条件请求:If-Match / If-None-Match / If,前者取值是服务端给出的 ETag(含引号,原样透传)或通配符 *,用于乐观并发控制; lock_token 是要随请求提交的锁令牌,渲染成 If: (<令牌>) (RFC 4918 §10.4.1 的无标签列表形态);if_lists 是显式的 If 列表, 覆盖多令牌析取、Not / ETag 形态与带标签(</url> (<令牌>))的条件。

    ContentRange

    206(部分内容)响应 Content-Range 头的解析结果: bytes first-last/total,描述服务器实际返回的区间。

    Depth

    Depth 头取值(RFC 4918 §10.2):PROPFIND 的作用深度与 COPY 的 是否浅拷贝都由它表达。

    FileInfo

    PROPFIND 解析出的资源条目(文件或目录)。

    GetResult

    get_file 的结果:内容与响应元数据。

    IfCondition

    If 头里的一条条件,可带 Not 取反。

    IfConditionKind

    If 头里一条条件的作用对象(RFC 4918 §10.4.2): 状态令牌(<令牌>,锁令牌属于此类)或 ETag([etag])。

    IfList

    If 头里的一个列表:(<条件> …),可选带资源标签(</url> (<条件>))。 同一列表内的条件是合取;多个列表之间是析取(RFC 4918 §10.4.3)。

    LockResult

    LOCK(申请或刷新)的响应结果。

    LockScope

    锁作用域(RFC 4918 §14.11 lockscope):独占写锁或共享写锁。

    Progress

    传输进度快照,由上传 / 下载回调接收。

    PropResult

    属性级读取结果:status 是该属性所在 propstat 的状态码, 404 表示「服务端没有这个属性」,与「属性值为空串」区分开。

    PutResult

    put_file 的结果。

    ResourceFailure

    部分失败条目:207 Multi-Status 中非 2xx 的那个资源。

    ServerCapabilities

    OPTIONS 能力探测结果(DAV / Allow / Server 头)。

    WebdavClient

    WebDAV 客户端:全部操作以方法形式挂在本类型上。 通过 create_webdav_client 构造(根包重导出)。

    WebdavConfig

    客户端配置,字段可直接构造,未填字段取 Default(None / 超时默认值)。

    • base_url:服务器根地址,如 "http://127.0.0.1:8080/dav/", 末尾多余的 / 会被内部规范化容忍;
    • username / password:Basic Auth 凭据,均为 None 时不携带认证;
    • timeout_ms:单请求超时毫秒数,None 时取 DEFAULT_TIMEOUT_MS;
    • retry_on_423:写请求被 423(资源被锁定)拒绝、且本客户端自己持有一把 覆盖该路径的锁时,是否用这把锁的令牌自动重试一次;None / false 时保持「不自动重试」的默认行为。

    WebdavError

    所有 WebDAV 操作的统一错误(单构造器包装,MoonBit 错误类型约定)。 用 detail() 取出分类后模式匹配,to_string() 得中文人读描述。

    WebdavErrorDetail

    错误分类:可按「该怎么处理」分支。

    DEFAULT_PROP_NS

    let DEFAULT_PROP_NS : String

    自定义属性(死属性)的默认命名空间,与 @client.DEFAULT_PROP_NS 同值。

    DEFAULT_TIMEOUT_MS

    let DEFAULT_TIMEOUT_MS : Int

    默认单请求超时(毫秒),与 @types.DEFAULT_TIMEOUT_MS 同值。

    create_webdav_client

    创建 WebDAV 客户端:所有操作以方法形式挂在返回值上。

    示例:
    let client = @webdav.create_webdav_client({ base_url: "http://127.0.0.1:8080/dav/", username: Some("user"), password: Some("pass"), timeout_ms: None, })

    create_webdav_client_with_transport

    创建使用指定传输层的客户端:测试注入 MockTransport, 或自定义连接 / 代理行为。

    Source Files