Spacedrive Entry 中心化数据模型:用统一 Entry 建模文件、目录与符号链接的 VDFS 核心设计
2026/9/19 21:55:06 网站建设 项目流程

Spacedrive Entry 中心化数据模型:用统一 Entry 建模文件、目录与符号链接的 VDFS 核心设计

【免费下载链接】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 的虚拟分布式文件系统(VDFS)以Entry作为一切文件系统对象(文件、目录、符号链接)的统一载体:无论对象种类如何,Entry通过metadata_id外键链接一条UserMetadata记录,使任意文件在索引发现的第一时间就具备打标签、加备注、收藏等元数据能力。本文以任务文档 CORE-001-entry-centric-model.md 为核心骨架,结合core工作区中的领域模型、数据库实体、迁移脚本与同步实现,完整讲解 Entry 模型的字段设计、数据库 Schema、内容身份去重机制与跨设备同步语义,帮助读者理解并复用这套“文件即 Entry”的数据建模思路。

一、从任务到落地:CORE-001 的交付物与仓库路径映射

CORE-001 是 Spacedrive core 任务看板中的一项已交付任务(status: Done,负责人jamiepine,父任务CORE-000,优先级 High),其目标非常明确:

实现通用的Entry数据模型,它表示任意文件系统条目(文件、目录、符号链接)。每个Entry都通过一条链接的UserMetadata记录具备即时元数据能力,允许用户在文件被发现的瞬间就对其进行打标签与组织。

任务文档给出的三条实现要点与验收标准如下(原文摘录):

  • 核心领域模型定义于src/domain/entry.rs
  • 对应数据库实体实现于src/infrastructure/database/entities/entry.rs
  • EntryKind枚举正确区分FileDirectorySymlink三种类型;
  • 验收标准(均已勾选完成):Entry结构体存在且包含metadata_idcontent_id字段;系统能使用统一模型表示文件与目录;数据库 Schema 正确反映Entry模型。

需要说明的是,任务文档撰写时使用的路径是简写形式,在当前仓库的实际布局中,这些模块的落点为:

