claude-mpm 5.4.55__py3-none-any.whl → 5.4.85__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md +405 -0
- claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +63 -241
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +109 -1925
- claude_mpm/agents/PM_INSTRUCTIONS.md +36 -9
- claude_mpm/cli/__init__.py +5 -1
- claude_mpm/cli/commands/agents.py +2 -4
- claude_mpm/cli/commands/agents_reconcile.py +197 -0
- claude_mpm/cli/commands/configure.py +620 -21
- claude_mpm/cli/commands/skills.py +166 -14
- claude_mpm/cli/executor.py +1 -0
- claude_mpm/cli/interactive/__init__.py +10 -0
- claude_mpm/cli/interactive/agent_wizard.py +30 -50
- claude_mpm/cli/interactive/questionary_styles.py +65 -0
- claude_mpm/cli/interactive/skill_selector.py +481 -0
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/startup.py +223 -388
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -2
- claude_mpm/core/interactive_session.py +7 -7
- claude_mpm/core/output_style_manager.py +21 -13
- claude_mpm/core/unified_config.py +50 -8
- claude_mpm/core/unified_paths.py +30 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +8 -0
- claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
- claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +7 -4
- claude_mpm/services/agents/startup_sync.py +5 -2
- claude_mpm/services/pm_skills_deployer.py +4 -0
- claude_mpm/services/skills/git_skill_source_manager.py +24 -8
- claude_mpm/services/skills/selective_skill_deployer.py +82 -83
- claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +79 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +178 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
- claude_mpm/skills/bundled/collaboration/git-worktrees.md +317 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +112 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +146 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
- claude_mpm/skills/bundled/collaboration/stacked-prs.md +251 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +81 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +152 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +131 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +325 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +499 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/INTEGRATION.md +611 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/README.md +596 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/SKILL.md +260 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/examples/nextjs-env-structure.md +315 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/frameworks.md +436 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/security.md +433 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/synchronization.md +452 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/troubleshooting.md +404 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/validation.md +420 -0
- claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +86 -0
- claude_mpm/skills/bundled/main/internal-comms/SKILL.md +43 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
- claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +160 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
- claude_mpm/skills/bundled/main/skill-creator/SKILL.md +189 -0
- claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
- claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
- claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
- claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
- claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
- claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
- claude_mpm/skills/bundled/pm/pm-bug-reporting/pm-bug-reporting.md +248 -0
- claude_mpm/skills/bundled/pm/pm-delegation-patterns/SKILL.md +167 -0
- claude_mpm/skills/bundled/pm/pm-git-file-tracking/SKILL.md +113 -0
- claude_mpm/skills/bundled/pm/pm-pr-workflow/SKILL.md +124 -0
- claude_mpm/skills/bundled/pm/pm-teaching-mode/SKILL.md +657 -0
- claude_mpm/skills/bundled/pm/pm-ticketing-integration/SKILL.md +154 -0
- claude_mpm/skills/bundled/pm/pm-verification-protocols/SKILL.md +198 -0
- claude_mpm/skills/bundled/react/flexlayout-react.md +742 -0
- claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
- claude_mpm/skills/bundled/tauri/tauri-async-patterns.md +495 -0
- claude_mpm/skills/bundled/tauri/tauri-build-deploy.md +599 -0
- claude_mpm/skills/bundled/tauri/tauri-command-patterns.md +535 -0
- claude_mpm/skills/bundled/tauri/tauri-error-handling.md +613 -0
- claude_mpm/skills/bundled/tauri/tauri-event-system.md +648 -0
- claude_mpm/skills/bundled/tauri/tauri-file-system.md +673 -0
- claude_mpm/skills/bundled/tauri/tauri-frontend-integration.md +767 -0
- claude_mpm/skills/bundled/tauri/tauri-performance.md +669 -0
- claude_mpm/skills/bundled/tauri/tauri-state-management.md +573 -0
- claude_mpm/skills/bundled/tauri/tauri-testing.md +384 -0
- claude_mpm/skills/bundled/tauri/tauri-window-management.md +628 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +119 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
- claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/SKILL.md +458 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/examples/example-inspection-report.md +411 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/references/assertion-quality.md +317 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/references/inspection-checklist.md +270 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/references/red-flags.md +436 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +140 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
- claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +184 -0
- claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
- claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
- claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
- claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
- claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
- claude_mpm/utils/agent_dependency_loader.py +103 -4
- claude_mpm/utils/robust_installer.py +45 -24
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/METADATA +47 -23
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/RECORD +159 -47
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.55.dist-info → claude_mpm-5.4.85.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tauri-file-system
|
|
3
|
+
description: Safe file system operations in Tauri including path validation, file dialogs, directory operations, and secure file access patterns
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: development
|
|
6
|
+
author: Claude MPM Team
|
|
7
|
+
license: MIT
|
|
8
|
+
progressive_disclosure:
|
|
9
|
+
entry_point:
|
|
10
|
+
summary: "Secure file operations: path validation, scoped access, file dialogs, directory management, safe read/write patterns"
|
|
11
|
+
when_to_use: "When implementing file operations, document management, or any file system access in Tauri apps"
|
|
12
|
+
quick_start: "1. Configure fs allowlist 2. Validate paths 3. Use app directories 4. Implement dialogs 5. Handle errors"
|
|
13
|
+
context_limit: 600
|
|
14
|
+
tags:
|
|
15
|
+
- tauri
|
|
16
|
+
- filesystem
|
|
17
|
+
- security
|
|
18
|
+
- file-operations
|
|
19
|
+
- path-validation
|
|
20
|
+
requires_tools: []
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Tauri File System Operations
|
|
24
|
+
|
|
25
|
+
## Security Configuration
|
|
26
|
+
|
|
27
|
+
### Allowlist Setup
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
// src-tauri/tauri.conf.json
|
|
31
|
+
{
|
|
32
|
+
"tauri": {
|
|
33
|
+
"allowlist": {
|
|
34
|
+
"fs": {
|
|
35
|
+
"all": false,
|
|
36
|
+
"readFile": true,
|
|
37
|
+
"writeFile": true,
|
|
38
|
+
"readDir": true,
|
|
39
|
+
"createDir": true,
|
|
40
|
+
"removeFile": true,
|
|
41
|
+
"removeDir": true,
|
|
42
|
+
"renameFile": true,
|
|
43
|
+
"exists": true,
|
|
44
|
+
"scope": [
|
|
45
|
+
"$APPDATA/**",
|
|
46
|
+
"$DOCUMENT/**",
|
|
47
|
+
"$DOWNLOAD/**",
|
|
48
|
+
"$HOME/Documents/**"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Path Variables**:
|
|
57
|
+
- `$APPDATA` - Application data directory
|
|
58
|
+
- `$DOCUMENT` - User documents directory
|
|
59
|
+
- `$DOWNLOAD` - User downloads directory
|
|
60
|
+
- `$HOME` - User home directory
|
|
61
|
+
- `$TEMP` - Temporary directory
|
|
62
|
+
|
|
63
|
+
## Safe Path Handling
|
|
64
|
+
|
|
65
|
+
### Path Validation Pattern
|
|
66
|
+
|
|
67
|
+
```rust
|
|
68
|
+
use std::path::{Path, PathBuf};
|
|
69
|
+
|
|
70
|
+
fn validate_path(base_dir: &Path, user_path: &str) -> Result<PathBuf, String> {
|
|
71
|
+
// Resolve the path
|
|
72
|
+
let full_path = base_dir.join(user_path);
|
|
73
|
+
|
|
74
|
+
// Canonicalize to resolve .. and symlinks
|
|
75
|
+
let canonical = full_path.canonicalize()
|
|
76
|
+
.map_err(|e| format!("Invalid path: {}", e))?;
|
|
77
|
+
|
|
78
|
+
// CRITICAL: Ensure path is within base directory
|
|
79
|
+
if !canonical.starts_with(base_dir) {
|
|
80
|
+
return Err("Path traversal attempt detected".to_string());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Ok(canonical)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[tauri::command]
|
|
87
|
+
async fn read_app_file(
|
|
88
|
+
filename: String,
|
|
89
|
+
app: tauri::AppHandle,
|
|
90
|
+
) -> Result<String, String> {
|
|
91
|
+
let app_dir = app.path_resolver()
|
|
92
|
+
.app_data_dir()
|
|
93
|
+
.ok_or("Failed to get app data dir")?;
|
|
94
|
+
|
|
95
|
+
let safe_path = validate_path(&app_dir, &filename)?;
|
|
96
|
+
|
|
97
|
+
tokio::fs::read_to_string(safe_path)
|
|
98
|
+
.await
|
|
99
|
+
.map_err(|e| e.to_string())
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### App Directory Helpers
|
|
104
|
+
|
|
105
|
+
```rust
|
|
106
|
+
use tauri::Manager;
|
|
107
|
+
|
|
108
|
+
#[tauri::command]
|
|
109
|
+
async fn get_app_paths(app: tauri::AppHandle) -> Result<AppPaths, String> {
|
|
110
|
+
let resolver = app.path_resolver();
|
|
111
|
+
|
|
112
|
+
Ok(AppPaths {
|
|
113
|
+
app_data: resolver.app_data_dir()
|
|
114
|
+
.map(|p| p.to_string_lossy().to_string()),
|
|
115
|
+
app_config: resolver.app_config_dir()
|
|
116
|
+
.map(|p| p.to_string_lossy().to_string()),
|
|
117
|
+
app_cache: resolver.app_cache_dir()
|
|
118
|
+
.map(|p| p.to_string_lossy().to_string()),
|
|
119
|
+
app_log: resolver.app_log_dir()
|
|
120
|
+
.map(|p| p.to_string_lossy().to_string()),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[derive(serde::Serialize)]
|
|
125
|
+
struct AppPaths {
|
|
126
|
+
app_data: Option<String>,
|
|
127
|
+
app_config: Option<String>,
|
|
128
|
+
app_cache: Option<String>,
|
|
129
|
+
app_log: Option<String>,
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## File Operations
|
|
134
|
+
|
|
135
|
+
### Reading Files
|
|
136
|
+
|
|
137
|
+
```rust
|
|
138
|
+
use tokio::fs;
|
|
139
|
+
|
|
140
|
+
#[tauri::command]
|
|
141
|
+
async fn read_document(
|
|
142
|
+
filename: String,
|
|
143
|
+
app: tauri::AppHandle,
|
|
144
|
+
) -> Result<String, String> {
|
|
145
|
+
let app_data = app.path_resolver()
|
|
146
|
+
.app_data_dir()
|
|
147
|
+
.ok_or("Failed to get app data dir")?;
|
|
148
|
+
|
|
149
|
+
let file_path = validate_path(&app_data, &filename)?;
|
|
150
|
+
|
|
151
|
+
// Check if file exists
|
|
152
|
+
if !file_path.exists() {
|
|
153
|
+
return Err(format!("File not found: {}", filename));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Read file
|
|
157
|
+
fs::read_to_string(file_path)
|
|
158
|
+
.await
|
|
159
|
+
.map_err(|e| format!("Failed to read file: {}", e))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[tauri::command]
|
|
163
|
+
async fn read_binary_file(
|
|
164
|
+
filename: String,
|
|
165
|
+
app: tauri::AppHandle,
|
|
166
|
+
) -> Result<Vec<u8>, String> {
|
|
167
|
+
let app_data = app.path_resolver()
|
|
168
|
+
.app_data_dir()
|
|
169
|
+
.ok_or("Failed to get app data dir")?;
|
|
170
|
+
|
|
171
|
+
let file_path = validate_path(&app_data, &filename)?;
|
|
172
|
+
|
|
173
|
+
fs::read(file_path)
|
|
174
|
+
.await
|
|
175
|
+
.map_err(|e| e.to_string())
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Writing Files
|
|
180
|
+
|
|
181
|
+
```rust
|
|
182
|
+
#[tauri::command]
|
|
183
|
+
async fn save_document(
|
|
184
|
+
filename: String,
|
|
185
|
+
content: String,
|
|
186
|
+
app: tauri::AppHandle,
|
|
187
|
+
) -> Result<(), String> {
|
|
188
|
+
let app_data = app.path_resolver()
|
|
189
|
+
.app_data_dir()
|
|
190
|
+
.ok_or("Failed to get app data dir")?;
|
|
191
|
+
|
|
192
|
+
// Ensure directory exists
|
|
193
|
+
fs::create_dir_all(&app_data)
|
|
194
|
+
.await
|
|
195
|
+
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
|
196
|
+
|
|
197
|
+
let file_path = validate_path(&app_data, &filename)?;
|
|
198
|
+
|
|
199
|
+
// Write file
|
|
200
|
+
fs::write(file_path, content)
|
|
201
|
+
.await
|
|
202
|
+
.map_err(|e| format!("Failed to write file: {}", e))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[tauri::command]
|
|
206
|
+
async fn append_to_file(
|
|
207
|
+
filename: String,
|
|
208
|
+
content: String,
|
|
209
|
+
app: tauri::AppHandle,
|
|
210
|
+
) -> Result<(), String> {
|
|
211
|
+
use tokio::io::AsyncWriteExt;
|
|
212
|
+
|
|
213
|
+
let app_data = app.path_resolver()
|
|
214
|
+
.app_data_dir()
|
|
215
|
+
.ok_or("Failed to get app data dir")?;
|
|
216
|
+
|
|
217
|
+
let file_path = validate_path(&app_data, &filename)?;
|
|
218
|
+
|
|
219
|
+
let mut file = fs::OpenOptions::new()
|
|
220
|
+
.append(true)
|
|
221
|
+
.create(true)
|
|
222
|
+
.open(file_path)
|
|
223
|
+
.await
|
|
224
|
+
.map_err(|e| e.to_string())?;
|
|
225
|
+
|
|
226
|
+
file.write_all(content.as_bytes())
|
|
227
|
+
.await
|
|
228
|
+
.map_err(|e| e.to_string())?;
|
|
229
|
+
|
|
230
|
+
Ok(())
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Directory Operations
|
|
235
|
+
|
|
236
|
+
### Listing Directories
|
|
237
|
+
|
|
238
|
+
```rust
|
|
239
|
+
#[derive(serde::Serialize)]
|
|
240
|
+
struct FileEntry {
|
|
241
|
+
name: String,
|
|
242
|
+
path: String,
|
|
243
|
+
is_dir: bool,
|
|
244
|
+
size: u64,
|
|
245
|
+
modified: Option<u64>,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[tauri::command]
|
|
249
|
+
async fn list_directory(
|
|
250
|
+
directory: String,
|
|
251
|
+
app: tauri::AppHandle,
|
|
252
|
+
) -> Result<Vec<FileEntry>, String> {
|
|
253
|
+
let app_data = app.path_resolver()
|
|
254
|
+
.app_data_dir()
|
|
255
|
+
.ok_or("Failed to get app data dir")?;
|
|
256
|
+
|
|
257
|
+
let dir_path = validate_path(&app_data, &directory)?;
|
|
258
|
+
|
|
259
|
+
let mut entries = Vec::new();
|
|
260
|
+
let mut read_dir = fs::read_dir(dir_path)
|
|
261
|
+
.await
|
|
262
|
+
.map_err(|e| e.to_string())?;
|
|
263
|
+
|
|
264
|
+
while let Some(entry) = read_dir.next_entry()
|
|
265
|
+
.await
|
|
266
|
+
.map_err(|e| e.to_string())? {
|
|
267
|
+
|
|
268
|
+
let metadata = entry.metadata()
|
|
269
|
+
.await
|
|
270
|
+
.map_err(|e| e.to_string())?;
|
|
271
|
+
|
|
272
|
+
let modified = metadata.modified()
|
|
273
|
+
.ok()
|
|
274
|
+
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
|
275
|
+
.map(|d| d.as_secs());
|
|
276
|
+
|
|
277
|
+
entries.push(FileEntry {
|
|
278
|
+
name: entry.file_name().to_string_lossy().to_string(),
|
|
279
|
+
path: entry.path().to_string_lossy().to_string(),
|
|
280
|
+
is_dir: metadata.is_dir(),
|
|
281
|
+
size: metadata.len(),
|
|
282
|
+
modified,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Ok(entries)
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Creating Directories
|
|
291
|
+
|
|
292
|
+
```rust
|
|
293
|
+
#[tauri::command]
|
|
294
|
+
async fn create_directory(
|
|
295
|
+
directory: String,
|
|
296
|
+
app: tauri::AppHandle,
|
|
297
|
+
) -> Result<(), String> {
|
|
298
|
+
let app_data = app.path_resolver()
|
|
299
|
+
.app_data_dir()
|
|
300
|
+
.ok_or("Failed to get app data dir")?;
|
|
301
|
+
|
|
302
|
+
let dir_path = validate_path(&app_data, &directory)?;
|
|
303
|
+
|
|
304
|
+
fs::create_dir_all(dir_path)
|
|
305
|
+
.await
|
|
306
|
+
.map_err(|e| format!("Failed to create directory: {}", e))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[tauri::command]
|
|
310
|
+
async fn remove_directory(
|
|
311
|
+
directory: String,
|
|
312
|
+
recursive: bool,
|
|
313
|
+
app: tauri::AppHandle,
|
|
314
|
+
) -> Result<(), String> {
|
|
315
|
+
let app_data = app.path_resolver()
|
|
316
|
+
.app_data_dir()
|
|
317
|
+
.ok_or("Failed to get app data dir")?;
|
|
318
|
+
|
|
319
|
+
let dir_path = validate_path(&app_data, &directory)?;
|
|
320
|
+
|
|
321
|
+
if recursive {
|
|
322
|
+
fs::remove_dir_all(dir_path)
|
|
323
|
+
.await
|
|
324
|
+
.map_err(|e| e.to_string())
|
|
325
|
+
} else {
|
|
326
|
+
fs::remove_dir(dir_path)
|
|
327
|
+
.await
|
|
328
|
+
.map_err(|e| e.to_string())
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## File Dialogs
|
|
334
|
+
|
|
335
|
+
### Open File Dialog
|
|
336
|
+
|
|
337
|
+
```rust
|
|
338
|
+
use tauri::api::dialog::FileDialogBuilder;
|
|
339
|
+
|
|
340
|
+
#[tauri::command]
|
|
341
|
+
async fn select_file() -> Result<Option<String>, String> {
|
|
342
|
+
let result = tokio::task::spawn_blocking(|| {
|
|
343
|
+
FileDialogBuilder::new()
|
|
344
|
+
.add_filter("Text Files", &["txt", "md", "json"])
|
|
345
|
+
.add_filter("All Files", &["*"])
|
|
346
|
+
.set_title("Select a file")
|
|
347
|
+
.pick_file()
|
|
348
|
+
})
|
|
349
|
+
.await
|
|
350
|
+
.map_err(|e| e.to_string())?;
|
|
351
|
+
|
|
352
|
+
Ok(result.map(|p| p.to_string_lossy().to_string()))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[tauri::command]
|
|
356
|
+
async fn select_multiple_files() -> Result<Vec<String>, String> {
|
|
357
|
+
let result = tokio::task::spawn_blocking(|| {
|
|
358
|
+
FileDialogBuilder::new()
|
|
359
|
+
.add_filter("Documents", &["txt", "pdf", "doc", "docx"])
|
|
360
|
+
.pick_files()
|
|
361
|
+
})
|
|
362
|
+
.await
|
|
363
|
+
.map_err(|e| e.to_string())?;
|
|
364
|
+
|
|
365
|
+
Ok(result.map(|files| {
|
|
366
|
+
files.iter()
|
|
367
|
+
.map(|p| p.to_string_lossy().to_string())
|
|
368
|
+
.collect()
|
|
369
|
+
}).unwrap_or_default())
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Save File Dialog
|
|
374
|
+
|
|
375
|
+
```rust
|
|
376
|
+
#[tauri::command]
|
|
377
|
+
async fn save_file_dialog(
|
|
378
|
+
default_name: String,
|
|
379
|
+
content: String,
|
|
380
|
+
) -> Result<Option<String>, String> {
|
|
381
|
+
let path = tokio::task::spawn_blocking(move || {
|
|
382
|
+
FileDialogBuilder::new()
|
|
383
|
+
.set_file_name(&default_name)
|
|
384
|
+
.add_filter("Text Files", &["txt"])
|
|
385
|
+
.save_file()
|
|
386
|
+
})
|
|
387
|
+
.await
|
|
388
|
+
.map_err(|e| e.to_string())?;
|
|
389
|
+
|
|
390
|
+
if let Some(file_path) = path {
|
|
391
|
+
tokio::fs::write(&file_path, content)
|
|
392
|
+
.await
|
|
393
|
+
.map_err(|e| e.to_string())?;
|
|
394
|
+
|
|
395
|
+
Ok(Some(file_path.to_string_lossy().to_string()))
|
|
396
|
+
} else {
|
|
397
|
+
Ok(None)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Folder Selection Dialog
|
|
403
|
+
|
|
404
|
+
```rust
|
|
405
|
+
#[tauri::command]
|
|
406
|
+
async fn select_folder() -> Result<Option<String>, String> {
|
|
407
|
+
let result = tokio::task::spawn_blocking(|| {
|
|
408
|
+
FileDialogBuilder::new()
|
|
409
|
+
.set_title("Select a folder")
|
|
410
|
+
.pick_folder()
|
|
411
|
+
})
|
|
412
|
+
.await
|
|
413
|
+
.map_err(|e| e.to_string())?;
|
|
414
|
+
|
|
415
|
+
Ok(result.map(|p| p.to_string_lossy().to_string()))
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Advanced File Operations
|
|
420
|
+
|
|
421
|
+
### File Watching
|
|
422
|
+
|
|
423
|
+
```rust
|
|
424
|
+
use notify::{Watcher, RecursiveMode, Event};
|
|
425
|
+
|
|
426
|
+
#[tauri::command]
|
|
427
|
+
async fn watch_file(
|
|
428
|
+
filepath: String,
|
|
429
|
+
window: tauri::Window,
|
|
430
|
+
app: tauri::AppHandle,
|
|
431
|
+
) -> Result<(), String> {
|
|
432
|
+
let app_data = app.path_resolver()
|
|
433
|
+
.app_data_dir()
|
|
434
|
+
.ok_or("Failed to get app data dir")?;
|
|
435
|
+
|
|
436
|
+
let safe_path = validate_path(&app_data, &filepath)?;
|
|
437
|
+
|
|
438
|
+
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
|
|
439
|
+
|
|
440
|
+
// Create watcher
|
|
441
|
+
let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
|
|
442
|
+
if let Ok(event) = res {
|
|
443
|
+
let _ = tx.blocking_send(event);
|
|
444
|
+
}
|
|
445
|
+
}).map_err(|e| e.to_string())?;
|
|
446
|
+
|
|
447
|
+
watcher.watch(&safe_path, RecursiveMode::NonRecursive)
|
|
448
|
+
.map_err(|e| e.to_string())?;
|
|
449
|
+
|
|
450
|
+
// Spawn task to handle events
|
|
451
|
+
tokio::spawn(async move {
|
|
452
|
+
while let Some(event) = rx.recv().await {
|
|
453
|
+
let _ = window.emit("file-changed", event);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
Ok(())
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Atomic File Operations
|
|
462
|
+
|
|
463
|
+
```rust
|
|
464
|
+
use tokio::fs;
|
|
465
|
+
use uuid::Uuid;
|
|
466
|
+
|
|
467
|
+
#[tauri::command]
|
|
468
|
+
async fn atomic_write(
|
|
469
|
+
filename: String,
|
|
470
|
+
content: String,
|
|
471
|
+
app: tauri::AppHandle,
|
|
472
|
+
) -> Result<(), String> {
|
|
473
|
+
let app_data = app.path_resolver()
|
|
474
|
+
.app_data_dir()
|
|
475
|
+
.ok_or("Failed to get app data dir")?;
|
|
476
|
+
|
|
477
|
+
let target_path = validate_path(&app_data, &filename)?;
|
|
478
|
+
|
|
479
|
+
// Write to temporary file
|
|
480
|
+
let temp_filename = format!("{}.tmp.{}", filename, Uuid::new_v4());
|
|
481
|
+
let temp_path = validate_path(&app_data, &temp_filename)?;
|
|
482
|
+
|
|
483
|
+
fs::write(&temp_path, content)
|
|
484
|
+
.await
|
|
485
|
+
.map_err(|e| format!("Failed to write temp file: {}", e))?;
|
|
486
|
+
|
|
487
|
+
// Atomic rename
|
|
488
|
+
fs::rename(temp_path, target_path)
|
|
489
|
+
.await
|
|
490
|
+
.map_err(|e| format!("Failed to rename file: {}", e))?;
|
|
491
|
+
|
|
492
|
+
Ok(())
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Batch File Operations
|
|
497
|
+
|
|
498
|
+
```rust
|
|
499
|
+
#[derive(serde::Deserialize)]
|
|
500
|
+
struct BatchOperation {
|
|
501
|
+
operation: String, // "copy", "move", "delete"
|
|
502
|
+
source: String,
|
|
503
|
+
destination: Option<String>,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[tauri::command]
|
|
507
|
+
async fn batch_file_operations(
|
|
508
|
+
operations: Vec<BatchOperation>,
|
|
509
|
+
app: tauri::AppHandle,
|
|
510
|
+
) -> Result<Vec<String>, String> {
|
|
511
|
+
let app_data = app.path_resolver()
|
|
512
|
+
.app_data_dir()
|
|
513
|
+
.ok_or("Failed to get app data dir")?;
|
|
514
|
+
|
|
515
|
+
let mut results = Vec::new();
|
|
516
|
+
|
|
517
|
+
for op in operations {
|
|
518
|
+
let source_path = validate_path(&app_data, &op.source)?;
|
|
519
|
+
|
|
520
|
+
let result = match op.operation.as_str() {
|
|
521
|
+
"copy" => {
|
|
522
|
+
if let Some(dest) = op.destination {
|
|
523
|
+
let dest_path = validate_path(&app_data, &dest)?;
|
|
524
|
+
fs::copy(source_path, dest_path)
|
|
525
|
+
.await
|
|
526
|
+
.map(|_| "Success".to_string())
|
|
527
|
+
.map_err(|e| e.to_string())
|
|
528
|
+
} else {
|
|
529
|
+
Err("Missing destination".to_string())
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
"move" => {
|
|
533
|
+
if let Some(dest) = op.destination {
|
|
534
|
+
let dest_path = validate_path(&app_data, &dest)?;
|
|
535
|
+
fs::rename(source_path, dest_path)
|
|
536
|
+
.await
|
|
537
|
+
.map(|_| "Success".to_string())
|
|
538
|
+
.map_err(|e| e.to_string())
|
|
539
|
+
} else {
|
|
540
|
+
Err("Missing destination".to_string())
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
"delete" => {
|
|
544
|
+
fs::remove_file(source_path)
|
|
545
|
+
.await
|
|
546
|
+
.map(|_| "Success".to_string())
|
|
547
|
+
.map_err(|e| e.to_string())
|
|
548
|
+
}
|
|
549
|
+
_ => Err(format!("Unknown operation: {}", op.operation))
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
results.push(result.unwrap_or_else(|e| format!("Error: {}", e)));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
Ok(results)
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Frontend Integration
|
|
560
|
+
|
|
561
|
+
### File Service Pattern
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
565
|
+
|
|
566
|
+
export class FileService {
|
|
567
|
+
async readFile(filename: string): Promise<string> {
|
|
568
|
+
return await invoke<string>('read_document', { filename });
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async saveFile(filename: string, content: string): Promise<void> {
|
|
572
|
+
await invoke('save_document', { filename, content });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async listFiles(directory: string): Promise<FileEntry[]> {
|
|
576
|
+
return await invoke<FileEntry[]>('list_directory', { directory });
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async selectFile(): Promise<string | null> {
|
|
580
|
+
return await invoke<string | null>('select_file');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async saveFileDialog(
|
|
584
|
+
defaultName: string,
|
|
585
|
+
content: string
|
|
586
|
+
): Promise<string | null> {
|
|
587
|
+
return await invoke<string | null>('save_file_dialog', {
|
|
588
|
+
defaultName,
|
|
589
|
+
content
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export const fileService = new FileService();
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## Best Practices
|
|
598
|
+
|
|
599
|
+
1. **Always validate paths** - Use `validate_path()` for all user inputs
|
|
600
|
+
2. **Use app directories** - Never hardcode paths, use path resolver
|
|
601
|
+
3. **Configure scopes strictly** - Only allow necessary directories
|
|
602
|
+
4. **Handle errors gracefully** - Provide meaningful error messages
|
|
603
|
+
5. **Use atomic writes** - For critical data, write to temp then rename
|
|
604
|
+
6. **Spawn blocking for dialogs** - File dialogs block thread
|
|
605
|
+
7. **Check file existence** - Before read/write operations
|
|
606
|
+
8. **Use relative paths** - Store relative to app directories
|
|
607
|
+
9. **Implement proper cleanup** - Remove temp files
|
|
608
|
+
10. **Test path traversal** - Ensure security with `../` attacks
|
|
609
|
+
|
|
610
|
+
## Security Checklist
|
|
611
|
+
|
|
612
|
+
- [ ] Allowlist configured with minimal permissions
|
|
613
|
+
- [ ] All paths validated with `starts_with()` check
|
|
614
|
+
- [ ] No hardcoded absolute paths
|
|
615
|
+
- [ ] User input paths go through `validate_path()`
|
|
616
|
+
- [ ] Scopes defined in tauri.conf.json
|
|
617
|
+
- [ ] File operations use tokio::fs (async)
|
|
618
|
+
- [ ] Error messages don't leak path information
|
|
619
|
+
- [ ] Temporary files cleaned up
|
|
620
|
+
- [ ] Symlink attacks prevented with canonicalize()
|
|
621
|
+
|
|
622
|
+
## Common Pitfalls
|
|
623
|
+
|
|
624
|
+
❌ **Not validating paths**:
|
|
625
|
+
```rust
|
|
626
|
+
// WRONG - path traversal vulnerability
|
|
627
|
+
#[tauri::command]
|
|
628
|
+
async fn read_file_unsafe(path: String) -> Result<String, String> {
|
|
629
|
+
tokio::fs::read_to_string(path).await.map_err(|e| e.to_string())
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// CORRECT - validate first
|
|
633
|
+
#[tauri::command]
|
|
634
|
+
async fn read_file_safe(
|
|
635
|
+
filename: String,
|
|
636
|
+
app: tauri::AppHandle,
|
|
637
|
+
) -> Result<String, String> {
|
|
638
|
+
let app_dir = app.path_resolver().app_data_dir().unwrap();
|
|
639
|
+
let safe_path = validate_path(&app_dir, &filename)?;
|
|
640
|
+
tokio::fs::read_to_string(safe_path).await.map_err(|e| e.to_string())
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
❌ **Using blocking fs in async**:
|
|
645
|
+
```rust
|
|
646
|
+
// WRONG - blocks async runtime
|
|
647
|
+
std::fs::read_to_string(path)?;
|
|
648
|
+
|
|
649
|
+
// CORRECT - use tokio::fs
|
|
650
|
+
tokio::fs::read_to_string(path).await?;
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
❌ **Not using spawn_blocking for dialogs**:
|
|
654
|
+
```rust
|
|
655
|
+
// WRONG - blocks async runtime
|
|
656
|
+
FileDialogBuilder::new().pick_file();
|
|
657
|
+
|
|
658
|
+
// CORRECT - spawn blocking
|
|
659
|
+
tokio::task::spawn_blocking(|| {
|
|
660
|
+
FileDialogBuilder::new().pick_file()
|
|
661
|
+
}).await?
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
## Summary
|
|
665
|
+
|
|
666
|
+
- **Always validate paths** - Prevent path traversal attacks
|
|
667
|
+
- **Use app directories** - Via path resolver, not hardcoded
|
|
668
|
+
- **Configure allowlist** - Minimum necessary permissions
|
|
669
|
+
- **Async file operations** - Use tokio::fs, not std::fs
|
|
670
|
+
- **File dialogs** - Spawn in blocking context
|
|
671
|
+
- **Error handling** - Provide user-friendly messages
|
|
672
|
+
- **Atomic operations** - Write to temp, then rename
|
|
673
|
+
- **Security first** - Validate, scope, sanitize all inputs
|