Memories / 文档

本页目录

云存储与同步设计

状态:截至 2026-09-17,步骤 1-4 已实现;步骤 5 已有可复现的基线。持久化 outbox/inbox 回执、脏标记、进程本地同步会话,以及带 MemoryTransport 故障注入的提供方无关运行器已在 crates/core/src/sync.rs 中实现。OneDrive/Google Drive 文件适配器、令牌刷新和 CLI cloud sync 已实现,并通过本地测试夹具测试。真实提供方账户尚未验证。移动 AppAuth 登录和前台同步控制已实现;原生同意需要平台注册。当前授权范围参见 cloud-authorization.md。本文在 OneDrive/Google Drive 请求基础上增加了要求:并发设备不能损坏或锁住彼此的数据库文件。

决策

保留 SQLite 作为设备本地事务存储。增加提供方无关的同步协议,通过云 API 交换不可变、经过认证的加密对象。每台设备拥有自己的数据库、身份、密码和本地写入者。云存储是不可信的对象传输,不是挂载的数据库。

绝不要将正在使用的 memories.db、journal、WAL、SHM 或锁文件放入云管理的同步目录。不要通过远程文件系统打开数据库。保留默认 OS 应用数据位置。检测/拒绝已知 OneDrive/Drive 根目录是未来的配置防护,目前未强制执行;无法可靠检测任意同步客户端和挂载点。

flowchart LR
    A[Device A local SQLite] --> QA[Durable outgoing queue]
    QA --> C[OneDrive or Google Drive encrypted objects]
    C --> VB[Download and verify]
    VB --> B[Device B local SQLite]
    B --> QB[Durable outgoing queue]
    QB --> C
    C --> VA[Download and verify]
    VA --> A

现有行为与限制

  • crates/core/src/store.rs:OS 独占锁拒绝第二个本地存储拥有者。SQLite 使用 journal_mode=DELETE、synchronous=FULL,并以事务更新加密状态和签名检查点。这保护遵守协作约定的本地进程;它不是独立文件副本之间的分布式锁。
  • 每次保存都序列化并加密完整的 State;解锁检查完整检查点历史。较大的库会增加写入成本和启动时间。这是扩展性问题,不是必须替换 SQLite 本身的证据。本次评估未进行工作负载基准测试。
  • 数据库初始化在同一目录构建私有暂存数据库时,持有目标的 OS 锁。它提交初始加密状态和检查点、关闭 SQLite、同步文件,并在不替换已有目标的情况下发布。发布前崩溃会让最终路径保持可重试状态。崩溃后可能遗留废弃的 .memories-init-* 文件;它们绝不会作为正在使用的数据库加载,也不会自动删除。
  • Service 在克隆状态上处理变更,并在接受之前提交。Vault::merge 在替换当前保险库之前验证候选状态。签名修订历史已保留并发编辑和墓碑。
  • 桌面 IPC 最多接受 16 个并发请求;状态分派持有服务互斥锁。移动端使用独立服务锁和短暂注册表查找。直接在这些临界区内发起云 HTTP 请求会延迟其他操作,包括锁定。这是阻塞,不是已证明的循环死锁。云工作线程必须在网络 I/O 之前释放服务锁;桌面请求处理现在使用有界并发连接。
  • 现有同步导出是完整保险库、面向接收方加密的信封。刚完成云认证的设备仍需设备审批才能解密或导入保险库。云登录不能取代密码学设备配对。

数据库替代方案

选项 对此仓库的适用性 决策
本地 SQLite 加不可变云对象 复用事务、加密、审计和冲突处理;不需要服务器 推荐
redb 或 LMDB 作为本地存储 嵌入式事务存储可行,但更换引擎不能提供跨设备合并或云锁定 尚无迁移依据
同步文件夹中的单个共享数据库或加密归档 独立文件上传可能覆盖版本;附属锁和日志没有协调 拒绝用于主动同步
PostgreSQL 加专用同步 API 集中处理服务器并发,但需要运维服务和独立的离线/加密协议 仅在产品需求包含服务器时考虑
Automerge 或 Yjs 等 CRDT 库 可能适合未来协作文本编辑;不能替代持久存储或签名权限检查 不用于凭据或策略变更

将 SQLite 切换到 WAL 不是云同步方案。经过测量后,WAL 可能有助于本地读取并发,但要求同主机协调,且不能消除单一本地写入者的需要。

云对象契约