任务文档中的路径仓库中的实际实现
src/domain/entry.rscore/src/domain/file.rs(EntryKind与聚合模型File
src/infrastructure/database/entities/entry.rscore/src/infra/db/entities/entry.rs(SeaORM 实体)
EntryKind枚举领域层与数据库层各有一份,数值编码一致(见下文)

此外,领域模型通过 core/src/domain/mod.rs 统一导出:pub use file::{EntryKind, File, Sidecar};pub use user_metadata::UserMetadata;,供上层 ops、service 与前端 SDK 直接引用。

二、Entry 统一模型:一个模型表达文件、目录与符号链接

2.1 领域层的EntryKind枚举

在 core/src/domain/file.rs 中,领域层定义了三值枚举,并通过serdespecta::Type派生,使其既能序列化传输,也能自动生成 TypeScript 类型供前端使用:

/// Type of filesystem entry #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] pub enum EntryKind { /// Regular file File, /// Directory Directory, /// Symbolic link Symlink, }

数据库实体层在 core/src/infra/db/entities/entry.rs 中定义了数值编码完全一致的持久化枚举,并实现i32与枚举的双向转换,任何未知值都安全回退为File

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum EntryKind { File = 0, Directory = 1, Symlink = 2, }

领域层在将数据库模型转换为File时也按同一映射处理(见File::from_entity_model,core/src/domain/file.rs):0 → File1 → Directory2 → Symlink。目录、文件、符号链接从此共享同一张表、同一套索引、同一套同步通道,而无需为每种对象维护独立模型——这正是“Entry 中心化”的核心收益。

2.2 聚合模型File:Entry 的“开发者友好面”

Entry只是持久化骨架,面向业务与前端的是聚合模型File(core/src/domain/file.rs)。它是计算型领域模型:不重复存储数据,而是把EntryContentIdentityTagSidecar、媒体元数据在查询时一次性组装:

pub struct File { pub id: Uuid, pub sd_path: SdPath, // VDFS 通用路径 pub kind: EntryKind, // 文件 / 目录 / 符号链接 pub name: String, pub extension: Option<String>, // 不含点号 pub size: u64, pub content_identity: Option<ContentIdentity>, // 内容哈希身份,用于去重 pub alternate_paths: Vec<SdPath>, // 共享同一内容的其它路径(副本) pub tags: Vec<Tag>, // 语义标签 pub sidecars: Vec<Sidecar>, // 侧车文件 pub image_media_data: Option<ImageMediaData>, pub video_media_data: Option<VideoMediaData>, pub audio_media_data: Option<AudioMediaData>, pub created_at: DateTime<Utc>, pub modified_at: DateTime<Utc>, pub accessed_at: Option<DateTime<Utc>>, // ... }

File实现Identifiabletrait(core/src/domain/file.rs),声明了同步依赖链:entrycontent_identitysidecarimage_media_datavideo_media_dataaudio_media_datauser_metadatauser_metadata_tagtag。这意味着当任一条依赖(例如标签)发生变化时,资源系统可以沿依赖图反查受影响的文件并重发事件。其中与 Entry 直接相关的路由模式有:

  • 直接映射"entry"依赖下,File ID = Entry UUID
  • 经内容身份扇出"content_identity"依赖下,先按 UUID 找到内容身份,再查出所有content_id指向它的Entry——一条内容身份可对应多个物理位置;
  • 经用户元数据扇出"user_metadata"/"user_metadata_tag"依赖下,依据元数据的作用域(entry 级或 content 级)路由到单个 Entry 或该内容的所有 Entry。

批量组装由File::from_entry_uuids(core/src/domain/file.rs)完成,通过is_in批量加载 entries、content_identities、sidecars、tags,并借助HashMap归组避免 N+1 查询;同时为每个文件填充alternate_paths(含自身物理路径与其它同内容副本),使前端可以据此实现“服务器端过滤”与去重展示。

三、即时元数据能力:metadata_id与常驻UserMetadata

3.1 设计动机:文件被发现的瞬间即可组织

任务文档强调“EveryEntryis designed for immediate metadata capability via a linkedUserMetadatarecord”。领域层对这一点有更直白的注释(core/src/domain/user_metadata.rs):

This is the key innovation: EVERY Entry has UserMetadata, even if empty. This means any file can be organized immediately without content indexing.

即:即使某个文件尚未完成内容指纹/哈希(content_id为空),它也能立刻被收藏、加备注、隐藏或打标签,因为元数据能力不依赖内容识别结果。

3.2 领域结构UserMetadata

core/src/domain/user_metadata.rs 定义的UserMetadata包含:

字段类型说明
idUuid唯一标识,与Entry.metadata_id对应
notesOption<String>自由文本备注
favoritebool是否收藏
hiddenbool是否隐藏
custom_fieldsJsonValue自定义扩展字段(未来扩展点)
created_at/updated_atDateTime<Utc>创建与更新时间

UserMetadata::new(id)生成全空元数据(notes: Nonefavorite: falsehidden: false、空对象custom_fields),并提供set_notestoggle_favoriteset_hiddenis_empty()等操作,每个变更都会刷新updated_at。单元测试test_empty_metadata验证了空元数据的判定逻辑(core/src/domain/user_metadata.rs)。

3.3 数据库实体:作用域(Scope)设计

数据库层的user_metadata实体(core/src/infra/db/entities/user_metadata.rs)将作用域建模为二选一的可空外键

pub entry_uuid: Option<Uuid>, // File-specific metadata (higher priority) pub content_identity_uuid: Option<Uuid>, // Content-universal metadata (lower priority)

MetadataScope枚举(同文件 L68-L72)明确两种语义:

  • Entry 级(MetadataScope::Entry:只作用于某一个 Entry 实例(如“这个副本的备注”);
  • Content 级(MetadataScope::Content:作用于拥有同一内容身份的所有 Entry(如“这张照片的收藏,无论在哪台设备上”)——File::from_entry_uuids的标签组装逻辑(core/src/domain/file.rs)会把 content 级标签自动应用到该内容的所有副本上。

创建与获取由UserMetadataManager(core/src/ops/metadata/manager.rs)提供:get_or_create_entry_metadata(entry_uuid)(L47-L79)与get_or_create_content_metadata(content_identity_uuid)(L82 起)。以 entry 级为例,其创建逻辑为:先按entry_uuid查询,不存在则插入一条notes=None, favorite=false, hidden=false, custom_data={}的空元数据记录,从而兑现“发现即可用”。

四、内容身份:content_id与跨设备去重

Entry的另一个关键外键是content_id,指向内容身份表,用于去重:多个物理 Entry(不同设备、不同路径)可以共享同一个内容身份。

领域模型ContentIdentity(core/src/domain/content_identity.rs)包含uuidkindContentKind)、content_hashintegrity_hashmime_type_idtext_contenttotal_sizeentry_countfirst_seen_atlast_verified_atContentKind是 27 个值的分类枚举(Unknown=0Image=1Video=2Audio=3Document=4Archive=5……Memory=26,见 core/src/domain/content_identity.rs)。

哈希生成策略(ContentHashGenerator,同文件 L132-L264)值得一提,因为它直接决定去重的成本与准确性:

  • 空文件直接拒绝生成内容身份(ContentHashError::EmptyFile),避免所有空文件因相同哈希被误判为同一内容;
  • 小于等于MINIMUM_FILE_SIZE(100KB,1024 * 100)的文件整读全哈希;
  • 大文件采用采样哈希:读头部 8KB(HEADER_OR_FOOTER_SIZE)+ 均匀分布的 4 个采样块(SAMPLE_COUNT=4,每块 10KB,SAMPLE_SIZE)+ 尾部 8KB,总共只传输约 58KB 数据。该算法通过VolumeBackend::read_range实现,因此对云端文件同样只需范围读即可高效计算内容身份,无需整文件下载。

五、数据库 Schema:entries表与关联关系

5.1 建表语句

初始迁移 core/src/infra/db/migration/m20240101_000001_initial_schema.rs 中的entries表(SeaORM 使用Entries常量)完整字段如下:

类型约束/说明
idinteger自增主键
uuiduuid同步与 UI 缓存兼容用的稳定标识
namestring非空,条目名(含扩展名)
kindinteger非空,0=File / 1=Directory / 2=Symlink
extensionstring可空,文件扩展名(不含点),目录为 NULL
metadata_idinteger可空,外键 →user_metadata.idON DELETE SET NULL
content_idinteger可空,外键 →content_identities.idON DELETE SET NULL
sizebig_integer非空,文件字节数
aggregate_sizebig_integer非空,含全部子项的合计大小(目录)
child_countinteger非空,直接子项数量
file_countinteger非空,目录及其子目录中的文件总数
created_at/modified_attimestamp with tz非空
accessed_attimestamp with tz可空
permissionsstringUnix 权限字符串
inodebig_integer平台相关文件标识,用于变更检测
parent_idinteger自引用,指向父目录的entries.id

SeaORM 实体 core/src/infra/db/entities/entry.rs 与之对应,并额外携带indexed_at(索引/同步时间戳,同步水位线)与volume_id(所属卷,卷归属设备从而推导 Entry 的设备所有权)。实体定义的四个关系(同文件 L33-L55)为:UserMetadatametadata_id → user_metadata.id)、ContentIdentitycontent_id → content_identities.id)、Parent(自引用)、Volumevolume_id → volume.id)。metadata_idcontent_id均使用ON DELETE SET NULL,保证元数据/内容身份被删除时 Entry 仍然存活。

