dmsc/fs/mod.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC 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 DMSC, offering safe and reliable
23//! file operations with support for atomic writes, directory management, and structured data formats.
24//!
25//! ## Key Components
26//!
27//! - **DMSCFileSystem**: 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 dmsc::prelude::*;
44//! use std::path::PathBuf;
45//!
46//! fn example() -> DMSCResult<()> {
47//! // Create a file system with a project root
48//! let project_root = PathBuf::from(".");
49//! let fs = DMSCFileSystem::new_with_root(project_root);
50//!
51//! // Write text to a file
52//! fs.atomic_write_text("example.txt", "Hello, DMSC!")?;
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::DMSCResult;
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 /// Returns the project root directory.
129 ///
130 /// # Returns
131 ///
132 /// A reference to the project root path
133 fn project_root(&self) -> &Path {
134 &self.project_root
135 }
136
137 /// Safely creates a directory and all its parent directories if they don't exist.
138 ///
139 /// # Parameters
140 ///
141 /// - `path`: The path to the directory to create
142 ///
143 /// # Returns
144 ///
145 /// A `DMSCResult<PathBuf>` containing the created directory path
146 fn safe_mkdir(&self, path: &Path) -> DMSCResult<PathBuf> {
147 fs::create_dir_all(path).map_err(|e| crate::core::DMSCError::Other(format!("safe_mkdir failed: {e}")))?;
148 Ok(path.to_path_buf())
149 }
150
151 /// Ensures that the parent directory of a given path exists.
152 ///
153 /// # Parameters
154 ///
155 /// - `path`: The path whose parent directory should be ensured
156 ///
157 /// # Returns
158 ///
159 /// A `DMSCResult<PathBuf>` containing the parent directory path
160 fn ensure_parent_dir(&self, path: &Path) -> DMSCResult<PathBuf> {
161 if let Some(parent) = path.parent() {
162 self.safe_mkdir(parent)
163 } else {
164 Ok(self.project_root.clone())
165 }
166 }
167
168 /// Atomically writes text to a file.
169 ///
170 /// This method writes to a temporary file first, then renames it to the target path,
171 /// ensuring that the write operation is atomic.
172 ///
173 /// # Parameters
174 ///
175 /// - `path`: The path to the file to write
176 /// - `text`: The text to write to the file
177 ///
178 /// # Returns
179 ///
180 /// A `DMSCResult<()>` indicating success or failure
181 fn atomic_write_text(&self, path: &Path, text: &str) -> DMSCResult<()> {
182 self.ensure_parent_dir(path)?;
183 let dir = path.parent().unwrap_or_else(|| Path::new("."));
184 let ts = SystemTime::now()
185 .duration_since(SystemTime::UNIX_EPOCH)
186 .map_err(|e| crate::core::DMSCError::Other(format!("timestamp error: {e}")))?;
187 let tmp_name = format!(".tmp_{}_{}", ts.as_millis(), path.file_name().and_then(|s| s.to_str()).unwrap_or("tmp"));
188 let tmp_path = dir.join(tmp_name);
189
190 {
191 let mut file = OpenOptions::new()
192 .write(true)
193 .create(true)
194 .truncate(true)
195 .open(&tmp_path)
196 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_text open tmp failed: {e}")))?;
197 file.write_all(text.as_bytes())
198 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_text write failed: {e}")))?;
199 file.sync_all()
200 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_text sync failed: {e}")))?;
201 }
202
203 fs::rename(&tmp_path, path)
204 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_text rename failed: {e}")))?;
205
206 Ok(())
207 }
208
209 /// Atomically writes bytes to a file.
210 ///
211 /// This method writes to a temporary file first, then renames it to the target path,
212 /// ensuring that the write operation is atomic.
213 ///
214 /// # Parameters
215 ///
216 /// - `path`: The path to the file to write
217 /// - `data`: The bytes to write to the file
218 ///
219 /// # Returns
220 ///
221 /// A `DMSCResult<()>` indicating success or failure
222 fn atomic_write_bytes(&self, path: &Path, data: &[u8]) -> DMSCResult<()> {
223 self.ensure_parent_dir(path)?;
224 let dir = path.parent().unwrap_or_else(|| Path::new("."));
225 let ts = SystemTime::now()
226 .duration_since(SystemTime::UNIX_EPOCH)
227 .map_err(|e| crate::core::DMSCError::Other(format!("timestamp error: {e}")))?;
228 let tmp_name = format!(".tmp_{}_{}", ts.as_millis(), path.file_name().and_then(|s| s.to_str()).unwrap_or("tmp"));
229 let tmp_path = dir.join(tmp_name);
230
231 {
232 let mut file = OpenOptions::new()
233 .write(true)
234 .create(true)
235 .truncate(true)
236 .open(&tmp_path)
237 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_bytes open tmp failed: {e}")))?;
238 file.write_all(data)
239 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_bytes write failed: {e}")))?;
240 file.sync_all()
241 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_bytes sync failed: {e}")))?;
242 }
243
244 fs::rename(&tmp_path, path)
245 .map_err(|e| crate::core::DMSCError::Other(format!("atomic_write_bytes rename failed: {e}")))?;
246
247 Ok(())
248 }
249
250 /// Reads text from a file.
251 ///
252 /// # Parameters
253 ///
254 /// - `path`: The path to the file to read
255 ///
256 /// # Returns
257 ///
258 /// A `DMSCResult<String>` containing the file content
259 fn read_text(&self, path: &Path) -> DMSCResult<String> {
260 let mut file = OpenOptions::new()
261 .read(true)
262 .open(path)
263 .map_err(|e| crate::core::DMSCError::Other(format!("read_text open failed: {e}")))?;
264 let mut buf = String::new();
265 file.read_to_string(&mut buf)
266 .map_err(|e| crate::core::DMSCError::Other(format!("read_text read failed: {e}")))?;
267 Ok(buf)
268 }
269
270 /// Returns the application data directory.
271 ///
272 /// Ensures the directory exists before returning it.
273 ///
274 /// # Returns
275 ///
276 /// The application data directory path
277 fn app_dir(&self) -> PathBuf {
278 let _ = fs::create_dir_all(&self.app_data_root);
279 self.app_data_root.clone()
280 }
281
282 /// Returns a category-specific directory.
283 ///
284 /// Ensures the directory exists before returning it.
285 ///
286 /// # Parameters
287 ///
288 /// - `name`: The name of the category
289 ///
290 /// # Returns
291 ///
292 /// The category directory path
293 fn category_dir(&self, name: &str) -> PathBuf {
294 let dir = self.app_dir().join(name);
295 let _ = fs::create_dir_all(&dir);
296 dir
297 }
298}
299
300/// Public-facing filesystem class.
301///
302/// This struct provides a comprehensive file system abstraction for DMSC, offering safe and reliable
303/// file operations with support for atomic writes, directory management, and structured data formats.
304#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
305#[derive(Clone)]
306pub struct DMSCFileSystem {
307 /// Internal file system implementation
308 inner: FileSystemImpl,
309}
310
311impl DMSCFileSystem {
312 /// Creates a new file system with a project root and default app data root.
313 ///
314 /// # Parameters
315 ///
316 /// - `project_root`: The project root directory
317 ///
318 /// # Returns
319 ///
320 /// A new `DMSCFileSystem` instance
321 pub fn new_with_root(project_root: PathBuf) -> Self {
322 let inner = FileSystemImpl::new_with_root(project_root);
323 DMSCFileSystem { inner }
324 }
325
326 /// Creates a new file system with specified roots.
327 ///
328 /// # Parameters
329 ///
330 /// - `project_root`: The project root directory
331 /// - `app_data_root`: The application data root directory
332 ///
333 /// # Returns
334 ///
335 /// A new `DMSCFileSystem` instance
336 pub fn new_with_roots(project_root: PathBuf, app_data_root: PathBuf) -> Self {
337 let inner = FileSystemImpl::new_with_roots(project_root, app_data_root);
338 DMSCFileSystem { inner }
339 }
340
341 /// Creates a new file system with the current working directory as the project root.
342 ///
343 /// # Returns
344 ///
345 /// A `DMSCResult<Self>` containing the new `DMSCFileSystem` instance
346 pub fn new_auto_root() -> DMSCResult<Self> {
347 let cwd = std::env::current_dir()
348 .map_err(|e| crate::core::DMSCError::Other(format!("detect project root failed: {e}")))?;
349 Ok(Self::new_with_root(cwd))
350 }
351
352 /// Returns the project root directory.
353 ///
354 /// # Returns
355 ///
356 /// A reference to the project root path
357 pub fn project_root(&self) -> &Path {
358 self.inner.project_root()
359 }
360
361 /// Safely creates a directory and all its parent directories if they don't exist.
362 ///
363 /// # Parameters
364 ///
365 /// - `path`: The path to the directory to create
366 ///
367 /// # Returns
368 ///
369 /// A `DMSCResult<PathBuf>` containing the created directory path
370 pub fn safe_mkdir<P: AsRef<Path>>(&self, path: P) -> DMSCResult<PathBuf> {
371 self.inner.safe_mkdir(path.as_ref())
372 }
373
374 /// Ensures that the parent directory of a given path exists.
375 ///
376 /// # Parameters
377 ///
378 /// - `path`: The path whose parent directory should be ensured
379 ///
380 /// # Returns
381 ///
382 /// A `DMSCResult<PathBuf>` containing the parent directory path
383 pub fn ensure_parent_dir<P: AsRef<Path>>(&self, path: P) -> DMSCResult<PathBuf> {
384 self.inner.ensure_parent_dir(path.as_ref())
385 }
386
387 /// Atomically writes text to a file.
388 ///
389 /// This method writes to a temporary file first, then renames it to the target path,
390 /// ensuring that the write operation is atomic.
391 ///
392 /// # Parameters
393 ///
394 /// - `path`: The path to the file to write
395 /// - `text`: The text to write to the file
396 ///
397 /// # Returns
398 ///
399 /// A `DMSCResult<()>` indicating success or failure
400 pub fn atomic_write_text<P: AsRef<Path>>(&self, path: P, text: &str) -> DMSCResult<()> {
401 self.inner.atomic_write_text(path.as_ref(), text)
402 }
403
404 /// Atomically writes bytes to a file.
405 ///
406 /// This method writes to a temporary file first, then renames it to the target path,
407 /// ensuring that the write operation is atomic.
408 ///
409 /// # Parameters
410 ///
411 /// - `path`: The path to the file to write
412 /// - `data`: The bytes to write to the file
413 ///
414 /// # Returns
415 ///
416 /// A `DMSCResult<()>` indicating success or failure
417 pub fn atomic_write_bytes<P: AsRef<Path>>(&self, path: P, data: &[u8]) -> DMSCResult<()> {
418 self.inner.atomic_write_bytes(path.as_ref(), data)
419 }
420
421 /// Reads text from a file.
422 ///
423 /// # Parameters
424 ///
425 /// - `path`: The path to the file to read
426 ///
427 /// # Returns
428 ///
429 /// A `DMSCResult<String>` containing the file content
430 pub fn read_text<P: AsRef<Path>>(&self, path: P) -> DMSCResult<String> {
431 self.inner.read_text(path.as_ref())
432 }
433
434 /// Reads JSON from a file and deserializes it into a type.
435 ///
436 /// # Parameters
437 ///
438 /// - `path`: The path to the JSON file to read
439 ///
440 /// # Type Parameters
441 ///
442 /// - `T`: The type to deserialize the JSON into
443 ///
444 /// # Returns
445 ///
446 /// A `DMSCResult<T>` containing the deserialized data
447 pub fn read_json<P: AsRef<Path>, T: DeserializeOwned>(&self, path: P) -> DMSCResult<T> {
448 let text = self.read_text(path)?;
449 serde_json::from_str(&text)
450 .map_err(|e| crate::core::DMSCError::Other(format!("json read failed: {e}")))
451 }
452
453 /// Checks if a file or directory exists.
454 ///
455 /// # Parameters
456 ///
457 /// - `path`: The path to check
458 ///
459 /// # Returns
460 ///
461 /// `true` if the path exists, `false` otherwise
462 pub fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
463 path.as_ref().exists()
464 }
465
466 /// Removes a file.
467 ///
468 /// # Parameters
469 ///
470 /// - `path`: The path to the file to remove
471 ///
472 /// # Returns
473 ///
474 /// A `DMSCResult<()>` indicating success or failure
475 pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> DMSCResult<()> {
476 let p = path.as_ref();
477 match fs::remove_file(p) {
478 Ok(()) => Ok(()),
479 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
480 Err(e) => Err(crate::core::DMSCError::Other(format!("remove_file failed: {e}"))),
481 }
482 }
483
484 /// Removes a directory and all its contents.
485 ///
486 /// # Parameters
487 ///
488 /// - `path`: The path to the directory to remove
489 ///
490 /// # Returns
491 ///
492 /// A `DMSCResult<()>` indicating success or failure
493 pub fn remove_dir_all<P: AsRef<Path>>(&self, path: P) -> DMSCResult<()> {
494 let p = path.as_ref();
495 match fs::remove_dir_all(p) {
496 Ok(()) => Ok(()),
497 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
498 Err(e) => Err(crate::core::DMSCError::Other(format!("remove_dir_all failed: {e}"))),
499 }
500 }
501
502 /// Copies a file from one path to another.
503 ///
504 /// # Parameters
505 ///
506 /// - `from`: The source file path
507 /// - `to`: The destination file path
508 ///
509 /// # Returns
510 ///
511 /// A `DMSCResult<()>` indicating success or failure
512 pub fn copy_file<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> DMSCResult<()> {
513 let src = from.as_ref();
514 let dst = to.as_ref();
515 if let Some(parent) = dst.parent() {
516 self.safe_mkdir(parent)?;
517 }
518 fs::copy(src, dst)
519 .map_err(|e| crate::core::DMSCError::Other(format!("copy_file failed: {e}")))?;
520 Ok(())
521 }
522
523 /// Appends text to a file.
524 ///
525 /// # Parameters
526 ///
527 /// - `path`: The path to the file to append to
528 /// - `text`: The text to append to the file
529 ///
530 /// # Returns
531 ///
532 /// A `DMSCResult<()>` indicating success or failure
533 pub fn append_text<P: AsRef<Path>>(&self, path: P, text: &str) -> DMSCResult<()> {
534 use std::io::Write as _;
535
536 let path_ref = path.as_ref();
537 self.ensure_parent_dir(path_ref)?;
538 let mut file = OpenOptions::new()
539 .create(true)
540 .append(true)
541 .open(path_ref)
542 .map_err(|e| crate::core::DMSCError::Other(format!("append_text open failed: {e}")))?;
543 file.write_all(text.as_bytes())
544 .map_err(|e| crate::core::DMSCError::Other(format!("append_text write failed: {e}")))?;
545 file.flush()
546 .map_err(|e| crate::core::DMSCError::Other(format!("append_text flush failed: {e}")))?;
547 Ok(())
548 }
549
550 /// Writes a JSON value to a file.
551 ///
552 /// # Parameters
553 ///
554 /// - `path`: The path to the file to write
555 /// - `value`: The value to serialize and write
556 ///
557 /// # Type Parameters
558 ///
559 /// - `T`: The type of the value to serialize
560 ///
561 /// # Returns
562 ///
563 /// A `DMSCResult<()>` indicating success or failure
564 pub fn write_json<P: AsRef<Path>, T: Serialize>(&self, path: P, value: &T) -> DMSCResult<()> {
565 let text = serde_json::to_string_pretty(value)
566 .map_err(|e| crate::core::DMSCError::Other(format!("json serialize failed: {e}")))?;
567 self.atomic_write_text(path, &text)
568 }
569
570 /// Returns the application data directory.
571 ///
572 /// # Returns
573 ///
574 /// The application data directory path
575 pub fn app_dir(&self) -> PathBuf {
576 self.inner.app_dir()
577 }
578
579 /// Returns the logs directory.
580 ///
581 /// # Returns
582 ///
583 /// The logs directory path
584 pub fn logs_dir(&self) -> PathBuf {
585 self.inner.category_dir("logs")
586 }
587
588 /// Returns the cache directory.
589 ///
590 /// # Returns
591 ///
592 /// The cache directory path
593 pub fn cache_dir(&self) -> PathBuf {
594 self.inner.category_dir("cache")
595 }
596
597 /// Returns the reports directory.
598 ///
599 /// # Returns
600 ///
601 /// The reports directory path
602 pub fn reports_dir(&self) -> PathBuf {
603 self.inner.category_dir("reports")
604 }
605
606 /// Returns the observability directory.
607 ///
608 /// # Returns
609 ///
610 /// The observability directory path
611 pub fn observability_dir(&self) -> PathBuf {
612 self.inner.category_dir("observability")
613 }
614
615 /// Returns the temporary directory.
616 ///
617 /// # Returns
618 ///
619 /// The temporary directory path
620 pub fn temp_dir(&self) -> PathBuf {
621 self.inner.category_dir("tmp")
622 }
623
624 /// Ensures a path exists under a specific category directory.
625 ///
626 /// # Parameters
627 ///
628 /// - `category`: The category name ("logs", "cache", "reports", "observability", "tmp", or other)
629 /// - `path_or_name`: The path or name to ensure under the category directory
630 ///
631 /// # Returns
632 ///
633 /// The full path to the ensured file or directory
634 pub fn ensure_category_path<S: AsRef<str>, P: AsRef<Path>>(&self, category: S, path_or_name: P) -> PathBuf {
635 let base = match category.as_ref() {
636 "logs" => self.logs_dir(),
637 "cache" => self.cache_dir(),
638 "reports" => self.reports_dir(),
639 "observability" => self.observability_dir(),
640 "tmp" => self.temp_dir(),
641 _ => self.app_dir(),
642 };
643
644 let target = base.join(path_or_name.as_ref());
645 let _ = fs::create_dir_all(target.parent().unwrap_or(&base));
646 target
647 }
648
649 /// Normalizes a path under a specific category directory, using only the file name.
650 ///
651 /// # Parameters
652 ///
653 /// - `category`: The category name ("logs", "cache", "reports", "observability", "tmp", or other)
654 /// - `path_or_name`: The path or name to normalize
655 ///
656 /// # Returns
657 ///
658 /// The full path to the normalized file or directory
659 pub fn normalize_under_category<S: AsRef<str>, P: AsRef<Path>>(&self, category: S, path_or_name: P) -> PathBuf {
660 let name = path_or_name.as_ref().file_name().unwrap_or_else(|| std::ffi::OsStr::new(""));
661 self.ensure_category_path(category, PathBuf::from(name))
662 }
663}
664
665#[cfg(feature = "pyo3")]
666/// Python bindings for DMSCFileSystem
667#[pyo3::prelude::pymethods]
668impl DMSCFileSystem {
669 /// Create a new file system with a project root from Python
670 #[new]
671 fn py_new(project_root: String) -> Result<Self, pyo3::prelude::PyErr> {
672 let path = PathBuf::from(project_root);
673 Ok(Self::new_with_root(path))
674 }
675
676 /// Create a new file system with current working directory as root from Python
677 #[staticmethod]
678 fn new_auto_root_py() -> Result<Self, pyo3::PyErr> {
679 Self::new_auto_root()
680 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create filesystem: {e}")))
681 }
682
683 #[pyo3(name = "atomic_write_text")]
684 fn atomic_write_text_impl(&self, path: String, text: String) -> Result<(), pyo3::PyErr> {
685 self.atomic_write_text(&path, &text)
686 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to write text: {e}")))
687 }
688
689 #[pyo3(name = "atomic_write_bytes")]
690 fn atomic_write_bytes_impl(&self, path: String, data: Vec<u8>) -> Result<(), pyo3::PyErr> {
691 self.atomic_write_bytes(&path, &data)
692 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to write bytes: {e}")))
693 }
694
695 #[pyo3(name = "read_text")]
696 fn read_text_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
697 self.read_text(&path)
698 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to read text: {e}")))
699 }
700
701 #[pyo3(name = "write_json")]
702 fn write_json_impl(&self, path: String, value: String) -> Result<(), pyo3::PyErr> {
703 let json_value: serde_json::Value = serde_json::from_str(&value)
704 .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid JSON: {e}")))?;
705 self.write_json(&path, &json_value)
706 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to write JSON: {e}")))
707 }
708
709 #[pyo3(name = "read_json")]
710 fn read_json_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
711 let json_value: serde_json::Value = self.read_json(&path)
712 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to read JSON: {e}")))?;
713 serde_json::to_string_pretty(&json_value)
714 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to serialize JSON: {e}")))
715 }
716
717 #[pyo3(name = "exists")]
718 fn exists_impl(&self, path: String) -> bool {
719 self.exists(&path)
720 }
721
722 #[pyo3(name = "remove_file")]
723 fn remove_file_impl(&self, path: String) -> Result<(), pyo3::PyErr> {
724 self.remove_file(&path)
725 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to remove file: {e}")))
726 }
727
728 #[pyo3(name = "remove_dir_all")]
729 fn remove_dir_all_impl(&self, path: String) -> Result<(), pyo3::PyErr> {
730 self.remove_dir_all(&path)
731 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to remove directory: {e}")))
732 }
733
734 #[pyo3(name = "copy_file")]
735 fn copy_file_impl(&self, from: String, to: String) -> Result<(), pyo3::PyErr> {
736 self.copy_file(&from, &to)
737 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to copy file: {e}")))
738 }
739
740 #[pyo3(name = "append_text")]
741 fn append_text_impl(&self, path: String, text: String) -> Result<(), pyo3::PyErr> {
742 self.append_text(&path, &text)
743 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to append text: {e}")))
744 }
745
746 #[pyo3(name = "safe_mkdir")]
747 fn safe_mkdir_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
748 let result = self.safe_mkdir(&path)
749 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create directory: {e}")))?;
750 Ok(result.to_string_lossy().to_string())
751 }
752
753 #[pyo3(name = "ensure_parent_dir")]
754 fn ensure_parent_dir_impl(&self, path: String) -> Result<String, pyo3::PyErr> {
755 let result = self.ensure_parent_dir(&path)
756 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to ensure parent directory: {e}")))?;
757 Ok(result.to_string_lossy().to_string())
758 }
759
760 #[pyo3(name = "get_project_root")]
761 fn get_project_root_impl(&self) -> String {
762 self.project_root().to_string_lossy().to_string()
763 }
764
765 #[pyo3(name = "get_app_dir")]
766 fn get_app_dir_impl(&self) -> String {
767 self.app_dir().to_string_lossy().to_string()
768 }
769
770 #[pyo3(name = "get_logs_dir")]
771 fn get_logs_dir_impl(&self) -> String {
772 self.logs_dir().to_string_lossy().to_string()
773 }
774
775 #[pyo3(name = "get_cache_dir")]
776 fn get_cache_dir_impl(&self) -> String {
777 self.cache_dir().to_string_lossy().to_string()
778 }
779
780 #[pyo3(name = "get_reports_dir")]
781 fn get_reports_dir_impl(&self) -> String {
782 self.reports_dir().to_string_lossy().to_string()
783 }
784
785 #[pyo3(name = "get_observability_dir")]
786 fn get_observability_dir_impl(&self) -> String {
787 self.observability_dir().to_string_lossy().to_string()
788 }
789
790 #[pyo3(name = "get_temp_dir")]
791 fn get_temp_dir_impl(&self) -> String {
792 self.temp_dir().to_string_lossy().to_string()
793 }
794
795 #[pyo3(name = "ensure_category_path")]
796 fn ensure_category_path_impl(&self, category: String, path_or_name: String) -> String {
797 self.ensure_category_path(&category, &path_or_name)
798 .to_string_lossy()
799 .to_string()
800 }
801
802 #[pyo3(name = "normalize_under_category")]
803 fn normalize_under_category_impl(&self, category: String, path_or_name: String) -> String {
804 self.normalize_under_category(&category, &path_or_name)
805 .to_string_lossy()
806 .to_string()
807 }
808}