# Rust 应用接入

应用使用 `cipherfs_sync::SyncedVault` 调用异步对象接口和后台同步。每台设备保存完整共享索引；默认 Full 在后台获取全部未退休正文，Partial 获取固定目录，其余按需读取并缓存。Directory/S3 存放点同时保存加密索引和正文，已连接设备也可提供正文。

## 依赖与加入仓库

项目尚未发布，从源码接入；按 checkout 位置调整路径：

```toml
[dependencies]
cipherfs-core = { path = "../cipherfs/crates/cipherfs-core" }
cipherfs-sync = { path = "../cipherfs/crates/cipherfs-sync", default-features = false, features = ["iroh-p2p"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs", "io-util"] }
```

打开 S3 兼容存放点时给 Sync 增加 `s3` feature。Core 方法包含阻塞数据库和文件 I/O，异步应用应放在 `spawn_blocking` 中；流式 façade 将 Core 工作交给阻塞任务，输入/输出留在调用者的执行器。

首台设备用 CLI `init` 或 `Vault::create_with_recovery_key` 创建，保存返回的恢复秘密和独立可信的 `InitInfo`；其他设备通过 ticket/SAS 配对或恢复加入。相同密码独立创建的两个库仍是不同仓库。

`SyncedVault::open(...).await` 打开已有成员，`SyncedVault::new(core)` 包装已有 Core。便利方法 `SyncedVault::create` 不返回恢复秘密，需要恢复设置的应用应使用 Core 创建接口。低层 `PendingDevice`/`enroll_device` 必须接收经过认证和批准的请求；`crates/cipherfs-sync/examples/synced_vault.rs` 展示真实加入和两个节点的交换。

## 打开存放点并启用后台同步

下面打开已有设备和已保存的存放点配置；示例将配置打开失败视为错误，应用也可以明确报告后重试。

```rust
use std::path::PathBuf;
use cipherfs_core::Vault;
use cipherfs_sync::{configured_stores, BackgroundSyncOptions, SyncedVault};

type AppResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;

async fn run(path: PathBuf, password: String) -> AppResult<()> {
    let (vault, stores) = tokio::task::spawn_blocking(move || -> AppResult<_> {
        let core = Vault::open(&path, &password)?;
        let (stores, failed) = configured_stores(&core)?;
        if !failed.is_empty() {
            return Err(std::io::Error::other(format!(
                "{} configured stores could not be opened", failed.len()
            )).into());
        }
        Ok((SyncedVault::new(core), stores))
    }).await??;

    vault.enable_sync(BackgroundSyncOptions {
        stores,
        ..BackgroundSyncOptions::default()
    }).await?;

    let input = tokio::fs::File::open("/data/input.bin").await?;
    vault.put_from_reader("docs/input.bin", input).await?;
    let mut output = tokio::fs::File::create("/data/output.bin").await?;
    let written_bytes = vault.get_to_writer("docs/input.bin", &mut output).await?;
    println!("written_bytes={written_bytes}");

    vault.close().await?;
    Ok(())
}
```

`configured_stores` 返回 `(已打开, 失败列表)`，不能静默丢弃第二项。S3 凭据在打开时从 `AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY` 读取，不持久化。`enable_sync` 不会自动打开已保存配置，须将 `StoreTarget` 传入 `BackgroundSyncOptions.stores`。

P2P 对端须已经加入同一仓库，将它的 `NodeAddr` 放入 options 的 `peers`，或调用 `add_sync_peer`；本机 `sync_addr` 返回监听地址，同一节点也处理入站连接。`RelayConfig::no_relay()` 适合直接可达的部署/测试，默认选项使用默认中继配置。选项还包括端口、轮询与重试间隔；端口零请求系统分配。

每个对端和存放点有独立循环；写入立即唤醒同步，失败按退避重试，单独正文任务补齐 Full/固定内容。追赶中的 Full 会分别声明完整索引和尚未补齐的正文位置。写入成功只承诺本机持久化，远端复制最终一致。

## 对象方法与内存

| 方法 | 返回值与行为 |
| --- | --- |
| `put_from_reader(key, reader)` | `Result<()>`；输入为 `AsyncRead + Send + Unpin`，缓冲有界，显式 EOF 后提交 |
| `get_to_writer(key, writer)` | `Result<u64>`；向 `AsyncWrite` 流式写入认证快照，返回字节数 |
| `get_range_to_writer(key, offset_bytes, length_bytes, writer)` | `Result<u64>`；范围在 EOF 截断，仅获取相交单元 |
| `put(key, &[u8])` | 借用输入切片，转交流式方法 |
| `get(key)` / `get_range(key, offset_bytes, length_bytes)` | 返回整个请求结果的 Vec |
| `delete(key)` / `list(prefix)` | 发布墓碑 / 返回匹配字面键前缀的当前文件 Vec |
| `list_page(prefix, limit, continuation)` | 按路径排序，最多返回 limit 个匹配相同字面前缀的文件 |
| `history(key)` / `history_page(key, limit, continuation)` | 全部可达未退休版本 / 有界页，按 `(time_ms, VersionId)` 降序 |

适配器使用两槽 64KiB 通道。输出可以借用调用者状态，不要求 Send 或 Unpin；读取保持所选版本，成功时 flush。缺失单元、认证或输出错误明确失败，但 writer/HTTP 流可能已输出认证前缀，失败后必须丢弃输出或中止目标发布；缓冲型 `get`/`get_range` 返回 Err，不成功返回部分对象。需要单文件原子发布时由应用使用临时文件。