5.2 层级查询:entry_closure闭包表

父子层级通过自引用parent_id表达,而高效的后代/祖先查询依赖闭包表entry_closureancestor_id, descendant_id, depth)。同步链路在插入 Entry 后调用rebuild_entry_closure(core/src/infra/db/entities/entry.rs):删除该 Entry 作为后代的旧闭包记录 → 插入 depth=0 的自引用 → 复制父节点全部祖先关系并depth+1。同步回填完成后则调用rebuild_all_entry_closures(L772-L842)做整体重建:清空闭包表 → 批量插入自引用 → 迭代执行INSERT OR IGNORE直至无新关系(超过 100 轮视为存在环,报错退出)。这保证了子树删除(delete_subtree)、位置作用域过滤等操作在同步数据上依然正确。

六、同步语义:设备所有权的状态复制

Entry在 core/src/infra/db/entities/entry.rs 注册为设备所有(device-owned)的同步模型SYNC_MODEL = "entry"exclude_fields = ["id", "indexed_at"](主键与本地水位线不外发),sync_depends_on = ["volume", "content_identity", "user_metadata"],确保外键目标先于 Entry 到达。

关键设计(同文件apply_state_change,L476-L659)包括:

  • 无 HLC 排序、幂等 upsert、last-write-wins:Entry 只有属主设备修改,因此采用基于状态的复制而非日志复制,按 UUID 幂等写入;
  • UUID ↔ 本地整型主键映射foreign_key_mappings(L101-L110)声明volume_id→volumesparent_id→entriesmetadata_id→user_metadatacontent_id→content_identities四组外键映射,同步广播前批量把整型 FK 转换为 UUID,接收端再映射回本地 ID(map_sync_json_to_local);
  • 墓碑保护:Entry 自身或父节点已被墓碑标记时跳过应用,防止孤儿节点与复活竞态(L511-L560);
  • 目录路径一致化:同步时目录会附带directory_path绝对路径并写入directory_paths表(L623-L656),使不同设备上的目录拥有完全一致的寻址路径,避免本地重建路径的偏差(例如根目录应保留/Users/jamespine/Downloads而非Downloads);
  • 删除级联apply_deletion按 UUID 找到 Entry 后调用delete_subtree级联删除整棵子树(L407-L420),且不产生额外墓碑。

同步查询端(query_for_sync,L197-L397)以indexed_at作为水位线,采用(indexed_at, uuid)双字段游标分页保证确定性;设备过滤通过JOIN volume ON volume.device_id = ?实现,使卷的所有权变更可自动迁移其下全部 Entry。

七、验收标准核验与工程启示

对照任务文档的三条验收标准,可在当前源码中逐一找到证据:

验收标准源码证据
Entry结构体存在且包含metadata_idcontent_identries表列定义(迁移脚本 L242-L243)与实体模型 core/src/infra/db/entities/entry.rs
系统能使用统一模型表示文件和目录EntryKind::{File=0, Directory=1, Symlink=2}双端定义,File聚合模型统一承载
数据库 Schema 正确反映Entry模型初始迁移entries表 +entry_closure表 +user_metadata/content_identities外键

从这套设计中可以提炼几条值得借鉴的建模原则:

  1. 统一载体 + 可空增强列:文件/目录/符号链接共用一个模型,metadata_idcontent_id均设计为可空外键,让“基础能力”与“增强能力”解耦——未哈希、未加元数据的条目也完整可用;
  2. 计算型聚合视图:持久层保持扁平、可同步的Entry,对外暴露的是按需批量组装的File聚合视图,兼顾同步简单性与业务表达力;
  3. 作用域驱动的元数据entry_uuidcontent_identity_uuid二选一的作用域设计,让“单副本备注”与“全内容收藏”两种语义共存且冲突规则清晰(entry 级优先);
  4. 同步语义与数据所有权绑定:设备所有的状态复制省去冲突消解,闭包表随写入即时维护,墓碑与水位线(indexed_at)保障删除与增量同步的正确性。

若要在实际工程中落地或扩展该模型,可直接以 core/src/domain/file.rs、core/src/infra/db/entities/entry.rs 与 core/src/infra/db/migration/m20240101_000001_initial_schema.rs 为参照:新增字段时需同步修改实体模型、领域聚合与同步字段排除清单,并确保entry_closure重建逻辑覆盖新增的父子关系;扩展元数据类型时,优先复用custom_fieldsJSON 字段而非新增硬编码列,以保持同步与迁移的最小化。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询