heroforge_core/fs/mod.rs
1//! # Filesystem Interface
2//!
3//! This module provides a clean, high-level filesystem abstraction for Heroforge repositories.
4//! It hides the complexity of SQLite-backed storage and provides a familiar filesystem-like API.
5//!
6//! ## Architecture Overview
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │ FsInterface (sync API) │
11//! │ - RwLock<StagingState> │
12//! │ - Author name (set at initialization) │
13//! │ - All read/write operations acquire appropriate locks │
14//! └─────────────────────────────────────────────────────────────────┘
15//! │
16//! ┌───────────────────┴───────────────────┐
17//! │ │
18//! ▼ ▼
19//! ┌─────────────────────┐ ┌─────────────────────────┐
20//! │ Staging Directory │ │ Commit Thread │
21//! │ │ │ (background) │
22//! │ - All writes go │ │ │
23//! │ here first │ │ - Runs every 1 minute │
24//! │ - Files < 2MB │ │ - Acquires write lock │
25//! │ - Frequent updates │ │ - Blocks all I/O │
26//! │ allowed │ │ - Flushes to .forge DB │
27//! │ │ │ - Clears staging dir │
28//! └─────────────────────┘ └─────────────────────────┘
29//! │ │
30//! └───────────────────┬───────────────────┘
31//! │
32//! ▼
33//! ┌─────────────────────┐
34//! │ .forge Database │
35//! │ (SQLite) │
36//! └─────────────────────┘
37//! ```
38//!
39//! ## Quick Start with FsInterface (Recommended)
40//!
41//! The `FsInterface` provides staging-based writes with automatic background commits:
42//!
43//! ```no_run
44//! use heroforge_core::Repository;
45//! use std::sync::Arc;
46//!
47//! fn main() -> heroforge_core::Result<()> {
48//! let repo = Arc::new(Repository::open_rw("project.forge")?);
49//! let fs = heroforge_core::fs::FsInterface::new(repo, "developer@example.com")?;
50//!
51//! // Writes go to staging (fast)
52//! fs.write_file("config.json", b"{}")?;
53//!
54//! // Reads check staging first, then database
55//! let content = fs.read_file("config.json")?;
56//!
57//! // Partial updates use read-modify-write pattern
58//! fs.write_at("data.bin", 100, b"updated")?;
59//!
60//! // Force immediate commit (normally auto-commits every 1 minute)
61//! fs.commit()?;
62//!
63//! Ok(())
64//! }
65//! ```
66//!
67//! ## Legacy FileSystem API
68//!
69//! The original `FileSystem` API is still available for direct database operations:
70//!
71//! ```no_run
72//! use heroforge_core::Repository;
73//! use heroforge_core::fs::FileSystem;
74//! use std::sync::Arc;
75//!
76//! fn main() -> heroforge_core::Result<()> {
77//! let repo = Arc::new(Repository::open_rw("project.forge")?);
78//! let fs = FileSystem::new(repo);
79//!
80//! // Direct database operations (no staging)
81//! fs.write_file("config.json", b"{}", "developer", "Add config")?;
82//!
83//! Ok(())
84//! }
85//! ```
86//!
87//! ## Staging Directory
88//!
89//! All write operations go to a **staging directory** first, not directly to SQLite:
90//!
91//! 1. **Fast writes**: Writing to filesystem is faster than SQLite transactions
92//! 2. **Frequent updates**: Files can be modified many times before commit
93//! 3. **Atomic commits**: All staged changes are committed together
94//! 4. **Crash recovery**: Uncommitted work is recoverable from staging
95//!
96//! **Current Limitations:**
97//! - Files larger than **2 MB** are not supported
98//! - Staging directory is local to the repository
99//!
100//! ## Read Path (Layered Lookup)
101//!
102//! When reading a file, the interface checks locations in this order:
103//!
104//! 1. **Staging Directory** → If file exists here, return it (most recent)
105//! 2. **`.forge` Database** → Query SQLite for committed version
106//! 3. **Return Error** → File does not exist
107//!
108//! ## Auto-Commit Behavior
109//!
110//! - Commits happen automatically every **1 minute**
111//! - During commit, an **exclusive write lock** is held
112//! - All operations are blocked during commit (typically milliseconds)
113//! - Forced commits happen on: branch change, tag creation, shutdown
114//!
115//! ## Thread Safety
116//!
117//! Uses `RwLock<StagingState>` for concurrent access:
118//!
119//! | Operation | Lock Type | Blocks |
120//! |-----------|-----------|--------|
121//! | `exists()` | Read | Nothing |
122//! | `read_file()` | Read | Nothing |
123//! | `write_file()` | Write | Other writes |
124//! | `delete_file()` | Write | Other writes |
125//! | **Commit** | **Exclusive** | **Everything** |
126//!
127//! ## Error Handling
128//!
129//! All operations return `FsResult<T>` which is `Result<T, FsError>`:
130//!
131//! ```no_run
132//! # use heroforge_core::Repository;
133//! # use std::sync::Arc;
134//! let repo = Arc::new(Repository::open_rw("repo.forge")?);
135//! let fs = heroforge_core::fs::FsInterface::new(repo, "user")?;
136//!
137//! match fs.read_file("missing.txt") {
138//! Ok(content) => println!("Read {} bytes", content.len()),
139//! Err(err) => eprintln!("Error: {}", err),
140//! }
141//! # Ok::<(), heroforge_core::FossilError>(())
142//! ```
143
144pub mod commit_thread;
145pub mod errors;
146pub mod find;
147pub mod fs_interface;
148pub mod interface;
149pub mod operations;
150pub mod ops;
151pub mod staging;
152pub mod transaction;
153
154pub use errors::{FsError, FsResult};
155pub use interface::{FileHandle, FileSystem, FileSystemStatus};
156pub use operations::{
157 DirectoryEntry, FileKind, FileMetadata, FilePermissions, FindResults, FsOperation,
158 OperationSummary,
159};
160pub use transaction::{SavePoint, Transaction, TransactionMode, TransactionState};
161
162// New staging-based interface
163pub use commit_thread::{CommitConfig, CommitWorker, DEFAULT_COMMIT_INTERVAL};
164pub use fs_interface::{FsInterface, FsInterfaceStatus};
165pub use staging::{MAX_FILE_SIZE, StagedFile, Staging, StagingState};
166
167// Find and high-level operations
168pub use find::{
169 FileEntry, FileType, Find, FindResult, Permissions, count, du, exists, find, is_dir,
170 list_symlinks, stat,
171};
172pub use ops::{Modify, Op, Preview};
173
174// Upload/Download (OS filesystem <-> .forge database)
175pub use ops::{
176 download, download_dir, download_from_branch, download_matching, upload, upload_dir,
177};
178
179/// Filesystem interface version
180pub const VERSION: &str = "1.0.0";
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_version() {
188 assert_eq!(VERSION, "1.0.0");
189 }
190}