从现有完整保险库信封格式开始;避免同时改变存储、密码学和同步粒度。只有在不可变信封路径得到验证后,才添加增量加密修订/分块。

  1. 每个信封只生成一次,并在上传之前将其精确字节持久化到本地 outbox。使用完整密文对象的 SHA-256 作为逻辑 ID。重试复用这些字节;每次重试重新加密会创建新对象。
  2. 对象和发布描述符是不可变的。不存在共享可写 latest.json、共享锁文件、全局云序列或数据库覆盖。未来任何可变头/缓存都只是提示,可通过列出对象恢复。
  3. 先发布数据对象,再发布不可变签名描述符,绑定格式版本、不透明保险库引用、发送方、接收方、对象哈希与长度以及逐设备前驱/序列。文件名使用密文标识符,不使用明文标题、账户名称或密码哈希。
  4. 由于云列表和交付可能延迟或乱序,描述符可能先于依赖项可见。在每个对象都存在且验证通过之前保持待处理。缺失依赖时,绝不部分导入,也不推进已应用状态。
  5. 提交之前验证长度、密文哈希、签名、接收方、保险库范围、当前授权和现有历史不变量。提供方 ETag 和哈希是传输元数据,不是密码学权威。
  6. 重放对象和重复提供方文件无害:按已验证的逻辑对象 ID 去重,不按文件名。缺失云文件绝不意味着应用级删除;删除是签名墓碑。

云提供方可以删除或扣留对象。本地已知头/检查点可以检测部分回滚,但没有可信较新检查点的新设备无法证明它具有最新状态。此设计提供完整性和明确冲突,不保证面对恶意提供方时的可用性或新鲜度。

事务与工作线程生命周期

本地写入者拥有应用状态、检查点更新、outbox/inbox 状态和同步确认。在网络活动前后使用两个短暂临界区:

  1. 本地准备:授权、捕获一致的导出,并提交精确加密上传作业和代次/会话绑定。用户编辑及其脏/导出标记必须原子提交,确保崩溃不会丢失工作。
  2. 不持锁传输:独立工作线程在不持有服务互斥锁或数据库写事务的情况下执行 OAuth 刷新、上传、下载、分页和重试。限制并发、响应大小、重试以及连接/读取截止时间。遵守提供方限流和 Retry-After。
  3. 本地提交:重新获取写入者,验证授权和会话代次,合并到当前状态而不是旧的捕获副本,然后一起提交状态、检查点、已应用 ID 和 inbox 状态。只有发现的工作已持久化,提供方列表游标才可以推进。

锁定/退出登录取消作业并使迟到结果失效。在失效会话中,任何新解密/导入都不能完成。取消无法撤回提供方已经接受的密文上传。保留未完成作业以便恢复;上传成功意味着云持久化,不意味着另一台设备已确认。

OAuth 令牌始终加密并保留在设备本地,从云对象、设备审批包、导出备份、搜索和 MCP 中排除。刷新令牌轮换必须在后续操作使用新令牌之前持久化;并发刷新按账户串行化,HTTP 交换期间不持有服务锁。绝不要为工作线程添加公开令牌读取操作。

提供方特定行为

提供方 应用存储 上传/重试要求
OneDrive Microsoft Graph 应用文件夹,Files.ReadWrite.AppFolder 使用不可变对象名称和上传会话冲突行为 fail;遇到已有对象时下载/验证它,而不是替换。仅在端点文档说明支持的情况下使用条件请求。
Google Drive appDataFolder,drive.appdata 范围 预生成文件 ID 并随作业持久化,以安全重试结果不明确的创建;不要将文件名视为唯一。冲突后验证已有 ID/内容。

两个适配器都必须实现分页、可恢复传输、有界下载、响应丢失恢复和提供方特定错误处理。不要假设 Drive 支持文件系统重命名/锁定,或与 Graph 相同的前置条件语义。重定向下载 URL 和可恢复上传 URL 需要明确凭据转发规则,确保 OAuth bearer 令牌不会发送到任意主机。

初始应用文件夹设计服务于连接同一提供方账户和应用的用户配对设备。Google 应用数据存储不支持跨账户共享文件。跨用户保险库共享需要明确的传输/共享设计;不能静默扩大 OAuth 范围。若同时启用 OneDrive 和 Google Drive,它们是独立副本,不是相互同步的文件夹。分别跟踪待处理和成功传输。

真实账户验证之前需要 OAuth 客户端注册和提供方同意配置。CLI 授权应使用带 PKCE、state 验证和回环回调的浏览器 OAuth。移动端需要其支持的原生授权流程和生命周期处理;不能原样将桌面回调假设复制到移动端。

