Skip to content

Commit bd534ae

Browse files
feat(static-file): add conversation workspace file serving with pluggable 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>
1 parent 0fc0216 commit bd534ae

17 files changed

Lines changed: 1099 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"crates/aionui-auth",
1111
"crates/aionui-system",
1212
"crates/aionui-file",
13+
"crates/aionui-static-file",
1314
"crates/aionui-office",
1415
"crates/aionui-shell",
1516
"crates/aionui-ai-agent",
@@ -39,6 +40,7 @@ aionui-runtime = { path = "crates/aionui-runtime" }
3940
aionui-auth = { path = "crates/aionui-auth" }
4041
aionui-system = { path = "crates/aionui-system" }
4142
aionui-file = { path = "crates/aionui-file" }
43+
aionui-static-file = { path = "crates/aionui-static-file" }
4244
aionui-office = { path = "crates/aionui-office" }
4345
aionui-shell = { path = "crates/aionui-shell" }
4446
aionui-ai-agent = { path = "crates/aionui-ai-agent" }
@@ -118,6 +120,9 @@ dirs = "6"
118120
# Regex
119121
regex = "1"
120122

123+
# Async stream utilities
124+
tokio-util = { version = "0.7", features = ["io"] }
125+
121126
# File system
122127
include_dir = "0.7"
123128
notify = "8"

crates/aionui-app/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ aionui-shell.workspace = true
2828
aionui-ai-agent.workspace = true
2929
aionui-mcp.workspace = true
3030
aionui-conversation.workspace = true
31+
aionui-static-file.workspace = true
3132
aionui-extension.workspace = true
3233
aionui-channel.workspace = true
3334
aionui-team.workspace = true

crates/aionui-app/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ use aionui_auth::{
3232
use aionui_channel::weixin_login_route;
3333
use aionui_channel::{ChannelRouterState, channel_routes};
3434
use aionui_common::OnConversationDelete;
35-
use aionui_conversation::{ConversationRouterState, conversation_ops_routes, conversation_routes};
35+
use aionui_conversation::{
36+
ConversationRouterState, StaticFileRouterState, conversation_ops_routes, conversation_routes,
37+
conversation_static_file_routes,
38+
};
3639
use aionui_cron::{CronRouterState, cron_routes};
3740
use aionui_db::{
3841
Database, IAcpSessionRepository, IAgentMetadataRepository, IConversationRepository, IUserRepository,
@@ -341,6 +344,7 @@ pub struct ModuleStates {
341344
pub cron: CronRouterState,
342345
pub office: OfficeRouterState,
343346
pub shell: ShellRouterState,
347+
pub static_file: StaticFileRouterState,
344348
pub assistant: AssistantRouterState,
345349
}
346350

@@ -529,6 +533,10 @@ pub fn create_router_with_all_state(services: &AppServices, states: ModuleStates
529533
let assistant_authenticated =
530534
assistant_routes(states.assistant).route_layer(from_fn_with_state(auth_mw_state.clone(), auth_middleware));
531535

536+
// Static file serving for conversation workspaces, protected by auth
537+
let static_file_authenticated = conversation_static_file_routes(states.static_file)
538+
.route_layer(from_fn_with_state(auth_mw_state.clone(), auth_middleware));
539+
532540
// Guide MCP diagnostic endpoint protected by auth middleware
533541
let guide_mcp_authenticated = Router::new()
534542
.route("/api/system/guide-mcp", get(guide_mcp_status))
@@ -563,6 +571,7 @@ pub fn create_router_with_all_state(services: &AppServices, states: ModuleStates
563571
.merge(office_authenticated)
564572
.merge(shell_authenticated)
565573
.merge(assistant_authenticated)
574+
.merge(static_file_authenticated)
566575
.merge(guide_mcp_authenticated);
567576

568577
// Conditionally merge WeChat login SSE route (feature-gated)

crates/aionui-app/src/state_builders.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use aionui_ai_agent::{AgentRouterState, AgentService, RemoteAgentRouterState, Re
44
use aionui_assistant::{AssistantRouterState, AssistantService, BuiltinAssistantRegistry};
55
use aionui_auth::extract_token_from_ws_headers;
66
use aionui_channel::ChannelRouterState;
7-
use aionui_conversation::{ConversationRouterState, ConversationService};
7+
use aionui_conversation::{ConversationRouterState, ConversationService, StaticFileRouterState};
8+
use aionui_static_file::StaticFileService;
89
use aionui_cron::{CronEventEmitter, CronRouterState};
910
use aionui_db::{
1011
IAcpSessionRepository, IAgentMetadataRepository, IAssistantOverrideRepository, IAssistantRepository,
@@ -85,9 +86,15 @@ pub async fn build_module_states(services: &AppServices) -> (ModuleStates, Chann
8586

8687
let agent_service = AgentService::new(services.agent_registry.clone(), services.data_dir.clone());
8788

89+
let conversation_state = build_conversation_state(services, Some(cron.cron_service.clone()));
90+
let static_file = StaticFileRouterState {
91+
conversation_service: conversation_state.service.clone(),
92+
static_file_service: Arc::new(StaticFileService::permissive()),
93+
};
94+
8895
let states = ModuleStates {
8996
system: build_system_state(services),
90-
conversation: build_conversation_state(services, Some(cron.cron_service.clone())),
97+
conversation: conversation_state,
9198
remote_agent: build_remote_agent_state(services),
9299
agent: AgentRouterState {
93100
agent_registry: services.agent_registry.clone(),
@@ -110,6 +117,7 @@ pub async fn build_module_states(services: &AppServices) -> (ModuleStates, Chann
110117
office: build_office_state(services),
111118
shell: build_shell_state(services),
112119
assistant,
120+
static_file,
113121
};
114122

115123
(states, channel_components)

0 commit comments

Comments
 (0)