Rust · 11085 bytes Raw Blame History
1 use anyhow::{Context, Result};
2 use dirs;
3 use serde::{Deserialize, Serialize};
4 use std::path::PathBuf;
5 use sysinfo::{System, SystemExt, DiskExt};
6 use tauri::command;
7 use fs2::free_space;
8
9 /// Safe storage configuration for volunteers
10 #[derive(Debug, Clone, Serialize, Deserialize)]
11 pub struct SafeStorageConfig {
12 /// User-selected folder path (NO partitioning)
13 pub folder_path: PathBuf,
14 /// Maximum storage to allocate in bytes
15 pub max_size_bytes: u64,
16 /// Warning threshold (80% by default)
17 pub warn_at_percent: f32,
18 /// Auto cleanup enabled
19 pub auto_cleanup_enabled: bool,
20 }
21
22 /// Current storage status
23 #[derive(Debug, Clone, Serialize, Deserialize)]
24 pub struct StorageStatus {
25 /// Currently used storage in bytes
26 pub used_bytes: u64,
27 /// Total allocated storage in bytes
28 pub total_allocated_bytes: u64,
29 /// Usage percentage
30 pub usage_percent: f32,
31 /// Number of chunks stored
32 pub chunks_count: u32,
33 /// Available space on disk
34 pub disk_available_bytes: u64,
35 /// Is approaching warning threshold
36 pub needs_attention: bool,
37 }
38
39 /// Storage folder validation result
40 #[derive(Debug, Clone, Serialize, Deserialize)]
41 pub struct FolderValidation {
42 pub is_valid: bool,
43 pub error_message: Option<String>,
44 pub available_space_bytes: u64,
45 pub recommended_max_size: u64,
46 pub warnings: Vec<String>,
47 }
48
49 impl Default for SafeStorageConfig {
50 fn default() -> Self {
51 let default_folder = dirs::document_dir()
52 .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")))
53 .join("ZephyrFS-Storage");
54
55 Self {
56 folder_path: default_folder,
57 max_size_bytes: 10 * 1024 * 1024 * 1024, // 10GB default
58 warn_at_percent: 80.0,
59 auto_cleanup_enabled: true,
60 }
61 }
62 }
63
64 /// Tauri command to get default storage suggestions
65 #[command]
66 pub fn get_default_storage_suggestions() -> Result<Vec<PathBuf>, String> {
67 let mut suggestions = Vec::new();
68
69 // Documents folder (safest)
70 if let Some(docs) = dirs::document_dir() {
71 suggestions.push(docs.join("ZephyrFS-Storage"));
72 }
73
74 // Desktop folder (visible)
75 if let Some(desktop) = dirs::desktop_dir() {
76 suggestions.push(desktop.join("ZephyrFS-Storage"));
77 }
78
79 // Home folder (traditional)
80 if let Some(home) = dirs::home_dir() {
81 suggestions.push(home.join("ZephyrFS-Storage"));
82 }
83
84 // Downloads folder (temporary files area)
85 if let Some(downloads) = dirs::download_dir() {
86 suggestions.push(downloads.join("ZephyrFS-Storage"));
87 }
88
89 Ok(suggestions)
90 }
91
92 /// Tauri command to validate a selected folder
93 #[command]
94 pub fn validate_storage_folder(folder_path: String) -> Result<FolderValidation, String> {
95 let path = PathBuf::from(&folder_path);
96 let mut warnings = Vec::new();
97
98 // Check if path exists or can be created
99 let parent = path.parent().unwrap_or(&path);
100 if !parent.exists() {
101 return Ok(FolderValidation {
102 is_valid: false,
103 error_message: Some("Parent directory does not exist".to_string()),
104 available_space_bytes: 0,
105 recommended_max_size: 0,
106 warnings,
107 });
108 }
109
110 // Check disk space
111 let available_space = match free_space(parent) {
112 Ok(space) => space,
113 Err(e) => {
114 return Ok(FolderValidation {
115 is_valid: false,
116 error_message: Some(format!("Cannot check disk space: {}", e)),
117 available_space_bytes: 0,
118 recommended_max_size: 0,
119 warnings,
120 });
121 }
122 };
123
124 // Safety checks
125 let path_str = folder_path.to_lowercase();
126
127 // Warn about system directories
128 if path_str.contains("/system") || path_str.contains("\\system32") ||
129 path_str.contains("/usr") || path_str.contains("/bin") ||
130 path_str.contains("program files") {
131 warnings.push("This appears to be a system directory. Please choose a safer location.".to_string());
132 }
133
134 // Warn about root directories
135 if path == PathBuf::from("/") || path == PathBuf::from("C:\\") {
136 return Ok(FolderValidation {
137 is_valid: false,
138 error_message: Some("Cannot use root directory for storage".to_string()),
139 available_space_bytes: 0,
140 recommended_max_size: 0,
141 warnings,
142 });
143 }
144
145 // Recommend conservative storage size (max 50% of available space)
146 let recommended_max = (available_space as f64 * 0.5) as u64;
147 let recommended_max = std::cmp::min(recommended_max, 100 * 1024 * 1024 * 1024); // Max 100GB
148
149 // Check minimum space requirement (1GB)
150 if available_space < 1024 * 1024 * 1024 {
151 warnings.push("Less than 1GB available space. Consider choosing a different location.".to_string());
152 }
153
154 Ok(FolderValidation {
155 is_valid: true,
156 error_message: None,
157 available_space_bytes: available_space,
158 recommended_max_size: recommended_max,
159 warnings,
160 })
161 }
162
163 /// Tauri command to create storage folder safely
164 #[command]
165 pub async fn create_storage_folder(config: SafeStorageConfig) -> Result<bool, String> {
166 // Ensure the folder exists
167 if !config.folder_path.exists() {
168 std::fs::create_dir_all(&config.folder_path)
169 .map_err(|e| format!("Failed to create directory: {}", e))?;
170 }
171
172 // Create subdirectory structure
173 let chunks_dir = config.folder_path.join("chunks");
174 let metadata_dir = config.folder_path.join("metadata");
175 let logs_dir = config.folder_path.join("logs");
176 let config_dir = config.folder_path.join("config");
177
178 std::fs::create_dir_all(&chunks_dir)
179 .map_err(|e| format!("Failed to create chunks directory: {}", e))?;
180 std::fs::create_dir_all(&metadata_dir)
181 .map_err(|e| format!("Failed to create metadata directory: {}", e))?;
182 std::fs::create_dir_all(&logs_dir)
183 .map_err(|e| format!("Failed to create logs directory: {}", e))?;
184 std::fs::create_dir_all(&config_dir)
185 .map_err(|e| format!("Failed to create config directory: {}", e))?;
186
187 // Save configuration
188 let config_file = config_dir.join("storage_config.json");
189 let config_json = serde_json::to_string_pretty(&config)
190 .map_err(|e| format!("Failed to serialize config: {}", e))?;
191
192 std::fs::write(&config_file, config_json)
193 .map_err(|e| format!("Failed to write config file: {}", e))?;
194
195 // Create README file for transparency
196 let readme_content = format!(
197 r#"# ZephyrFS Storage Folder
198
199 This folder contains encrypted chunks from the ZephyrFS network.
200
201 ## What's stored here:
202 - `/chunks/` - Encrypted file chunks (completely unreadable)
203 - `/metadata/` - Local indexing information
204 - `/logs/` - Activity logs for transparency
205 - `/config/` - Configuration files
206
207 ## Safety Information:
208 - All files in /chunks/ are encrypted and cannot be read by this computer
209 - No personal information from other users is stored here
210 - You can safely delete this entire folder to stop participating
211 - Storage limit: {} GB
212
213 ## Created: {}
214
215 For more information, visit: https://zephyrfs.org
216 "#,
217 config.max_size_bytes / (1024 * 1024 * 1024),
218 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
219 );
220
221 std::fs::write(config.folder_path.join("README.txt"), readme_content)
222 .map_err(|e| format!("Failed to write README: {}", e))?;
223
224 Ok(true)
225 }
226
227 /// Tauri command to get current storage status
228 #[command]
229 pub fn get_storage_status(folder_path: String) -> Result<StorageStatus, String> {
230 let path = PathBuf::from(&folder_path);
231
232 if !path.exists() {
233 return Err("Storage folder does not exist".to_string());
234 }
235
236 // Calculate used space by scanning chunks directory
237 let chunks_dir = path.join("chunks");
238 let mut used_bytes = 0u64;
239 let mut chunks_count = 0u32;
240
241 if chunks_dir.exists() {
242 match std::fs::read_dir(&chunks_dir) {
243 Ok(entries) => {
244 for entry in entries {
245 if let Ok(entry) = entry {
246 if let Ok(metadata) = entry.metadata() {
247 if metadata.is_file() {
248 used_bytes += metadata.len();
249 chunks_count += 1;
250 }
251 }
252 }
253 }
254 }
255 Err(_) => return Err("Cannot read chunks directory".to_string()),
256 }
257 }
258
259 // Get disk available space
260 let disk_available_bytes = free_space(&path)
261 .map_err(|e| format!("Cannot check disk space: {}", e))?;
262
263 // Load configuration to get total allocated
264 let config_file = path.join("config").join("storage_config.json");
265 let total_allocated_bytes = if config_file.exists() {
266 match std::fs::read_to_string(&config_file) {
267 Ok(content) => {
268 match serde_json::from_str::<SafeStorageConfig>(&content) {
269 Ok(config) => config.max_size_bytes,
270 Err(_) => 10 * 1024 * 1024 * 1024, // Default 10GB
271 }
272 }
273 Err(_) => 10 * 1024 * 1024 * 1024, // Default 10GB
274 }
275 } else {
276 10 * 1024 * 1024 * 1024 // Default 10GB
277 };
278
279 let usage_percent = if total_allocated_bytes > 0 {
280 (used_bytes as f64 / total_allocated_bytes as f64 * 100.0) as f32
281 } else {
282 0.0
283 };
284
285 let needs_attention = usage_percent > 80.0 || disk_available_bytes < 1024 * 1024 * 1024; // < 1GB
286
287 Ok(StorageStatus {
288 used_bytes,
289 total_allocated_bytes,
290 usage_percent,
291 chunks_count,
292 disk_available_bytes,
293 needs_attention,
294 })
295 }
296
297 /// Tauri command to safely remove all storage data
298 #[command]
299 pub async fn remove_storage_data(folder_path: String, confirm: bool) -> Result<bool, String> {
300 if !confirm {
301 return Err("Confirmation required to remove storage data".to_string());
302 }
303
304 let path = PathBuf::from(&folder_path);
305
306 if !path.exists() {
307 return Ok(true); // Already doesn't exist
308 }
309
310 // Safety check - ensure this looks like a ZephyrFS storage folder
311 let readme_path = path.join("README.txt");
312 if !readme_path.exists() {
313 return Err("This doesn't appear to be a ZephyrFS storage folder".to_string());
314 }
315
316 // Remove the entire directory
317 std::fs::remove_dir_all(&path)
318 .map_err(|e| format!("Failed to remove storage directory: {}", e))?;
319
320 Ok(true)
321 }
322
323 /// Format bytes to human-readable string
324 pub fn format_bytes(bytes: u64) -> String {
325 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
326 let mut size = bytes as f64;
327 let mut unit_index = 0;
328
329 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
330 size /= 1024.0;
331 unit_index += 1;
332 }
333
334 if unit_index == 0 {
335 format!("{} {}", size as u64, UNITS[unit_index])
336 } else {
337 format!("{:.2} {}", size, UNITS[unit_index])
338 }
339 }