EOF 前取消上传不会发布半个文件；EOF 后已接纳的提交可能继续完成并唤醒同步。`disable_sync` 停止后台服务，`close` 关闭整个 façade 及其克隆、唤醒阻塞适配器并等待已接纳任务退出。重新打开设备前等待 `close`。

正文适配器限制传输缓冲，不代表整个应用内存固定：bytes Vec/Blob 结果为 O(对象或请求范围大小)，列表/历史/垃圾箱/冲突 Vec 为 O(返回项数)。页扫描最多保留 limit+1 个结果和一条正在解密的记录；文件 heads、路径争用者和单版本正文单元元数据另有成本。历史仍保留已访问 VersionId 与待访问父版本；完整成员/索引状态与远端来源获取/缓存窗口也各有成本。本机 512MiB 内存回归的精确范围见 `docs/IMPLEMENTATION_AUDIT.md`。

Core `Vault::list_page(directory, limit, continuation.as_ref())` 列直接子项，同名目录在文件前；`Vault::list_file_entries_page` 与异步 `SyncedVault::list_page` 匹配字面对象键前缀（去掉开头的斜杠），前缀 `docs` 也匹配 `docs2/a`。Core `Vault::file_history_page` 与异步 `SyncedVault::history_page` 保留冲突解决中的跨 FileId 父版本，退休祖先先遍历再从结果排除。

limit 必须在 `1..=MAX_PAGE_ENTRIES`（1024）。首个调用传 None，处理 `page.entries`，将 `page.continuation` 传给同一方法和范围，直到 None。Core 借用续页值，异步方法接收拥有的值；Core 与 Sync 导出的 `EntryContinuation`/`HistoryContinuation` 可克隆、序列化，不持有 Vault/数据库句柄，可跨关闭重开使用。范围绑定仓库与规范目录或字面前缀，历史还核对规范路径与选中的 FileId，路径被其他身份复用时明确失败。

每页读取当前视图，不承诺跨页快照；修改、删除、退休可改变后续结果，边界前的新插入不会出现。静止仓库拼接各页等于便利接口结果。HMAC 索引可能每页完整扫描，因此有界结果不等于扫描时间固定。C 提供 `cipherfs_vault_write_stream`/`cipherfs_vault_read_stream` 回调，Go 提供 `WriteFrom`/`ReadTo`，Python 提供 `write_from_reader`/`read_to_writer`，Web 下载使用 HTTP 流；它们的 bytes/Blob 和列表便利接口分配完整结果，目前不暴露上述 Rust 列表/历史页。

## 共享版本与明文导出

Core 提供 `conflicts`、`read_head_to_writer`、`resolve_conflict`、`move_file_id`、`file_history`、垃圾箱/恢复和退休 API。保留版本共享可见，后续写入将已知所有头作为父版本。策略默认候选年龄 365 天、可取消宽限期 30 天。Full/Partial 是存储角色，不是访问权限。

明文导出是阻塞 Core 操作，异步程序放在阻塞任务中调用：

```rust
fn export(vault: &cipherfs_core::Vault) -> cipherfs_core::Result<cipherfs_core::ExportReport> {
    vault.export_plaintext("/data/new-export", cipherfs_core::ExportOptions {
        include_history: true,
        include_conflicts: true,
    })
}
```

两个选项相互独立；旧的非头版本使用原路径，额外头使用 FileId 当前导出的路径。归档位于 `.cipherfs-history/<path>/<time_ms>-<完整设备ID>-<完整版本ID>`。报告统计完整正文文件，缺失列表是目标相对路径，归档带具体版本文件名；损坏/认证/I/O 错误中止，不归为正文缺失。mtime 和 owner 可执行位会应用，本机空目录不导出。

完整离线导出保证适用于 Full 已相对于本机接纳的索引 frontier 完成正文同步、且本机缓存完整的设备，覆盖选择中的当前及未退休历史/冲突正文，不包括本机尚未接纳的新修改。追赶中的 Full 或 Partial 离线只能导出本机已有正文，报告缺失目标路径；角色本身不能保证完整。

目标必须不存在，父目录已存在且路径没有符号链接，所选键与归档路径须能由原生文件系统无损表示。`a` 与 `a/b`、大小写/名字/路径碰撞明确失败，不自动改名。Linux/Android/Apple 实现独占的描述符锚定发布，其他平台返回 Unsupported。选中的版本及正文在并发修改/退休期间保持固定并保留，内容流式读取，选择元数据随导出规模增长。

## 设备与容量边界

恢复重设密码保留 DeviceId；设备撤销和恢复轮换创建随机新密钥纪元，不能收回此前披露的秘密或正文。恢复加入可以立即读写，设备管理需要独立已有成员批准。所有已有设备丢失时，单个恢复设备仍不能撤销、轮换恢复密钥或批准配对，直到独立成员返回。

配对/恢复冷启动证明与快照仍为上限 32MiB 的单对象，multipart 待实现。过大恢复发布以 `recovery_proof_deferred` 报告，普通索引/正文同步和 GC 可继续；首个没有确认前缀的上传也可能触及清单证明限制。长历史/百万文件规模、子树授权、移动端及 WebDAV 存放点尚不在当前验收范围。
