feat(static-file): conversation workspace file serving with pluggable access guard#281
Open
zhuqingyv wants to merge 13 commits into
Open
feat(static-file): conversation workspace file serving with pluggable access guard#281zhuqingyv wants to merge 13 commits into
zhuqingyv wants to merge 13 commits into
Conversation
79f8361 to
0c12c18
Compare
…able access guard
Introduces a framework-agnostic static file service (aionui-static-file) with:
- Pluggable async access guard for authorization decisions
- HTTP Range request support for video/PDF seeking
- Path traversal prevention with canonicalize sandbox
- 256MB file size limit
- ETag generation from mtime + file size
Exposes endpoint: GET /api/conversations/{id}/files/{*path}
Extracts has_traversal to aionui-common for single source of truth.
Includes 9 E2E integration tests and 17 unit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
aionui-static-file 在 c90fbd8 (feat: static-file) 升到 0.1.8, 但 Cargo.lock 当时未一并提交,本次补齐。
之前 Phase 1 分发只看 change.path 的「直接父目录」,订阅了根目录的 连接拿不到深层文件 (例如 src/docs/foo.md) 的事件,因为它的父目录是 src/docs 而非 ""。结果:右键删除 src/docs 整个文件夹时,根订阅者收不 到任何 workspace.changed,前端必须 Cmd+R 才看到 docs 消失。 新分发规则:对每个 change,沿祖先链找出**每个连接**最近订阅的目录, 路径重写为该订阅者的直接子目录 (rewritten kind=Modify);订阅者恰好 是直接父目录时保留原 kind。多连接在不同深度订阅时各自拿到自己最近祖 先的事件,互不干扰;同一连接的多个深层 change 折叠到同一 rewritten path 上自动去重。 新增辅助 ancestor_chain / resolve_subscribed_path 纯函数 + 6 个单测, 以及 5 个 dispatch_changes 集成测试 (覆盖直接子、重写、最近祖先、 多连接、去重、无订阅者)。
0c12c18 to
c5f6f48
Compare
Remove 10 mcp-workflow-proposal-ELECTRON-*.json files that were committed by mistake — they are sentry-patrol per-issue MCP server configs containing local-machine absolute paths (e.g. /Users/zhuqingyu/.cargo/bin/sentry-patrol-mcp) and have nothing to do with this branch's static-file work. Add a .gitignore entry to prevent future accidental commits.
These types were only used by their own self-tests after the fileStream.contentUpdate broadcast was removed from FileService. No external import, no pub use export, no serialization callers.
FileService no longer broadcasts the fileStream.contentUpdate event, but write_file/remove_entry doc comments still claimed it does.
The write/remove request types' workspace field doc still referenced the removed fileStream.contentUpdate event payload.
…api-types WatchChange / WatchChangeKind / WatchBatchEvent / WatchOverflowEvent are WS event payloads, so per AGENTS.md they belong in aionui-api-types alongside other request/response/WS DTOs, not inside aionui-file. Pure relocation: definitions and serde derives unchanged, no behavior change. aionui-file now imports them from aionui-api-types. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background & Motivation
Old architecture (main)
crates/aionui-file/src/watch_service.rsprovides two independent features:file_watcher) — one sharedRecommendedWatcher, NonRecursive, broadcastsfileWatch.fileChangedon change.office_watchers) — per-workspaceRecommendedWatcher, Recursive, broadcastsworkspaceOfficeWatch.fileAddedon office file create.Both share a single
DashMap<String, Instant>for 200ms debouncing, with no subscription model and no WS-disconnect cleanup.Capabilities that did not exist:
MessageRoutertrait had noon_disconnectlifecycle hook.What this branch does
Adds (does not replace):
workspace_watcher.rs+workspace_watcher_registry.rs+workspace_watcher_router.rs— subscription-based directory change push, built onnotify-debouncer-fullwith one OS watcher per workspace and event fan-out (also covers office-file create detection).aionui-static-filecrate — static-file service with a pluggable asyncAccessGuard(Range 206, ETag, 256 MB default size limit).aionui-conversation/src/routes_static_file.rs—GET /api/conversations/{id}/files/{*path}.MessageRoutertrait gains anon_disconnectcallback.Modifies:
service.rsremoves the post-write/-deletefileStream.contentUpdatebroadcast (that responsibility moves to the workspace watcher's modify events).Untouched:
watch_service.rsis left as-is (the oldfile_watcher+office_watchersstill run).Known issue: duplicate office watch
The new
EventDispatcheralready implements office fan-out (with_office_broadcaster). When the frontend subscribes to a workspace, the new system emitsworkspaceOfficeWatch.fileAddedautomatically. However the frontend still calls the legacy/api/fs/office-watch/startAPI, so the same workspace ends up with two OS watchers and may emit duplicate events.Follow-up required: the frontend must drop
/api/fs/office-watch/start|stopand rely on the new fan-out; once no caller remains,office_watchersinwatch_service.rscan be deleted.Architecture
Data flow: directory expand → push
WS protocol
workspace.subscribe{workspace: string, dirs?: string[], extensions?: string[]}workspace.unsubscribe{workspace: string, dirs?: string[], extensions?: string[]}workspace.changed{workspace: string, changes: [{path, kind}]}workspace.overflow{workspace: string}kind:create/modify/deletepath: relative to the workspace rootAuthorization: Bearer <token>header oraionui-sessioncookieFollow-up plan
Current state: the new workspace watcher already provides office fan-out, but the legacy
office_watchersinwatch_service.rsare still running. Frontend code that uses both ends up with two OS watchers per workspace.Steps:
/api/fs/office-watch/start|stopand relies on workspace-watcher fan-out.office_watcherscode path inwatch_service.rs.file_watcher(single-file watch) can also be replaced by the workspace watcher.Files changed
New crate:
crates/aionui-static-file/— static-file service with pluggable AccessGuardNew files:
crates/aionui-file/src/workspace_watcher.rs—SharedWorkspaceWatcher,GitignoreFilter,EventDispatcher,WorkspaceWatchManagercrates/aionui-file/src/workspace_watcher_registry.rs—SubscriptionRegistrycrates/aionui-file/src/workspace_watcher_router.rs—WorkspaceWatchRouter(implMessageRouter)crates/aionui-conversation/src/routes_static_file.rs— conversation static-file routeModified:
crates/aionui-realtime/src/router.rs— addsMessageRouter::on_disconnectcrates/aionui-realtime/src/handler.rs— callson_disconnecton WS closecrates/aionui-file/src/service.rs— drops broadcaster dependency and thefileStream.contentUpdatebroadcastcrates/aionui-file/src/lib.rs— module declarations / exportscrates/aionui-app/src/router/state.rs— wiresWorkspaceWatchRouterand the office broadcasterUntouched:
crates/aionui-file/src/watch_service.rs— legacyfile_watcher+office_watchersleft as-isTest plan
aionui-filetests passingaionui-realtimetests passingaionui-apptests passing (includingstatic_file_e2e)-D warnings)🤖 Generated with Claude Code