Skip to main content

ri/fs/
mod.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # File System Module
21//! 
22//! This module provides a comprehensive file system abstraction for Ri, offering safe and reliable
23//! file operations with support for atomic writes, directory management, and structured data formats.
24//! 
25//! ## Key Components
26//! 
27//! - **RiFileSystem**: Public-facing file system class
28//! - **FileSystemImpl**: Internal file system implementation
29//! 
30//! ## Design Principles
31//! 
32//! 1. **Safe Operations**: All file operations are designed to be safe and reliable
33//! 2. **Atomic Writes**: Uses atomic write operations to prevent data corruption
34//! 3. **Directory Management**: Automatically creates necessary directories
35//! 4. **Structured Data Support**: Built-in support for JSON serialization and deserialization
36//! 5. **Category-Based Organization**: Organizes files into categories (logs, cache, reports, etc.)
37//! 6. **Error Handling**: Provides comprehensive error handling for all file operations
38//! 7. **Cloneable**: Designed to be easily cloned for use across different components
39//! 
40//! ## Usage
41//! 
42//! ```rust
43//! use ri::prelude::*;
44//! use std::path::PathBuf;
45//! 
46//! fn example() -> RiResult<()> {
47//!     // Create a file system with a project root
48//!     let project_root = PathBuf::from(".");
49//!     let fs = RiFileSystem::new_with_root(project_root);
50//!     
51//!     // Write text to a file
52//!     fs.atomic_write_text("example.txt", "Hello, Ri!")?;
53//!     
54//!     // Read text from a file
55//!     let content = fs.read_text("example.txt")?;
56//!     println!("File content: {}", content);
57//!     
58//!     // Write JSON to a file
59//!     let data = json!({"key": "value"});
60//!     fs.write_json("example.json", &data)?;
61//!     
62//!     // Read JSON from a file
63//!     let read_data: serde_json::Value = fs.read_json("example.json")?;
64//!     println!("JSON data: {:?}", read_data);
65//!     
66//!     // Get category directories
67//!     let logs_dir = fs.logs_dir();
68//!     println!("Logs directory: {:?}", logs_dir);
69//!     
70//!     Ok(())
71//! }
72//! ```
73
74use std::fs;
75use std::io::{Read, Write};
76use std::path::{Path, PathBuf};
77use std::fs::OpenOptions;
78use std::time::SystemTime;
79
80use crate::core::RiResult;
81use serde::de::DeserializeOwned;
82use serde::Serialize;
83
84/// Internal filesystem implementation.
85/// 
86/// This struct provides the internal implementation of the file system functionality, including
87/// directory management, file operations, and category-based organization.
88#[derive(Clone)]
89struct FileSystemImpl {
90    /// Project root directory
91    project_root: PathBuf,
92    /// Application data root directory
93    app_data_root: PathBuf,
94}
95
96impl FileSystemImpl {
97    /// Creates a new internal file system implementation with specified roots.
98    /// 
99    /// # Parameters
100    /// 
101    /// - `project_root`: The project root directory
102    /// - `app_data_root`: The application data root directory
103    /// 
104    /// # Returns
105    /// 
106    /// A new `FileSystemImpl` instance
107    fn new_with_roots(project_root: PathBuf, app_data_root: PathBuf) -> Self {
108        FileSystemImpl { project_root, app_data_root }
109    }
110
111    /// Creates a new internal file system implementation with a project root and default app data root.
112    /// 
113    /// The default app data root is created under the project root at `.dms`.
114    /// 
115    /// # Parameters
116    /// 
117    /// - `project_root`: The project root directory
118    /// 
119    /// # Returns
120    /// 
121    /// A new `FileSystemImpl` instance
122    fn new_with_root(project_root: PathBuf) -> Self {
123        // Default app data root under project root; can be overridden by core/config.
124        let app_data_root = project_root.join(".dms");
125        FileSystemImpl::new_with_roots(project_root, app_data_root)
126    }
127
128    /// Validates that a path is safe and within the allowed directories.
129    ///
130    /// This method prevents path traversal attacks by ensuring that the resolved
131    /// path is within the project root or app data root.
132    ///
133    /// # Parameters
134    ///
135    /// - `path`: The path to validate
136    ///
137    /// # Returns
138    ///
139    /// A `RiResult<PathBuf>` containing the canonicalized path if valid
140    fn validate_path(&self, path: &Path) -> RiResult<PathBuf> {
141        // Security: Check for symlink attacks
142        // 1. First canonicalize the allowed directories
143        let canonical_project = self.project_root.canonicalize()
144            .map_err(|e| crate::core::RiError::Other(format!("Project root canonicalization failed: {e}")))?;
145        
146        let canonical_app_data = self.app_data_root.canonicalize()
147            .map_err(|e| crate::core::RiError::Other(format!("App data root canonicalization failed: {e}")))?;
148        
149        // 2. For the target path, handle non-existent paths safely
150        let canonical_path = if path.exists() {
151            // Path exists - canonicalize it (this resolves symlinks)
152            path.canonicalize()
153                .map_err(|e| crate::core::RiError::Other(format!("Path canonicalization failed: {e}")))?
154        } else {
155            // Path doesn't exist - check parent directory for symlinks
156            let parent = path.parent().ok_or_else(|| {
157                crate::core::RiError::Other("Invalid path: no parent directory".to_string())
158            })?;
159            
160            if parent.exists() {
161                // Canonicalize parent to resolve any symlinks
162                let canonical_parent = parent.canonicalize()
163                    .map_err(|e| crate::core::RiError::Other(format!("Parent canonicalization failed: {e}")))?;
164                
165                // Reconstruct the path with canonicalized parent
166                let file_name = path.file_name().ok_or_else(|| {
167                    crate::core::RiError::Other("Invalid path: no file name".to_string())
168                })?;
169                
170                canonical_parent.join(file_name)
171            } else {
172                // Parent doesn't exist - this is an error for security
173                return Err(crate::core::RiError::Other(
174                    "Path validation failed: parent directory does not exist".to_string()
175                ));
176            }
177        };
178        
179        // 3. Check if the canonical path is within allowed directories
180        if !canonical_path.starts_with(&canonical_project) && 
181           !canonical_path.starts_with(&canonical_app_data) {
182            return Err(crate::core::RiError::Other(
183                "Path traversal detected: path is outside allowed directories".to_string()
184            ));
185        }
186        
187        // 4. Additional symlink check: ensure no symlink components in the path
188        // This prevents TOCTOU attacks where a symlink is created after validation
189        let mut current = canonical_path.clone();
190        while let Some(parent) = current.parent() {
191            if parent.join(current.file_name().unwrap_or_default()).exists() {
192                let metadata = std::fs::symlink_metadata(&current)
193                    .map_err(|e| crate::core::RiError::Other(format!("Symlink metadata check failed: {e}")))?;
194                
195                if metadata.file_type().is_symlink() {
196                    // Symlink found - verify it points to allowed directory
197                    let symlink_target = std::fs::read_link(&current)
198                        .map_err(|e| crate::core::RiError::Other(format!("Failed to read symlink: {e}")))?;
199                    
200                    let canonical_target = symlink_target.canonicalize()
201                        .map_err(|e| crate::core::RiError::Other(format!("Symlink target canonicalization failed: {e}")))?;
202                    
203                    if !canonical_target.starts_with(&canonical_project) && 
204                       !canonical_target.starts_with(&canonical_app_data) {
205                        return Err(crate::core::RiError::Other(
206                            "Symlink points outside allowed directories".to_string()
207                        ));
208                    }
209                }
210            }
211            current = parent.to_path_buf();
212        }
213        
214        Ok(canonical_path)
215    }
216
217    /// Safely resolves and validates a path relative to the project root.
218    ///
219    /// # Parameters
220    ///
221    /// - `path`: The relative or absolute path to resolve
222    ///
223    /// # Returns
224    ///
225    /// A `RiResult<PathBuf>` containing the resolved and validated path
226    fn resolve_and_validate_path(&self, path: &Path) -> RiResult<PathBuf> {
227        let resolved = if path.is_absolute() {
228            path.to_path_buf()
229        } else {
230            self.project_root.join(path)
231        };
232        
233        self.validate_path(&resolved)
234    }
235
236    /// Returns the project root directory.
237    /// 
238    /// # Returns
239    /// 
240    /// A reference to the project root path
241    fn project_root(&self) -> &Path {
242        &self.project_root
243    }
244
245    /// Safely creates a directory and all its parent directories if they don't exist.
246    /// 
247    /// # Parameters
248    /// 
249    /// - `path`: The path to the directory to create
250    /// 
251    /// # Returns
252    /// 
253    /// A `RiResult<PathBuf>` containing the created directory path
254    fn safe_mkdir(&self, path: &Path) -> RiResult<PathBuf> {
255        fs::create_dir_all(path).map_err(|e| crate::core::RiError::Other(format!("safe_mkdir failed: {e}")))?;
256        Ok(path.to_path_buf())
257    }
258
259    /// Ensures that the parent directory of a given path exists.
260    /// 
261    /// # Parameters
262    /// 
263    /// - `path`: The path whose parent directory should be ensured
264    /// 
265    /// # Returns
266    /// 
267    /// A `RiResult<PathBuf>` containing the parent directory path
268    fn ensure_parent_dir(&self, path: &Path) -> RiResult<PathBuf> {
269        if let Some(parent) = path.parent() {
270            self.safe_mkdir(parent)
271        } else {
272            Ok(self.project_root.clone())
273        }
274    }
275
276    /// Atomically writes text to a file.
277    /// 
278    /// This method writes to a temporary file first, then renames it to the target path,
279    /// ensuring that the write operation is atomic.
280    /// 
281    /// # Parameters
282    /// 
283    /// - `path`: The path to the file to write
284    /// - `text`: The text to write to the file
285    /// 
286    /// # Returns
287    /// 
288    /// A `RiResult<()>` indicating success or failure
289    fn atomic_write_text(&self, path: &Path, text: &str) -> RiResult<()> {
290        let validated_path = self.resolve_and_validate_path(path)?;
291        self.ensure_parent_dir(&validated_path)?;
292        let dir = validated_path.parent().unwrap_or_else(|| Path::new("."));
293        let ts = SystemTime::now()
294            .duration_since(SystemTime::UNIX_EPOCH)
295            .map_err(|e| crate::core::RiError::Other(format!("timestamp error: {e}")))?;
296        let tmp_name = format!(".tmp_{}_{}", ts.as_millis(), validated_path.file_name().and_then(|s| s.to_str()).unwrap_or("tmp"));
297        let tmp_path = dir.join(tmp_name);
298
299        {
300            let mut file = OpenOptions::new()
301                .write(true)
302                .create(true)
303                .truncate(true)
304                .open(&tmp_path)
305                .map_err(|e| crate::core::RiError::Other(format!("atomic_write_text open tmp failed: {e}")))?;
306            file.write_all(text.as_bytes())
307                .map_err(|e| crate::core::RiError::Other(format!("atomic_write_text write failed: {e}")))?;
308            file.sync_all()
309                .map_err(|e| crate::core::RiError::Other(format!("atomic_write_text sync failed: {e}")))?;
310        }
311
312        fs::rename(&tmp_path, &validated_path)
313            .map_err(|e| crate::core::RiError::Other(format!("atomic_write_text rename failed: {e}")))?;
314
315        Ok(())
316    }
317
318    /// Atomically writes bytes to a file.
319    /// 
320    /// This method writes to a temporary file first, then renames it to the target path,
321    /// ensuring that the write operation is atomic.
322    /// 
323    /// # Parameters
324    /// 
325    /// - `path`: The path to the file to write
326    /// - `data`: The bytes to write to the file
327    /// 
328    /// # Returns
329    /// 
330    /// A `RiResult<()>` indicating success or failure
331    fn atomic_write_bytes(&self, path: &Path, data: &[u8]) -> RiResult<()> {
332        let validated_path = self.resolve_and_validate_path(path)?;
333        self.ensure_parent_dir(&validated_path)?;
334        let dir = validated_path.parent().unwrap_or_else(|| Path::new("."));
335        let ts = SystemTime::now()
336            .duration_since(SystemTime::UNIX_EPOCH)
337            .map_err(|e| crate::core::RiError::Other(format!("timestamp error: {e}")))?;
338        let tmp_name = format!(".tmp_{}_{}", ts.as_millis(), validated_path.file_name().and_then(|s| s.to_str()).unwrap_or("tmp"));
339        let tmp_path = dir.join(tmp_name);
340
341        {
342            let mut file = OpenOptions::new()
343                .write(true)
344                .create(true)
345                .truncate(true)
346                .open(&tmp_path)
347                .map_err(|e| crate::core::RiError::Other(format!("atomic_write_bytes open tmp failed: {e}")))?;
348            file.write_all(data)
349                .map_err(|e| crate::core::RiError::Other(format!("atomic_write_bytes write failed: {e}")))?;
350            file.sync_all()
351                .map_err(|e| crate::core::RiError::Other(format!("atomic_write_bytes sync failed: {e}")))?;
352        }
353
354        fs::rename(&tmp_path, &validated_path)
355            .map_err(|e| crate::core::RiError::Other(format!("atomic_write_bytes rename failed: {e}")))?;
356
357        Ok(())
358    }
359
360    /// Reads text from a file.
361    /// 
362    /// # Parameters
363    /// 
364    /// - `path`: The path to the file to read
365    /// 
366    /// # Returns
367    /// 
368    /// A `RiResult<String>` containing the file content
369    fn read_text(&self, path: &Path) -> RiResult<String> {
370        let validated_path = self.resolve_and_validate_path(path)?;
371        let mut file = OpenOptions::new()
372            .read(true)
373            .open(&validated_path)
374            .map_err(|e| crate::core::RiError::Other(format!("read_text open failed: {e}")))?;
375        let mut buf = String::new();
376        file.read_to_string(&mut buf)
377            .map_err(|e| crate::core::RiError::Other(format!("read_text read failed: {e}")))?;
378        Ok(buf)
379    }
380
381    /// Returns the application data directory.
382    /// 
383    /// Ensures the directory exists before returning it.
384    /// 
385    /// # Returns
386    /// 
387    /// The application data directory path
388    fn app_dir(&self) -> PathBuf {
389        let _ = fs::create_dir_all(&self.app_data_root);
390        self.app_data_root.clone()
391    }
392
393    /// Returns a category-specific directory.
394    /// 
395    /// Ensures the directory exists before returning it.
396    /// 
397    /// # Parameters
398    /// 
399    /// - `name`: The name of the category
400    /// 
401    /// # Returns
402    /// 
403    /// The category directory path
404    fn category_dir(&self, name: &str) -> PathBuf {
405        let dir = self.app_dir().join(name);
406        let _ = fs::create_dir_all(&dir);
407        dir
408    }
409}
410
411/// Public-facing filesystem class.
412/// 
413/// This struct provides a comprehensive file system abstraction for Ri, offering safe and reliable
414/// file operations with support for atomic writes, directory management, and structured data formats.
415#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
416#[derive(Clone)]
417pub struct RiFileSystem {
418    /// Internal file system implementation
419    inner: FileSystemImpl,
420}
421
422impl RiFileSystem {
423    /// Creates a new file system with a project root and default app data root.
424    /// 
425    /// # Parameters
426    /// 
427    /// - `project_root`: The project root directory
428    /// 
429    /// # Returns
430    /// 
431    /// A new `RiFileSystem` instance
432    pub fn new_with_root(project_root: PathBuf) -> Self {
433        let inner = FileSystemImpl::new_with_root(project_root);
434        RiFileSystem { inner }
435    }
436
437    /// Creates a new file system with specified roots.
438    /// 
439    /// # Parameters
440    /// 
441    /// - `project_root`: The project root directory
442    /// - `app_data_root`: The application data root directory
443    /// 
444    /// # Returns
445    /// 
446    /// A new `RiFileSystem` instance
447    pub fn new_with_roots(project_root: PathBuf, app_data_root: PathBuf) -> Self {
448        let inner = FileSystemImpl::new_with_roots(project_root, app_data_root);
449        RiFileSystem { inner }
450    }
451
452    /// Creates a new file system with the current working directory as the project root.
453    /// 
454    /// # Returns
455    /// 
456    /// A `RiResult<Self>` containing the new `RiFileSystem` instance
457    pub fn new_auto_root() -> RiResult<Self> {
458        let cwd = std::env::current_dir()
459            .map_err(|e| crate::core::RiError::Other(format!("detect project root failed: {e}")))?;
460        Ok(Self::new_with_root(cwd))
461    }
462
463    /// Returns the project root directory.
464    /// 
465    /// # Returns
466    /// 
467    /// A reference to the project root path
468    pub fn project_root(&self) -> &Path {
469        self.inner.project_root()
470    }
471
472    /// Safely creates a directory and all its parent directories if they don't exist.
473    /// 
474    /// # Parameters
475    /// 
476    /// - `path`: The path to the directory to create
477    /// 
478    /// # Returns
479    /// 
480    /// A `RiResult<PathBuf>` containing the created directory path
481    pub fn safe_mkdir<P: AsRef<Path>>(&self, path: P) -> RiResult<PathBuf> {
482        self.inner.safe_mkdir(path.as_ref())
483    }
484
485    /// Ensures that the parent directory of a given path exists.
486    /// 
487    /// # Parameters
488    /// 
489    /// - `path`: The path whose parent directory should be ensured
490    /// 
491    /// # Returns
492    /// 
493    /// A `RiResult<PathBuf>` containing the parent directory path
494    pub fn ensure_parent_dir<P: AsRef<Path>>(&self, path: P) -> RiResult<PathBuf> {
495        self.inner.ensure_parent_dir(path.as_ref())
496    }
497
498    /// Atomically writes text to a file.
499    /// 
500    /// This method writes to a temporary file first, then renames it to the target path,
501    /// ensuring that the write operation is atomic.
502    /// 
503    /// # Parameters
504    /// 
505    /// - `path`: The path to the file to write
506    /// - `text`: The text to write to the file
507    /// 
508    /// # Returns
509    /// 
510    /// A `RiResult<()>` indicating success or failure
511    pub fn atomic_write_text<P: AsRef<Path>>(&self, path: P, text: &str) -> RiResult<()> {
512        self.inner.atomic_write_text(path.as_ref(), text)
513    }
514
515    /// Atomically writes bytes to a file.
516    /// 
517    /// This method writes to a temporary file first, then renames it to the target path,
518    /// ensuring that the write operation is atomic.
519    /// 
520    /// # Parameters
521    /// 
522    /// - `path`: The path to the file to write
523    /// - `data`: The bytes to write to the file
524    /// 
525    /// # Returns
526    /// 
527    /// A `RiResult<()>` indicating success or failure
528    pub fn atomic_write_bytes<P: AsRef<Path>>(&self, path: P, data: &[u8]) -> RiResult<()> {
529        self.inner.atomic_write_bytes(path.as_ref(), data)
530    }
531
532    /// Reads text from a file.
533    /// 
534    /// # Parameters
535    /// 
536    /// - `path`: The path to the file to read
537    /// 
538    /// # Returns
539    /// 
540    /// A `RiResult<String>` containing the file content
541    pub fn read_text<P: AsRef<Path>>(&self, path: P) -> RiResult<String> {
542        self.inner.read_text(path.as_ref())
543    }
544
545    /// Reads JSON from a file and deserializes it into a type.
546    /// 
547    /// # Parameters
548    /// 
549    /// - `path`: The path to the JSON file to read
550    /// 
551    /// # Type Parameters
552    /// 
553    /// - `T`: The type to deserialize the JSON into
554    /// 
555    /// # Returns
556    /// 
557    /// A `RiResult<T>` containing the deserialized data
558    pub fn read_json<P: AsRef<Path>, T: DeserializeOwned>(&self, path: P) -> RiResult<T> {
559        let text = self.read_text(path)?;
560        serde_json::from_str(&text)
561            .map_err(|e| crate::core::RiError::Other(format!("json read failed: {e}")))
562    }
563
564    /// Checks if a file or directory exists.
565    /// 
566    /// # Parameters
567    /// 
568    /// - `path`: The path to check
569    /// 
570    /// # Returns
571    /// 
572    /// `true` if the path exists, `false` otherwise
573    pub fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
574        path.as_ref().exists()
575    }
576
577    /// Removes a file.
578    /// 
579    /// # Security
580    /// 
581    /// This method validates the path to prevent path traversal attacks.
582    /// 
583    /// # Parameters
584    /// 
585    /// - `path`: The path to the file to remove
586    /// 
587    /// # Returns
588    /// 
589    /// A `RiResult<()>` indicating success or failure
590    pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> RiResult<()> {
591        let validated_path = self.inner.resolve_and_validate_path(path.as_ref())?;
592        match fs::remove_file(&validated_path) {
593            Ok(()) => Ok(()),
594            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
595            Err(e) => Err(crate::core::RiError::Other(format!("remove_file failed: {e}"))),
596        }
597    }
598
599    /// Removes a directory and all its contents.
600    /// 
601    /// # Security
602    /// 
603    /// This method validates the path to prevent path traversal attacks.
604    /// 
605    /// # Parameters
606    /// 
607    /// - `path`: The path to the directory to remove
608    /// 
609    /// # Returns
610    /// 
611    /// A `RiResult<()>` indicating success or failure
612    pub fn remove_dir_all<P: AsRef<Path>>(&self, path: P) -> RiResult<()> {
613        let validated_path = self.inner.resolve_and_validate_path(path.as_ref())?;
614        match fs::remove_dir_all(&validated_path) {
615            Ok(()) => Ok(()),
616            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
617            Err(e) => Err(crate::core::RiError::Other(format!("remove_dir_all failed: {e}"))),
618        }
619    }
620
621    /// Copies a file from one path to another.
622    /// 
623    /// # Security
624    /// 
625    /// This method validates both source and destination paths to prevent
626    /// path traversal attacks.
627    /// 
628    /// # Parameters
629    /// 
630    /// - `from`: The source file path
631    /// - `to`: The destination file path
632    /// 
633    /// # Returns
634    /// 
635    /// A `RiResult<()>` indicating success or failure
636    pub fn copy_file<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> RiResult<()> {
637        let src = self.inner.resolve_and_validate_path(from.as_ref())?;
638        let dst = self.inner.resolve_and_validate_path(to.as_ref())?;
639        
640        if let Some(parent) = dst.parent() {
641            self.safe_mkdir(parent)?;
642        }
643        fs::copy(&src, &dst)
644            .map_err(|e| crate::core::RiError::Other(format!("copy_file failed: {e}")))?;
645        Ok(())
646    }
647
648    /// Appends text to a file.
649    /// 
650    /// # Security
651    /// 
652    /// This method validates the path to prevent path traversal attacks.
653    /// 
654    /// # Parameters
655    /// 
656    /// - `path`: The path to the file to append to
657    /// - `text`: The text to append to the file
658    /// 
659    /// # Returns
660    /// 
661    /// A `RiResult<()>` indicating success or failure
662    pub fn append_text<P: AsRef<Path>>(&self, path: P, text: &str) -> RiResult<()> {
663        use std::io::Write as _;
664
665        let validated_path = self.inner.resolve_and_validate_path(path.as_ref())?;
666        self.ensure_parent_dir(&validated_path)?;
667        let mut file = OpenOptions::new()
668            .create(true)
669            .append(true)
670            .open(&validated_path)
671            .map_err(|e| crate::core::RiError::Other(format!("append_text open failed: {e}")))?;
672        file.write_all(text.as_bytes())
673            .map_err(|e| crate::core::RiError::Other(format!("append_text write failed: {e}")))?;
674        file.flush()
675            .map_err(|e| crate::core::RiError::Other(format!("append_text flush failed: {e}")))?;
676        Ok(())
677    }
678
679    /// Writes a JSON value to a file.
680    /// 
681    /// # Parameters
682    /// 
683    /// - `path`: The path to the file to write
684    /// - `value`: The value to serialize and write
685    /// 
686    /// # Type Parameters
687    /// 
688    /// - `T`: The type of the value to serialize
689    /// 
690    /// # Returns
691    /// 
692    /// A `RiResult<()>` indicating success or failure
693    pub fn write_json<P: AsRef<Path>, T: Serialize>(&self, path: P, value: &T) -> RiResult<()> {
694        let text = serde_json::to_string_pretty(value)
695            .map_err(|e| crate::core::RiError::Other(format!("json serialize failed: {e}")))?;
696        self.atomic_write_text(path, &text)
697    }
698
699    /// Returns the application data directory.
700    /// 
701    /// # Returns
702    /// 
703    /// The application data directory path
704    pub fn app_dir(&self) -> PathBuf {
705        self.inner.app_dir()
706    }
707
708    /// Returns the logs directory.
709    /// 
710    /// # Returns
711    /// 
712    /// The logs directory path
713    pub fn logs_dir(&self) -> PathBuf {
714        self.inner.category_dir("logs")
715    }
716
717    /// Returns the cache directory.
718    /// 
719    /// # Returns
720    /// 
721    /// The cache directory path
722    pub fn cache_dir(&self) -> PathBuf {
723        self.inner.category_dir("cache")
724    }
725
726    /// Returns the reports directory.
727    /// 
728    /// # Returns
729    /// 
730    /// The reports directory path
731    pub fn reports_dir(&self) -> PathBuf {
732        self.inner.category_dir("reports")
733    }
734
735    /// Returns the observability directory.
736    /// 
737    /// # Returns
738    /// 
739    /// The observability directory path
740    pub fn observability_dir(&self) -> PathBuf {
741        self.inner.category_dir("observability")
742    }
743
744    /// Returns the temporary directory.
745    /// 
746    /// # Returns
747    /// 
748    /// The temporary directory path
749    pub fn temp_dir(&self) -> PathBuf {
750        self.inner.category_dir("tmp")
751    }
752
753    /// Ensures a path exists under a specific category directory.
754    /// 
755    /// # Parameters
756    /// 
757    /// - `category`: The category name ("logs", "cache", "reports", "observability", "tmp", or other)
758    /// - `path_or_name`: The path or name to ensure under the category directory
759    /// 
760    /// # Returns
761    /// 
762    /// The full path to the ensured file or directory
763    pub fn ensure_category_path<S: AsRef<str>, P: AsRef<Path>>(&self, category: S, path_or_name: P) -> PathBuf {
764        let base = match category.as_ref() {
765            "logs" => self.logs_dir(),
766            "cache" => self.cache_dir(),
767            "reports" => self.reports_dir(),
768            "observability" => self.observability_dir(),
769            "tmp" => self.temp_dir(),
770            _ => self.app_dir(),
771        };
772
773        let target = base.join(path_or_name.as_ref());
774        let _ = fs::create_dir_all(target.parent().unwrap_or(&base));
775        target
776    }
777
778    /// Normalizes a path under a specific category directory, using only the file name.
779    /// 
780    /// # Parameters
781    /// 
782    /// - `category`: The category name ("logs", "cache", "reports", "observability", "tmp", or other)
783    /// - `path_or_name`: The path or name to normalize
784    /// 
785    /// # Returns
786    /// 
787    /// The full path to the normalized file or directory
788    pub fn normalize_under_category<S: AsRef<str>, P: AsRef<Path>>(&self, category: S, path_or_name: P) -> PathBuf {
789        let name = path_or_name.as_ref().file_name().unwrap_or_else(|| std::ffi::OsStr::new(""));
790        self.ensure_category_path(category, PathBuf::from(name))
791    }
792}
793
794#[cfg(feature = "pyo3")]
795/// Python bindings for RiFileSystem
796#[pyo3::prelude::pymethods]
797impl RiFileSystem {
798    /// Create a new file system with a project root from Python
799    #[new]
800    fn py_new(project_root: String) -> Result<Self, pyo3::prelude::PyErr> {
801        let path = PathBuf::from(project_root);
802        Ok(Self::new_with_root(path))
803    }
804    
805    /// Create a new file system with current working directory as root from Python
806    #[staticmethod]
807    fn new_auto_root_py() -> Result<Self, pyo3::PyErr> {
808        Self::new_auto_root()
809            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create filesystem: {e}")))
810    }
811    
812    #[pyo3(name = "atomic_write_text")]
813    fn atomic_write_text_impl(&self, path: String, text: String) -> Result<(), pyo3::PyErr> {
814        self.atomic_write_text(&path, &text)
815            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to write text: {e}")))
816    }
817    
818    #[pyo3(name = "atomic_write_bytes")]
819    fn atomic_write_bytes_impl(&self, path: String, data: Vec<u8>) -> Result<(), pyo3::PyErr> {
820        self.atomic_write_bytes(&path, &data)
821            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to write bytes: {e}")))
822    }
823    
824    #[pyo3(name = "read_text")]
825    fn read_text_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
826        self.read_text(&path)
827            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to read text: {e}")))
828    }
829    
830    #[pyo3(name = "write_json")]
831    fn write_json_impl(&self, path: String, value: String) -> Result<(), pyo3::PyErr> {
832        let json_value: serde_json::Value = serde_json::from_str(&value)
833            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid JSON: {e}")))?;
834        self.write_json(&path, &json_value)
835            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to write JSON: {e}")))
836    }
837    
838    #[pyo3(name = "read_json")]
839    fn read_json_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
840        let json_value: serde_json::Value = self.read_json(&path)
841            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to read JSON: {e}")))?;
842        serde_json::to_string_pretty(&json_value)
843            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to serialize JSON: {e}")))
844    }
845    
846    #[pyo3(name = "exists")]
847    fn exists_impl(&self, path: String) -> bool {
848        self.exists(&path)
849    }
850    
851    #[pyo3(name = "remove_file")]
852    fn remove_file_impl(&self, path: String) -> Result<(), pyo3::PyErr> {
853        self.remove_file(&path)
854            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to remove file: {e}")))
855    }
856    
857    #[pyo3(name = "remove_dir_all")]
858    fn remove_dir_all_impl(&self, path: String) -> Result<(), pyo3::PyErr> {
859        self.remove_dir_all(&path)
860            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to remove directory: {e}")))
861    }
862    
863    #[pyo3(name = "copy_file")]
864    fn copy_file_impl(&self, from: String, to: String) -> Result<(), pyo3::PyErr> {
865        self.copy_file(&from, &to)
866            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to copy file: {e}")))
867    }
868    
869    #[pyo3(name = "append_text")]
870    fn append_text_impl(&self, path: String, text: String) -> Result<(), pyo3::PyErr> {
871        self.append_text(&path, &text)
872            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to append text: {e}")))
873    }
874    
875    #[pyo3(name = "safe_mkdir")]
876    fn safe_mkdir_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
877        let result = self.safe_mkdir(&path)
878            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create directory: {e}")))?;
879        Ok(result.to_string_lossy().to_string())
880    }
881    
882    #[pyo3(name = "ensure_parent_dir")]
883    fn ensure_parent_dir_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
884        let result = self.ensure_parent_dir(&path)
885            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to ensure parent directory: {e}")))?;
886        Ok(result.to_string_lossy().to_string())
887    }
888    
889    #[pyo3(name = "get_project_root")]
890    fn get_project_root_impl(&self) -> String {
891        self.project_root().to_string_lossy().to_string()
892    }
893    
894    #[pyo3(name = "get_app_dir")]
895    fn get_app_dir_impl(&self) -> String {
896        self.app_dir().to_string_lossy().to_string()
897    }
898    
899    #[pyo3(name = "get_logs_dir")]
900    fn get_logs_dir_impl(&self) -> String {
901        self.logs_dir().to_string_lossy().to_string()
902    }
903    
904    #[pyo3(name = "get_cache_dir")]
905    fn get_cache_dir_impl(&self) -> String {
906        self.cache_dir().to_string_lossy().to_string()
907    }
908    
909    #[pyo3(name = "get_reports_dir")]
910    fn get_reports_dir_impl(&self) -> String {
911        self.reports_dir().to_string_lossy().to_string()
912    }
913    
914    #[pyo3(name = "get_observability_dir")]
915    fn get_observability_dir_impl(&self) -> String {
916        self.observability_dir().to_string_lossy().to_string()
917    }
918    
919    #[pyo3(name = "get_temp_dir")]
920    fn get_temp_dir_impl(&self) -> String {
921        self.temp_dir().to_string_lossy().to_string()
922    }
923    
924    #[pyo3(name = "ensure_category_path")]
925    fn ensure_category_path_impl(&self, category: String, path_or_name: String) -> String {
926        self.ensure_category_path(&category, &path_or_name)
927            .to_string_lossy()
928            .to_string()
929    }
930    
931    #[pyo3(name = "normalize_under_category")]
932    fn normalize_under_category_impl(&self, category: String, path_or_name: String) -> String {
933        self.normalize_under_category(&category, &path_or_name)
934            .to_string_lossy()
935            .to_string()
936    }
937}