heroforge_core/fs/
interface.rs

1//! Main filesystem interface for Heroforge repositories
2//!
3//! This module provides the primary API for interacting with a Heroforge repository
4//! as if it were a filesystem. It handles all the complexity of the underlying
5//! SQLite storage while providing a familiar, intuitive interface.
6//!
7//! # Overview
8//!
9//! The `FileSystem` type is the main entry point. It provides methods for:
10//! - Reading files and directories (no commits needed)
11//! - Writing, copying, moving, and deleting files (auto-commit by default)
12//! - Finding files with glob patterns
13//! - Checking file status and permissions
14//! - Transaction support for batch operations
15
16use crate::fs::errors::{FsError, FsResult};
17use crate::fs::operations::{
18    DirectoryEntry, FileMetadata, FilePermissions, FindResults, FsOperation,
19};
20use crate::fs::transaction::{Transaction, TransactionMode};
21use crate::repo::Repository;
22use std::sync::Arc;
23use std::sync::Mutex;
24
25/// Status of the filesystem interface
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum FileSystemStatus {
28    /// Opened in read-only mode
29    ReadOnly,
30    /// Opened in read-write mode
31    ReadWrite,
32    /// Transaction is active
33    TransactionActive,
34    /// Closed/disposed
35    Closed,
36}
37
38/// Handle to a file in the repository
39#[derive(Debug, Clone)]
40pub struct FileHandle {
41    /// Path to the file
42    pub path: String,
43    /// File metadata
44    pub metadata: FileMetadata,
45    /// Commit hash where file exists
46    pub commit: String,
47}
48
49/// Main filesystem interface for Heroforge repositories
50///
51/// This provides a filesystem-like API over the Heroforge repository storage.
52/// All operations are atomic and maintain version history.
53///
54/// # Thread Safety
55///
56/// `FileSystem` is thread-safe. Multiple threads can read simultaneously,
57/// but writes are serialized through an internal mutex.
58///
59/// # Example
60///
61/// ```no_run
62/// use heroforge_core::Repository;
63/// use heroforge_core::fs::FileSystem;
64/// use std::sync::Arc;
65///
66/// fn main() -> heroforge_core::Result<()> {
67///     let repo = Arc::new(Repository::open_rw("project.forge")?);
68///     let fs = FileSystem::new(repo);
69///
70///     // Read without commit
71///     let exists = fs.exists("README.md")?;
72///     if !exists {
73///         // Write with auto-commit
74///         fs.write_file("README.md", b"# My Project", "admin", "Initialize README")?;
75///     }
76///
77///     Ok(())
78/// }
79/// ```
80pub struct FileSystem {
81    /// Reference to the underlying repository
82    repo: Arc<Repository>,
83
84    /// Current status
85    status: Arc<Mutex<FileSystemStatus>>,
86
87    /// Active transaction (if any)
88    active_transaction: Arc<Mutex<Option<Transaction>>>,
89
90    /// Write lock for serializing concurrent writes
91    write_lock: Arc<Mutex<()>>,
92}
93
94impl FileSystem {
95    /// Create a new filesystem interface for a repository
96    pub fn new(repo: Arc<Repository>) -> Self {
97        Self {
98            repo,
99            status: Arc::new(Mutex::new(FileSystemStatus::ReadWrite)),
100            active_transaction: Arc::new(Mutex::new(None)),
101            write_lock: Arc::new(Mutex::new(())),
102        }
103    }
104
105    /// Get current status
106    pub fn status(&self) -> FileSystemStatus {
107        *self.status.lock().unwrap()
108    }
109
110    /// Check if a path exists
111    ///
112    /// # Arguments
113    ///
114    /// * `path` - Path to check (relative to repository root)
115    ///
116    /// # Returns
117    ///
118    /// `true` if path exists, `false` otherwise
119    pub fn exists(&self, path: &str) -> FsResult<bool> {
120        self.validate_path(path)?;
121        // TODO: Implement by checking manifest at trunk
122        Ok(true)
123    }
124
125    /// Check if path is a directory
126    pub fn is_dir(&self, path: &str) -> FsResult<bool> {
127        self.validate_path(path)?;
128        let metadata = self.stat(path)?;
129        Ok(metadata.is_dir)
130    }
131
132    /// Check if path is a file
133    pub fn is_file(&self, path: &str) -> FsResult<bool> {
134        self.validate_path(path)?;
135        let metadata = self.stat(path)?;
136        Ok(!metadata.is_dir)
137    }
138
139    /// Get file metadata
140    ///
141    /// # Arguments
142    ///
143    /// * `path` - Path to file/directory
144    ///
145    /// # Returns
146    ///
147    /// File metadata including size, permissions, etc.
148    pub fn stat(&self, path: &str) -> FsResult<FileMetadata> {
149        self.validate_path(path)?;
150        // TODO: Implement by querying manifest
151        Ok(FileMetadata {
152            path: path.to_string(),
153            is_dir: false,
154            size: 0,
155            permissions: FilePermissions::file(),
156            is_symlink: false,
157            symlink_target: None,
158            modified: 0,
159            hash: None,
160            kind: crate::fs::operations::FileKind::File,
161        })
162    }
163
164    /// Read file content as bytes
165    ///
166    /// # Arguments
167    ///
168    /// * `path` - Path to file
169    ///
170    /// # Returns
171    ///
172    /// File content as `Vec<u8>`
173    pub fn read_file(&self, path: &str) -> FsResult<Vec<u8>> {
174        self.validate_path(path)?;
175        self.validate_is_file(path)?;
176        // TODO: Implement by reading from blob storage
177        Ok(Vec::new())
178    }
179
180    /// Read file content as string
181    ///
182    /// # Arguments
183    ///
184    /// * `path` - Path to file
185    ///
186    /// # Returns
187    ///
188    /// File content as `String`
189    pub fn read_file_string(&self, path: &str) -> FsResult<String> {
190        let bytes = self.read_file(path)?;
191        String::from_utf8(bytes).map_err(|e| FsError::Encoding(e.to_string()))
192    }
193
194    /// List directory contents
195    ///
196    /// # Arguments
197    ///
198    /// * `path` - Directory path
199    ///
200    /// # Returns
201    ///
202    /// Vector of directory entries
203    pub fn list_dir(&self, path: &str) -> FsResult<Vec<DirectoryEntry>> {
204        self.validate_path(path)?;
205        self.validate_is_directory(path)?;
206        // TODO: Implement by traversing manifest
207        Ok(Vec::new())
208    }
209
210    /// Write file content (creates or overwrites)
211    ///
212    /// # Arguments
213    ///
214    /// * `path` - Path to file
215    /// * `content` - File content
216    /// * `author` - Author name
217    /// * `message` - Commit message
218    ///
219    /// # Returns
220    ///
221    /// Commit hash of the change
222    pub fn write_file(
223        &self,
224        path: &str,
225        content: &[u8],
226        author: &str,
227        message: &str,
228    ) -> FsResult<String> {
229        self.validate_path(path)?;
230        let _lock = self.write_lock.lock();
231
232        // Create operation
233        let _op = FsOperation::WriteFile {
234            path: path.to_string(),
235            content: content.to_vec(),
236        };
237
238        // TODO: Execute operation and create commit
239        // For now, return a dummy hash
240        let _ = (author, message); // Use parameters
241        Ok("commit_abc123def".to_string())
242    }
243
244    /// Copy a file
245    pub fn copy_file(&self, src: &str, dst: &str, author: &str, message: &str) -> FsResult<String> {
246        self.validate_path(src)?;
247        self.validate_path(dst)?;
248        self.validate_is_file(src)?;
249        let _lock = self.write_lock.lock();
250
251        let _op = FsOperation::CopyFile {
252            src: src.to_string(),
253            dst: dst.to_string(),
254        };
255
256        // TODO: Execute operation and create commit
257        let _ = (author, message); // Use parameters
258        Ok("commit_abc123def".to_string())
259    }
260
261    /// Copy a directory recursively
262    pub fn copy_dir(&self, src: &str, dst: &str, author: &str, message: &str) -> FsResult<String> {
263        self.validate_path(src)?;
264        self.validate_path(dst)?;
265        self.validate_is_directory(src)?;
266        let _lock = self.write_lock.lock();
267
268        let _op = FsOperation::CopyDir {
269            src: src.to_string(),
270            dst: dst.to_string(),
271        };
272
273        // TODO: Execute operation and create commit
274        let _ = (author, message); // Use parameters
275        Ok("commit_abc123def".to_string())
276    }
277
278    /// Move/rename a file
279    pub fn move_file(&self, src: &str, dst: &str, author: &str, message: &str) -> FsResult<String> {
280        self.validate_path(src)?;
281        self.validate_path(dst)?;
282        self.validate_is_file(src)?;
283        let _lock = self.write_lock.lock();
284
285        let _op = FsOperation::MoveFile {
286            src: src.to_string(),
287            dst: dst.to_string(),
288        };
289
290        // TODO: Execute operation and create commit
291        let _ = (author, message); // Use parameters
292        Ok("commit_abc123def".to_string())
293    }
294
295    /// Move/rename a directory
296    pub fn move_dir(&self, src: &str, dst: &str, author: &str, message: &str) -> FsResult<String> {
297        self.validate_path(src)?;
298        self.validate_path(dst)?;
299        self.validate_is_directory(src)?;
300        let _lock = self.write_lock.lock();
301
302        let _op = FsOperation::MoveDir {
303            src: src.to_string(),
304            dst: dst.to_string(),
305        };
306
307        // TODO: Execute operation and create commit
308        let _ = (author, message); // Use parameters
309        Ok("commit_abc123def".to_string())
310    }
311
312    /// Delete a file
313    pub fn delete_file(&self, path: &str, author: &str, message: &str) -> FsResult<String> {
314        self.validate_path(path)?;
315        self.validate_is_file(path)?;
316        let _lock = self.write_lock.lock();
317
318        let _op = FsOperation::DeleteFile {
319            path: path.to_string(),
320        };
321
322        // TODO: Execute operation and create commit
323        let _ = (author, message); // Use parameters
324        Ok("commit_abc123def".to_string())
325    }
326
327    /// Delete a directory recursively
328    pub fn delete_dir(&self, path: &str, author: &str, message: &str) -> FsResult<String> {
329        self.validate_path(path)?;
330        self.validate_is_directory(path)?;
331        let _lock = self.write_lock.lock();
332
333        let _op = FsOperation::DeleteDir {
334            path: path.to_string(),
335        };
336
337        // TODO: Execute operation and create commit
338        let _ = (author, message); // Use parameters
339        Ok("commit_abc123def".to_string())
340    }
341
342    /// Change file permissions
343    pub fn chmod(
344        &self,
345        path: &str,
346        permissions: FilePermissions,
347        author: &str,
348        message: &str,
349    ) -> FsResult<String> {
350        self.validate_path(path)?;
351        let _lock = self.write_lock.lock();
352
353        let _op = FsOperation::Chmod {
354            path: path.to_string(),
355            permissions,
356            recursive: false,
357        };
358
359        // TODO: Execute operation and create commit
360        let _ = (author, message); // Use parameters
361        Ok("commit_abc123def".to_string())
362    }
363
364    /// Change permissions recursively
365    pub fn chmod_recursive(
366        &self,
367        path: &str,
368        permissions: FilePermissions,
369        author: &str,
370        message: &str,
371    ) -> FsResult<String> {
372        self.validate_path(path)?;
373        let _lock = self.write_lock.lock();
374
375        let _op = FsOperation::Chmod {
376            path: path.to_string(),
377            permissions,
378            recursive: true,
379        };
380
381        // TODO: Execute operation and create commit
382        let _ = (author, message); // Use parameters
383        Ok("commit_abc123def".to_string())
384    }
385
386    /// Make file executable
387    pub fn make_executable(&self, path: &str, author: &str, message: &str) -> FsResult<String> {
388        self.validate_path(path)?;
389        self.validate_is_file(path)?;
390        let _lock = self.write_lock.lock();
391
392        let _op = FsOperation::MakeExecutable {
393            path: path.to_string(),
394        };
395
396        // TODO: Execute operation and create commit
397        let _ = (author, message); // Use parameters
398        Ok("commit_abc123def".to_string())
399    }
400
401    /// Create a symbolic link
402    pub fn symlink(
403        &self,
404        link_path: &str,
405        target_path: &str,
406        author: &str,
407        message: &str,
408    ) -> FsResult<String> {
409        self.validate_path(link_path)?;
410        let _lock = self.write_lock.lock();
411
412        let _op = FsOperation::Symlink {
413            link_path: link_path.to_string(),
414            target_path: target_path.to_string(),
415        };
416
417        // TODO: Execute operation and create commit
418        let _ = (author, message); // Use parameters
419        Ok("commit_abc123def".to_string())
420    }
421
422    /// Find files matching a glob pattern
423    ///
424    /// # Arguments
425    ///
426    /// * `pattern` - Glob pattern (e.g., "**/*.rs")
427    ///
428    /// # Returns
429    ///
430    /// List of matching file paths
431    pub fn find(&self, pattern: &str) -> FsResult<FindResults> {
432        // Validate pattern
433        if pattern.is_empty() {
434            return Err(FsError::PatternError("Pattern cannot be empty".to_string()));
435        }
436
437        // TODO: Implement glob matching
438        Ok(FindResults::new())
439    }
440
441    /// Calculate disk usage of a path
442    ///
443    /// # Arguments
444    ///
445    /// * `path` - Path to directory or file
446    ///
447    /// # Returns
448    ///
449    /// Total size in bytes
450    pub fn disk_usage(&self, path: &str) -> FsResult<u64> {
451        self.validate_path(path)?;
452        // TODO: Implement by traversing manifests
453        Ok(0)
454    }
455
456    /// Count files matching a pattern
457    ///
458    /// # Arguments
459    ///
460    /// * `pattern` - Glob pattern
461    ///
462    /// # Returns
463    ///
464    /// Number of matching files
465    pub fn count_files(&self, pattern: &str) -> FsResult<usize> {
466        let results = self.find(pattern)?;
467        Ok(results.count)
468    }
469
470    /// Begin a transaction for batch operations
471    ///
472    /// # Returns
473    ///
474    /// A transaction that can be used to group multiple operations
475    pub fn begin_transaction(&self) -> FsResult<Transaction> {
476        let tx = Transaction::new(TransactionMode::ReadWrite);
477
478        let mut active = self.active_transaction.lock().unwrap();
479        if active.is_some() {
480            return Err(FsError::TransactionError(
481                "Transaction already active".to_string(),
482            ));
483        }
484
485        *active = Some(tx.clone());
486        Ok(tx)
487    }
488
489    /// End the active transaction
490    pub fn end_transaction(&self) -> FsResult<()> {
491        let mut active = self.active_transaction.lock().unwrap();
492        *active = None;
493        Ok(())
494    }
495
496    /// Check if a transaction is active
497    pub fn has_active_transaction(&self) -> bool {
498        self.active_transaction.lock().unwrap().is_some()
499    }
500
501    /// Get the active transaction (if any)
502    pub fn active_transaction(&self) -> Option<Transaction> {
503        self.active_transaction.lock().unwrap().clone()
504    }
505
506    // Helper methods
507
508    /// Validate path format
509    fn validate_path(&self, path: &str) -> FsResult<()> {
510        if path.is_empty() {
511            return Err(FsError::InvalidPath("Path cannot be empty".to_string()));
512        }
513
514        if path.contains('\0') {
515            return Err(FsError::InvalidPath(
516                "Path cannot contain null bytes".to_string(),
517            ));
518        }
519
520        // Normalize path - remove double slashes, etc.
521        Ok(())
522    }
523
524    /// Validate that path points to a file
525    fn validate_is_file(&self, path: &str) -> FsResult<()> {
526        let metadata = self.stat(path)?;
527        if metadata.is_dir {
528            return Err(FsError::NotAFile(path.to_string()));
529        }
530        Ok(())
531    }
532
533    /// Validate that path points to a directory
534    fn validate_is_directory(&self, path: &str) -> FsResult<()> {
535        let metadata = self.stat(path)?;
536        if !metadata.is_dir {
537            return Err(FsError::NotADirectory(path.to_string()));
538        }
539        Ok(())
540    }
541}
542
543impl Clone for FileSystem {
544    fn clone(&self) -> Self {
545        Self {
546            repo: Arc::clone(&self.repo),
547            status: Arc::clone(&self.status),
548            active_transaction: Arc::clone(&self.active_transaction),
549            write_lock: Arc::clone(&self.write_lock),
550        }
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_filesystem_creation() {
560        // This would need a real repo to test
561        // let fs = FileSystem::new(Arc::new(repo));
562        // assert_eq!(fs.status(), FileSystemStatus::ReadWrite);
563    }
564
565    #[test]
566    fn test_path_validation() {
567        // Test with a mock setup
568    }
569}