Spacedrive 文件同步数据库 Schema 设计:SyncConduit / SyncGeneration 实体与 SeaORM 迁移全解析
【免费下载链接】spacedriveSpacedrive is an open source cross-platform file explorer, powered by a virtual distributed filesystem written in Rust.项目地址: https://gitcode.com/gh_mirrors/sp/spacedrive
本文基于 Spacedrive 仓库中 FSYNC-002 任务文档(.tasks/core/FSYNC-002-database-schema.md)及其落盘实现,系统讲解文件同步(File Sync)功能持久化层的数据模型设计:sync_conduit与sync_generation两张表如何承载"目录对目录"的同步关系与每一次同步执行的完整历史,以及它们如何通过 SeaORM 迁移、实体、外键与索引落地为可查询的 SQLite 结构。读完本文,你将掌握这套 Schema 的字段语义、约束设计、验证状态机(Trust Watcher)与迁移验证方法,并能直接对照仓库源码理解文件同步系统的可恢复性、冲突检测与历史追踪是如何从数据层获得支撑的。
一、设计背景:为什么文件同步需要持久化存储
在 Spacedrive 的文件同步功能(FSYNC 系列任务,根任务见 .tasks/core/FSYNC-000-file-sync-system.md)中,同步不再是一次性的内存操作,而是需要满足三个长期目标:
- 可恢复同步(Resumable Sync):同步中断后能依据持久化状态继续执行,而不是重新全量扫描;
- 冲突检测(Conflict Detection):双向同步时能判断"两侧自上次同步以来是否都修改过同一文件";
- 同步历史追踪(Sync History Tracking):记录每一次同步执行的时间、结果、统计与验证状态,用于排查问题与审计。
FSYNC-002 正是为此构建持久化层,其核心交付物是SyncConduit与SyncGeneration两个实体,外加对应的 SeaORM 迁移。目标非常明确:"Persistent storage enabling resumable sync, conflict detection, and sync history tracking."(持久化存储,使同步可恢复、可检测冲突、可追踪历史。)
从源码结构看,这套持久化层实际落在core/src/infra/db/entities/与core/src/infra/db/migration/目录中,并已由下游服务core/src/service/file_sync/(FileSyncService、ConduitManager、SyncResolver)消费,说明该设计已从任务文档转化为真实可运行的实现。
二、SyncConduit 实体:一条"目录到目录"的同步关系
SyncConduit(同步管道)代表两个目录之间的一条持久化同步关系。它在仓库中的实际实现位于 core/src/infra/db/entities/sync_conduit.rs,通过 SeaORM 的DeriveEntityModel派生宏声明,table_name = "sync_conduit",约 130 行。完整字段如下:
#[sea_orm(table_name = "sync_conduit")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, #[sea_orm(unique)] pub uuid: Uuid, // 全局唯一标识 pub source_entry_id: i32, // 源目录 entry ID pub target_entry_id: i32, // 目标目录 entry ID pub sync_mode: String, // "mirror" | "bidirectional" | "selective" pub enabled: bool, // 是否启用 pub schedule: String, // "instant" | "interval:5m" | "manual" pub use_index_rules: bool, // 是否应用索引规则过滤 pub index_mode_override: Option<String>, // 可选覆盖索引模式(如 "shallow"/"deep") pub parallel_transfers: i32, // 并行传输数 pub bandwidth_limit_mbps: Option<i32>, // 可选带宽上限(MB/s) pub last_sync_completed_at: Option<DateTime<Utc>>, // 上次成功同步时间 pub sync_generation: i64, // 当前代际号,每次同步 +1 pub last_sync_error: Option<String>, // 最近一次错误信息 pub total_syncs: i64, // 累计同步次数 pub files_synced: i64, // 累计同步文件数 pub bytes_transferred: i64, // 累计传输字节数 pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, }2.1 字段分组解读
可以将这 20 个字段划分为五组职责:
| 分组 | 字段 | 说明 |
|---|---|---|
| 主键与标识 | id、uuid | id为自增主键;uuid带unique约束,用于跨实例/跨设备引用管道 |
| 端点(Endpoints) | source_entry_id、target_entry_id | 外键指向entry表,且两侧必须是kind=1的目录记录 |
| 配置(Configuration) | sync_mode、enabled、schedule、use_index_rules、index_mode_override、parallel_transfers、bandwidth_limit_mbps | 同步模式、启停、调度策略、索引规则开关、性能调优 |
| 状态追踪(State tracking) | last_sync_completed_at、sync_generation、last_sync_error | 上次完成时间、单调递增的代际号、最近错误 |
| 统计(Statistics) | total_syncs、files_synced、bytes_transferred | 累计运行指标,可直接用于 UI 展示与审计 |
值得注意的设计点:同步模式与调度都采用字符串枚举存储(String而非数据库枚举),配合 Rust 侧的SyncMode枚举做类型化解析。这样做的优势是数据库层保持简单、便于未来扩展新值,同时把枚举校验前移到应用层。
2.2 SyncMode 枚举:三种同步语义
实体文件内随附SyncMode枚举,提供as_str()/from_str()/Display实现(core/src/infra/db/entities/sync_conduit.rs#L100-L133):
pub enum SyncMode { Mirror, // 单向:源 → 目标,自动清理目标侧多余文件 Bidirectional, // 双向:两侧同步,带冲突检测 Selective, // 智能本地存储管理(未来功能预留) }三种模式的语义差异直接影响下游SyncResolver的差异计算逻辑(详见 FSYNC-003 任务文档 .tasks/core/FSYNC-003-sync-service-core.md):
- Mirror(镜像):
source_only → copy(源有目标无则复制),target_only → delete(目标有源无则删除),目标目录收敛为源的完整镜像; - Bidirectional(双向):需要检测"自上次同步以来两侧的变更",当同一文件两侧都变化时产生冲突(
SyncConflict)交由冲突解析器处理; - Selective(选择性):设计文档中标注为"智能本地存储管理(未来)",当前为占位语义。
2.3 关系定义:与 entry 的双外键
Relation枚举声明了两条belongs_to外键关系与一条has_many关系(core/src/infra/db/entities/sync_conduit.rs#L68-L89):
SourceEntry:source_entry_id→entry.id;TargetEntry:target_entry_id→entry.id;SyncGenerations:一条 conduit 对应多条sync_generation记录(一对多)。
也就是说,一张 conduit 同时引用 entry 表两次(源、目标),这要求两条外键在迁移中用不同的约束名区分(见下文迁移实现中的fk_sync_conduit_source_entry与fk_sync_conduit_target_entry)。文档明确要求两端都必须是kind=1的目录条目——kind是 entry 表区分文件类型的字段,这一校验在ConduitManager.create_conduit中执行,创建管道前会先验证目录身份并检查重复管道。
三、SyncGeneration 实体:一次同步执行的完整档案
SyncGeneration(同步代际)记录单次同步操作的过程与结果,用于历史查询、冲突检测与一致性验证。实际实现位于 core/src/infra/db/entities/sync_generation.rs,约 105 行:
#[sea_orm(table_name = "sync_generation")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub conduit_id: i32, // 外键 → sync_conduit.id pub generation: i64, // 代际号(单调递增) pub started_at: DateTime<Utc>, // 开始时间 pub completed_at: Option<DateTime<Utc>>, // 完成时间(None 表示仍在进行) pub files_copied: i32, // 本次复制文件数 pub files_deleted: i32, // 本次删除文件数 pub conflicts_resolved: i32, // 本次解决冲突数 pub bytes_transferred: i64, // 本次传输字节数 pub errors_encountered: i32, // 本次错误数 pub verified_at: Option<DateTime<Utc>>, // 验证时间 pub verification_status: String, // 验证状态 }3.1 与 conduit 的联动:generation 号
generation字段是理解这套模型的关键:每次同步执行时,ConduitManager先将 conduit 的sync_generation自增(active.sync_generation = Set(active.sync_generation.unwrap() + 1),见 core/src/service/file_sync/conduit.rs#L134),然后以该代际号创建新的 generation 记录(create_generation,见 core/src/service/file_sync/conduit.rs#L157-L174)。
这样 conduit 表上的sync_generation是"当前指针",而 generation 表通过(conduit_id, generation)组合索引保存了每一次执行的快照。双向同步的冲突检测正是依赖这一历史:比较"同一文件在当前索引中的修改时间"与"最近一次已完成的 generation 的completed_at",即可判断该文件是否在同步之后又被修改过。
3.2 运行中的 generation:completed_at 与 errors_encountered
completed_at为Option,同步进行中为None,完成后由complete_generation填充(core/src/service/file_sync/conduit.rs#L182-L194)。files_copied、files_deleted、conflicts_resolved、bytes_transferred、errors_encountered五个计数字段构成一次同步的操作摘要,可在同步结束后一次性写入,也可在执行过程中实时累加——从结构看它同时服务于"进度监控"与"历史审计"两种场景。
四、验证状态机:Trust Watcher 方案
SyncGeneration的验证字段(verified_at+verification_status)实现了文档中强调的Trust Watcher(信任文件系统监视器)验证策略。VerificationStatus枚举定义于 core/src/infra/db/entities/sync_generation.rs#L68-L105:
| 状态值 | 含义 |
|---|---|
unverified | 同步已完成,尚未验证 |
waiting_watcher | 等待文件系统监视器更新索引 |
waiting_library_sync | 等待库同步(library sync)传播变更 |
verified | 验证查询确认两侧一致 |
failed:<reason> | 验证发现仍有差异(动态拼接原因) |
注意failed是一个动态状态:Rust 侧枚举只覆盖前四个固定变体,from_str对failed:...前缀返回None(因为失败原因可变),同时提供静态工厂VerificationStatus::failed(reason) -> String来拼接出"failed:<reason>"字符串。这说明该列虽以字符串存储,但应用层通过as_str/from_str保持了类型安全。
4.1 为什么选择 Trust Watcher 而非 Eager Update?
任务文档给出了明确的设计取舍(原样保留):
- 单一事实来源:Spacedrive 的文件系统监视器(watcher)已经负责维护索引一致性,同步服务无需重复实现文件系统语义;
- 无重复逻辑:同步服务不需要关心文件系统语义细节,专注差异计算与作业派发;
- 最终一致性:系统天然收敛到一致状态,验证只是确认而非驱动。
对应的验证流程是:同步完成后状态进入unverified→ 等待 watcher 处理事件进入waiting_watcher→ 等待库同步传播进入waiting_library_sync→ 执行一致性查询确认后置为verified;若查询发现差异则置为failed:<reason>。update_verification_status方法(core/src/service/file_sync/conduit.rs#L196-L208)在状态被置为verified时同时写入verified_at = Utc::now(),使"验证时间"与"验证结论"保持一致。
五、SeaORM 迁移:从模型到 SQLite 表
任务文档中的 SQL 设计目标已由迁移文件 core/src/infra/db/migration/m20251015_000002_create_sync_tables.rs(约 280 行)完整实现。该迁移名遵循 SeaORM 的mYYYYMMDD_<序号>_<描述>规范,注册时通过DeriveMigrationName自动提取名称。
5.1 up:建表 + 索引 + 外键
up方法按顺序执行四步操作:
- 创建
sync_conduit表,if_not_exists保证可重入;id为自增主键;uuid为binary().unique_key()(SQLite 中 UUID 以 BLOB 存储);时间列使用timestamp_with_time_zone(映射为 TEXT);统计列使用big_integer以支持大计数。 - 创建
idx_sync_conduit_enabled索引(单列enabled),加速"查询所有启用的管道"这一高频操作(list_enabled)。 - 创建
sync_generation表,conduit_id非空,generation为big_integer,各计数字段带DEFAULT 0,verification_status默认'unverified'。 - 创建
idx_sync_generation_conduit复合索引(conduit_id, generation),支撑按管道高效检索代际历史(如get_last_completed_generation的排序查询)。
外键约束与级联删除是数据完整性的核心:
-- sync_conduit FOREIGN KEY (source_entry_id) REFERENCES entry(id) ON DELETE CASCADE FOREIGN KEY (target_entry_id) REFERENCES entry(id) ON DELETE CASCADE -- sync_generation FOREIGN KEY (conduit_id) REFERENCES sync_conduit(id) ON DELETE CASCADE删除一个 entry 会连带删除引用它的 conduit;删除一个 conduit 会连带删除它的全部 generation 历史,避免孤儿数据。两条指向 entry 的外键分别命名为fk_sync_conduit_source_entry/fk_sync_conduit_target_entry,sync_generation的外键命名为fk_sync_generation_conduit。
5.2 down:按依赖序回滚
down方法先删子表再删父表:DROP TABLE sync_generation→DROP TABLE sync_conduit,与建表顺序相反,保证外键依赖不被破坏。
5.3 迁移注册与实体导出
迁移必须在迁移器(Migrator)中注册才能生效。注册位于 core/src/infra/db/migration/mod.rs:第 17 行mod m20251015_000002_create_sync_tables;,第 61 行将其实例加入Migrator::Migrations列表,从而纳入整个数据库的迁移链。
实体导出位于 core/src/infra/db/entities/mod.rs:第 35-36 行声明模块,第 60-61 行导出Entity别名(SyncConduit/SyncGeneration),第 93-94 行导出ActiveModel别名(SyncConduitActive/SyncGenerationActive),供业务层以类型安全方式使用。
5.4 任务文档中的 SQL 对照
任务文档给出了等价的 SQL DDL(SQLite 方言),与迁移代码相互印证:enabled默认 1、schedule默认'manual'、use_index_rules默认 1、parallel_transfers默认 3、sync_generation/total_syncs/files_synced/bytes_transferred默认 0、verification_status默认'unverified'。这些默认值在迁移的default(...)声明中一一对应,是理解"新建管道时的初始状态"的第一手资料:
CREATE TABLE sync_conduit ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB NOT NULL UNIQUE, source_entry_id INTEGER NOT NULL, target_entry_id INTEGER NOT NULL, sync_mode TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, schedule TEXT NOT NULL DEFAULT 'manual', use_index_rules INTEGER NOT NULL DEFAULT 1, index_mode_override TEXT, parallel_transfers INTEGER NOT NULL DEFAULT 3, bandwidth_limit_mbps INTEGER, last_sync_completed_at TEXT, sync_generation INTEGER NOT NULL DEFAULT 0, last_sync_error TEXT, total_syncs INTEGER NOT NULL DEFAULT 0, files_synced INTEGER NOT NULL DEFAULT 0, bytes_transferred INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (source_entry_id) REFERENCES entry(id) ON DELETE CASCADE, FOREIGN KEY (target_entry_id) REFERENCES entry(id) ON DELETE CASCADE ); CREATE INDEX idx_sync_conduit_enabled ON sync_conduit(enabled);六、如何验证迁移:test_migration 示例
任务文档的验收标准要求"Migration runs successfully via test_migration example",仓库为此提供了专门的可执行示例 core/examples/test_migration.rs。它演示了验证这套 Schema 的标准流程:
- 在
./data/test_migration.db创建临时 SQLite 数据库; - 通过
Database::connect("sqlite://...?mode=rwc")建立连接; - 调用
Migrator::up(&db, None)执行全部迁移; - 查询
sqlite_master列出所有用户表,确认sync_conduit、sync_generation及其余表创建成功; - 结束后删除临时数据库文件。
运行方式(在仓库根目录执行):
cargo run --example test_migration --manifest-path core/Cargo.toml该示例同时佐证了任务进度记录中的结论:"Both tables created with foreign keys, indexes, and constraints"——迁移链整体可用,且建出的表能被 SeaORM 正常查询。若你需要在真实库上确认 schema,也可用 sqlite3 打开 Spacedrive 的数据库文件后执行.tables与PRAGMA foreign_keys;检查。
七、数据流与关系全貌
任务文档末尾给出了实体关系图,与实际代码完全一致:
sync_conduit ├─> entry (source_entry_id) ├─> entry (target_entry_id) └─> sync_generation (one-to-many)综合起来,一条完整的同步生命周期是:
- 创建:
ConduitManager.create_conduit校验两个 entry 均为目录、无重复管道后写入sync_conduit(sync_generation=0,各统计为 0); - 执行:同步开始时 conduit 的
sync_generation自增,并插入一条sync_generation记录(started_at写入,completed_at=NULL,状态unverified); - 派发:
SyncResolver.calculate_operations基于索引查询计算to_copy/to_delete与冲突,将工作派发给FileCopyJob/DeleteJob(服务只派发作业、不直接执行,职责分离); - 收尾:作业完成后
complete_generation填充计数与completed_at,并回写 conduit 的total_syncs、files_synced、bytes_transferred、last_sync_completed_at等统计; - 验证:按
unverified → waiting_watcher → waiting_library_sync → verified / failed:<reason>的状态机流转,verified_at在最终确认时写入。
这套"管道(conduit)+ 代际(generation)"双层结构,是 FSYNC-003 中FileSyncService的active_syncs追踪、防重复同步与进度监控的数据基础,也是 FSYNC-005 验证流程所依赖的历史档案——两者都建立在本篇所述两张表之上。
八、设计要点小结
- 持久化是同步可靠性的地基:
sync_generation指针 + generation 历史表,让"中断恢复"与"冲突检测"都有了可查询的依据; - 字符串枚举 + 应用层类型化:
sync_mode、schedule、verification_status以字符串存储、以 Rust 枚举解析,兼顾扩展性与类型安全,failed:<reason>的动态状态由此成为可能; - 级联删除保证数据完整:
entry → conduit → generation的级联链避免了孤儿记录,回滚顺序与依赖方向严格一致; - 索引服务查询模式:
idx_sync_conduit_enabled服务"列出启用管道",idx_sync_generation_conduit服务"按管道检索历史",均直击下游最高频的查询路径; - 验证可自动化:
test_migration示例让迁移的正确性验证成为一条命令的事,任务文档的验收标准因此全部可勾选。
如果你希望继续深入,可以顺着三条线索阅读:下游服务实现见 core/src/service/file_sync/mod.rs 与 core/src/service/file_sync/conduit.rs;同步语义与 UI 层的整体说明见 docs/core/file-sync.mdx;FSYNC 系列后续任务(FSYNC-003-sync-service-core.md、FSYNC-005-advanced-features.md)则展示了这些实体如何被同步编排与验证流程消费。
【免费下载链接】spacedriveSpacedrive is an open source cross-platform file explorer, powered by a virtual distributed filesystem written in Rust.项目地址: https://gitcode.com/gh_mirrors/sp/spacedrive
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考