冲突、恢复与保留

  • 保留两个并发凭据/卡片修订。使用之前要求明确选择或解决;不要选择最新墙上时钟时间戳,也不要合并密码字符串。复用现有策略分叉和旧纪元隔离规则。
  • 校验和/签名失败会拒绝传入对象并保留先前本地状态。状态中应区分损坏输入、缺失依赖、登录过期、配额和内容冲突,而不是一律报告为锁定错误。字节与哈希不匹配的响应保持可重试,并使用退避。通过哈希验证但无效的对象有独立拒绝回执;绝不算作已成功应用的依赖。拒绝会阻止报告同步成功,并在云状态中按提供方持续可见。断开提供方会清除其拒绝回执,使后续连接可以重新扫描。曾混合拒绝和成功导入的旧回执会一次性清除;现有签名数据包回执仍防止重复导入。
  • 目录发现具有 4096 条 inbox 预算,另加 16 个预留依赖槽位。已验证描述符按哈希直接调度缺失对象,优先于普通下载。压力下可丢弃未被引用的信封缓冲区,在描述符到达时再次获取。部分目录页绝不将游标推进到未接纳 ID 之后。
  • 每次状态提交都移除当前保险库访问已撤销的接收方的排队导出。工作线程在对象操作之间验证会话,包括提供方 ID 分配之后。Google 文件 ID 在首次上传之前保存在加密 outbox 中,因此确认丢失和重启会复用相同的不可变提供方对象。
  • 初期保留旧云对象。安全垃圾回收需要逐设备确认、离线设备策略、撤销处理和恢复保留策略。仅列表缺失和经过时间不够。
  • 保留独立加密备份。多设备同步也会分发有效删除,不能替代时间点恢复。
  • 在架构迁移之前添加考虑恢复的迁移路径:本地锁定、保留已验证备份、事务迁移、验证,然后启用。迁移失败必须让先前存储仍可使用。

实施顺序

  1. 已完成。有界写入者请求(CloudSyncEnqueue / CloudSyncWork / CloudSyncAck)和加密 State 内的持久化 outbox/inbox 回执。SQLite 加密、journal_mode=DELETE 和完整状态保存不变。没有 WAL 或数据库引擎迁移。
  2. 已完成,仅本地传输。提供方无关的不可变对象运行器(sync::execute),带 MemoryTransport 故障注入。测试覆盖精确字节重试、提交后重新打开、重复 put 验证、描述符先于数据、损坏对象、锁定/退出登录失效,以及两台配对设备。此测试传输不使用真实云账户。
  3. 已完成,仅本地测试夹具。令牌刷新、OneDrive/Google Drive 不可变对象适配器、守护进程 CloudSyncRun(HTTP 在写入者锁之外)和 memories cloud sync。真实 Microsoft/Google 账户未授权。
  4. 已实现,真实原生同意待完成。移动登录使用 AppAuth;前台同步共享核心运行器。进入后台锁定所有句柄并取消迟到结果。组件和原生 FFI 回归覆盖取消;真实提供方/真实设备同意仍是发布门槛。刻意未启用后台调度。
  5. 基线可用。一次性 release 基准测试的库大小、保存/解锁/搜索延迟和同步流量参见性能。如有需要,将本地完整状态 blob 迁移为带事务回执的加密逐保险库/修订/密钥行,再引入增量云对象。保留签名 ID、修订历史、密文隐私和备份恢复。

验证证据与剩余门槛

在 Windows 上,crates/core/tests/storage_resilience.rs 测试一次性存储和子进程。本次评估中三个场景通过:

  • 当另一个进程拥有存储时,独立进程被拒绝;终止拥有者释放 OS 锁,原始加密状态可重新打开。
  • 子进程启动 SQLite 写事务,强制将脏页写入回滚日志,并在提交之前被终止。Store::unlock 验证原始状态/检查点,PRAGMA integrity_check 返回 ok。
  • 两台授权设备独立编辑同一资产。两种合并顺序收敛,重放幂等,两个头均保留,缺失父修订的传入快照被拒绝且不改变良好状态。

刻意忽略的辅助测试由父测试明确启动。运行:

cargo test -p memories-core --test storage_resilience --locked -j 1 -- --test-threads=1
cargo test -p memories-core --lib store::tests --locked -j 1 -- --test-threads=1

store.rs 中的初始化回归在暂存后、发布前终止子进程,然后重新创建/解锁最终存储。它们还验证目标锁始终持有,已有或新出现的目标绝不被覆盖。OAuth 测试覆盖锁定后迟到结果失效。

这些是本地进程崩溃和模型测试,不是针对实际断电、磁盘/控制器故障、云客户端文件替换或真实提供方行为的证明。磁盘满故障、大型库、发布时断电以及云重试/发布边界仍需要测试。本次评估未授权或写入任何真实云账户。

参考资料

官方文档核查日期为 2026-09-09: