feat(file-manager): add schema and foundation for file management system#13451
feat(file-manager): add schema and foundation for file management system#13451
Conversation
Add comprehensive documentation covering: - VS Code explorer tree implementation overview - Current notes file tree implementation - Existing file architecture problems - Current file management implementation details - RFC for new file manager design (single node table + file tree) These documents provide technical reference and design context for the file management system, highlighting current issues and proposed solutions.
Key improvements from 3 rounds of review: - Add cross-mount move prohibition with explicit moveNode impl - Fix permanentDelete ordering (physical files before DB) - Fix OrphanRefScanner pagination bug (cursor-based instead of offset) - Add ext format normalization spec and resolvePhysicalPath reference impl - Add batch APIs (trash/move/delete) and children sorting/pagination - Add cycle detection in moveNode and system node protection in trashNode/permanentDelete - Add file_ref migration fault tolerance (skip missing nodes) - Add local_external physical file sync for trash/restore operations - Add .trash directory spec and chokidar exclusion rules - Add operation lock mechanism to prevent chokidar race conditions - Upgrade system_trash from dir to mount (provider_type: 'system') - Sync Handler/Service signatures with new API schema additions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: icarus <eurfelux@gmail.com>
Phase 1 implementation: Zod provider config schemas, FileNode/FileRef entity types, Drizzle node+file_ref tables with migration, API schema definitions with placeholder handlers, system node seeding (Files, Notes, Trash mounts), and path resolver utility with unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: icarus <eurfelux@gmail.com>
EurFelux
left a comment
There was a problem hiding this comment.
Overall
Solid Phase 1 foundation PR! The schema design, type system integration, and documentation quality are all excellent. The RFC is thorough with clear trade-off reasoning.
Highlights
- Idempotent seeding logic —
NodeSeed.migrate()checks existing records before inserting, very robust - Minimal interfaces in pathResolver —
PathResolvableNode/MountInfoinstead of fullFileNode, clean decoupling - Placeholder handlers —
notImplemented+ exhaustive type checking validates the type system integration early - Test coverage — 9 test cases for pathResolver covering all edge cases
- Documentation — RFC and supporting docs are detailed with clear trade-off records
Issues found
- [Medium]
nodeSeeding.ts—getFilesDir()/getNotesDir()called at module load time (see inline comment) - [Low]
files.ts—DELETEwith request body in batch/delete endpoint (see inline comment) - [Nit] Missing
remoteprovider type test case (see inline comment)
None of these are blocking. Great work! 🎉
- Defer getFilesDir()/getNotesDir() calls to migrate() runtime to avoid calling app.getPath() at module load time before app.ready - Change batch delete endpoint from DELETE to POST to avoid DELETE-with-body compatibility issues - Add missing test case for remote provider type path resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: icarus <eurfelux@gmail.com>
EurFelux
left a comment
There was a problem hiding this comment.
Re-review after fixes
All feedback from the previous review has been addressed:
-
✅
nodeSeeding.tslazy init —SYSTEM_NODESmodule-level constant replaced withgetSystemNodes()function, ensuringgetFilesDir()/getNotesDir()are only called atmigrate()time (after Electronapp.ready). -
✅
remotetest case added — Newdescribe('remote')block correctly tests the throw path, with proper Zod-required fields (auto_sync,options) included. -
ℹ️
DELETEwith body — Acknowledged as low priority for an internal IPC-based API, acceptable to defer.
CI Status
general-test✅basic-checks✅translate✅render-test/skills-check-windowsstill pending
LGTM! Clean Phase 1 foundation with solid schema design, good test coverage, and thorough documentation. 🎉
Replace plain TypeScript interfaces with Zod schemas for runtime validation. Adds system node ID constants, UUID v7 format validation, type invariants via superRefine, and lifecycle state guards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: icarus <eurfelux@gmail.com>
Use a nullable ms-epoch timestamp instead of a boolean flag so the service layer can compare with remote updatedAt to detect stale caches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: icarus <eurfelux@gmail.com>
There was a problem hiding this comment.
Architecture Review — Phase 1 Foundation
Note
This review was translated by Claude.
The schema design is solid overall, and the RFC documentation quality is high. Below are several architectural concerns; none of these block Phase 1, but I recommend addressing them during Phase 2 implementation:
Summary of Inline Comments
| # | Severity | File | Concern |
|---|---|---|---|
| 1 | Medium | node.ts:60 |
parentId CASCADE deletion may accidentally delete the entire file tree; recommend adding delete protection for system nodes |
| 2 | Medium | fileNode.ts:110 |
Type invariants rely only on Zod runtime validation; no constraints at the DB layer; recommend validating on write as well |
| 3 | Low | node.ts:83-98 |
file_ref polymorphic references have no FK; may leave orphaned references when source is deleted |
| 4 | Low | files.ts:76-93 |
recursive children query may have performance risks; suggest changing to maxDepth |
Not Filed as Inline (Minor)
local_externalTrash Path: The RFC describes moving Trash files to{base_path}/.trash/{nodeId}/, but the currentpathResolver'slocal_externalbranch doesn't reserve this path. When implementing trash in Phase 2, you'll need to modify theresolvePhysicalPathsignature.- Seeding Order: Currently
migrateSeed('preference')→migrateSeed('node')is manually sorted; this may become fragile as the number of seeds increases.
Overall: clean Phase 1 foundation, good to go once CI passes ✅
Original Content
Architecture Review — Phase 1 Foundation
Schema 设计整体扎实,RFC 文档质量很高。以下是从架构视角的几点担忧,均不 blocking Phase 1,但建议在 Phase 2 实现时注意:
Summary of Inline Comments
| # | Severity | File | Concern |
|---|---|---|---|
| 1 | Medium | node.ts:60 |
parentId CASCADE 删除可能误删整棵文件树,建议 system node 加删除保护 |
| 2 | Medium | fileNode.ts:110 |
Type invariants 仅靠 Zod 运行时验证,DB 层无约束,建议写入时也 validate |
| 3 | Low | node.ts:83-98 |
file_ref 多态引用无 FK,source 删除时可能留下孤儿引用 |
| 4 | Low | files.ts:76-93 |
recursive children 查询可能有性能风险,建议改为 maxDepth |
Not Filed as Inline (Minor)
local_externalTrash 路径:RFC 描述 Trash 文件移动到{base_path}/.trash/{nodeId}/,但当前pathResolver的local_external分支未预留这个分支。Phase 2 实现 trash 时需要改造resolvePhysicalPath签名。- Seeding 顺序:目前
migrateSeed('preference')→migrateSeed('node')是手动排序,seed 增多后可能变脆弱。
Overall: clean Phase 1 foundation, good to go once CI passes ✅
| }, | ||
| (t) => [ | ||
| // Self-referencing FK: cascade delete children when parent is deleted (Trash cleanup) | ||
| foreignKey({ columns: [t.parentId], foreignColumns: [t.id] }).onDelete('cascade'), |
There was a problem hiding this comment.
Note
This comment was translated by Claude.
[Medium] CASCADE Deletion Needs Phase 2 Protection
onDelete('cascade') is convenient when clearing Trash, but it also means that if mount_files seed node is accidentally deleted, the entire file tree along with all file_ref will cascade disappear, completely bypassing the service layer—no chance to trigger physical file cleanup.
The Phase 2 service layer is recommended to add hard-coded deletion protection for SYSTEM_NODE_IDS (mount_files, mount_notes, system_trash) (directly reject delete requests), to avoid any bugs or accidental operations triggering cascades through the service layer.
Not blocking for Phase 1.
Original Content
[Medium] CASCADE 删除需要 Phase 2 加保护
onDelete('cascade') 在清空 Trash 时很方便,但也意味着如果误操作删除了 mount_files 这个 seed 节点,整棵文件树连同所有 file_ref 都会级联消失,且完全绕过 service 层——没有机会触发物理文件清理。
Phase 2 的 service 层建议对 SYSTEM_NODE_IDS(mount_files、mount_notes、system_trash)加 hard-coded 删除保护(直接拒绝 delete 请求),避免任何 bug 或意外操作通过 service 层触发级联。
Not blocking for Phase 1.
| { | ||
| id: uuidPrimaryKey(), | ||
|
|
||
| // Referenced file node ID | ||
| nodeId: text() | ||
| .notNull() | ||
| .references(() => nodeTable.id, { onDelete: 'cascade' }), | ||
|
|
||
| // Business source type (e.g. 'chat_message', 'knowledge_item', 'painting', 'note') | ||
| // Enum validated at application layer (Zod), no CHECK constraint | ||
| sourceType: text().notNull(), | ||
| // Business object ID (polymorphic, no FK constraint) | ||
| sourceId: text().notNull(), | ||
| // Reference role (e.g. 'attachment', 'source', 'asset') | ||
| role: text().notNull(), | ||
|
|
There was a problem hiding this comment.
[Low] Orphan cleanup for file_ref polymorphic references
Note
This comment was translated by Claude.
sourceId has no FK constraint (polymorphic design, reasonable), but this means when deleting business objects like messages/knowledge_items, the corresponding service layer must actively clean up file_ref. If a service misses this step, orphan references will be created—potentially blocking a future "cleanup if unreferenced" auto-reclamation strategy.
Suggested Phase 2 considerations:
- Add defensive
deleteFileRefsBySource()calls in each source entity's delete logic - Periodic GC job / health check to detect records where
sourceIdpoints to non-existent business objects
Not blocking.
Original Content
[Low] file_ref 多态引用的孤儿清理
sourceId 没有 FK 约束(多态设计,合理),但这意味着删除 message / knowledge_item 等业务对象时,必须 由对应的 service 层主动清理 file_ref。如果某个 service 漏了这一步,就会产生孤儿引用——可能阻碍未来"无引用则可清理"的自动回收策略。
建议 Phase 2 考虑以下之一:
- 在各 source entity 的 delete 逻辑中加防御性
deleteFileRefsBySource()调用 - 定期 GC job / health check 检测
sourceId指向不存在的业务对象的记录
Not blocking.
| /** Remote file ID (e.g. OpenAI file-abc123) */ | ||
| remoteId: z.string().nullable(), | ||
| /** When the local cache was last downloaded (ms epoch). Null if not cached. Compare with remote updatedAt to detect staleness */ | ||
| cachedAt: z.int().nullable(), | ||
| /** Original parent ID before moving to Trash (only for Trash direct children) */ | ||
| previousParentId: NodeIdSchema.nullable(), | ||
| /** Creation timestamp (ms epoch) */ | ||
| createdAt: z.int(), | ||
| /** Last update timestamp (ms epoch) */ | ||
| updatedAt: z.int() | ||
| }) |
There was a problem hiding this comment.
Note
This comment was translated by Claude.
[Medium] Type invariants are only validated during Zod reads, with no DB-level guarantees
The superRefine here implements rich type invariants (mount must have parentId=null, dir cannot have providerConfig, etc.), which is of high quality.
However, these constraints only take effect during Zod parse—if Phase 2's service layer has a bug and directly writes data that violates constraints through Drizzle (e.g., type=dir but with providerConfig), SQLite will silently accept it until the next read parse exposes the problem.
It is recommended that Phase 2 add defensive assertions in the repository layer's write methods (validate before writing), rather than only relying on parse during reads. For example:
async createNode(dto: CreateNodeDto): Promise<FileNode> {
const row = await db.insert(nodeTable).values(mapped).returning()
// Parse and validate immediately after write to ensure DB data conforms to invariants
return FileNodeSchema.parse(row[0])
}This can move "bad write" problems from "discovered during read" to "fail immediately during write".
Original Content
[Medium] Type invariants 只在 Zod 读取时验证,DB 层无保障
这里的 superRefine 实现了丰富的 type invariants(mount 必须 parentId=null、dir 不能有 providerConfig 等),质量很高。
但这些约束仅在 Zod parse 时生效——如果 Phase 2 的 service 层有 bug 直接通过 Drizzle 写入了违反约束的数据(比如 type=dir 但带了 providerConfig),SQLite 会默默接受,直到下次读取 parse 才暴露问题。
建议 Phase 2 在 repository 层的写入方法中加防御性断言(写入前 validate),而不是只依赖读取时的 parse。比如:
async createNode(dto: CreateNodeDto): Promise<FileNode> {
const row = await db.insert(nodeTable).values(mapped).returning()
// 写入后立即 parse 验证,确保 DB 中数据符合 invariants
return FileNodeSchema.parse(row[0])
}这样可以把 "bad write" 问题从 "读取时才发现" 提前到 "写入时立即失败"。
Allows callers to limit tree depth when using recursive=true, preventing accidental full-tree queries on large directories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: icarus <eurfelux@gmail.com>
…tion plan Add comments documenting how the note table will integrate with the file manager node table (PR #13451): add nodeId FK, backfill from relativePath, and eventually derive path from node tree. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
What this PR does
Before this PR:
No file management data layer exists. The codebase lacks unified types, DB schema, and API definitions for the planned file manager feature.
After this PR:
Adds the complete Phase 1 foundation for the file management system (RFC:
docs/tmp/rfc-file-manager.md):fileProvider.ts) — runtime-validated discriminated union for mount provider types (local_managed,local_external,remote,system)fileNode.ts) —FileNode,FileRef, and corresponding DTOsnode.ts) —nodeandfile_reftables with indexes, CHECK constraints, self-referencing FK with CASCADEfiles.ts) —FileSchemastype covering all file CRUD, tree ops, refs, batch ops, and mount listing endpointsmount_files,mount_notes,system_trashresolvePhysicalPath()with 9 passing unit testsWhy we need it and why it was done in this way
This is the data layer foundation that Phase 2 (services, handlers, UI) builds on. Splitting into phases keeps PRs reviewable and allows early validation of the schema design.
The following tradeoffs were made:
throw Error('Not implemented')were added to satisfyApiImplementationexhaustive type checking. These will be replaced in Phase 2.number(ms epoch) instead of ISO strings, following the newer v2 DB convention.The following alternatives were considered:
FileSchemastoApiSchemasvalidates the type system integration early.Breaking changes
None. This is purely additive — new tables, new types, no changes to existing functionality.
Special notes for your reviewer
docs/tmp/rfc-file-manager.mdfor full design context.nodeSeeding.tscallsgetFilesDir()andgetNotesDir()which depend on Electronapp.getPath('userData')— this runs at app startup after Electron is ready.Checklist
Release note