Skip to main content

ralph_workflow/workspace/
workspace_fs.rs

1// WorkspaceFs - Production filesystem implementation of the Workspace trait.
2//
3// This file contains the production implementation that performs actual
4// filesystem operations relative to the repository root.
5
6/// Production workspace implementation using the real filesystem.
7///
8/// All file operations are performed relative to the repository root using `std::fs`.
9#[derive(Debug, Clone)]
10pub struct WorkspaceFs {
11    root: PathBuf,
12}
13
14impl WorkspaceFs {
15    /// Create a new workspace filesystem rooted at the given path.
16    ///
17    /// # Arguments
18    ///
19    /// * `repo_root` - The repository root directory (typically discovered via git)
20    pub fn new(repo_root: PathBuf) -> Self {
21        Self { root: repo_root }
22    }
23}
24
25impl Workspace for WorkspaceFs {
26    fn root(&self) -> &Path {
27        &self.root
28    }
29
30    fn read(&self, relative: &Path) -> io::Result<String> {
31        fs::read_to_string(self.root.join(relative))
32    }
33
34    fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>> {
35        fs::read(self.root.join(relative))
36    }
37
38    fn write(&self, relative: &Path, content: &str) -> io::Result<()> {
39        let path = self.root.join(relative);
40        if let Some(parent) = path.parent() {
41            fs::create_dir_all(parent)?;
42        }
43        fs::write(path, content)
44    }
45
46    fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
47        let path = self.root.join(relative);
48        if let Some(parent) = path.parent() {
49            fs::create_dir_all(parent)?;
50        }
51        fs::write(path, content)
52    }
53
54    fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
55        use std::io::Write;
56        let path = self.root.join(relative);
57        if let Some(parent) = path.parent() {
58            fs::create_dir_all(parent)?;
59        }
60        let mut file = fs::OpenOptions::new()
61            .create(true)
62            .append(true)
63            .open(path)?;
64        file.write_all(content)?;
65        file.flush()
66    }
67
68    fn exists(&self, relative: &Path) -> bool {
69        self.root.join(relative).exists()
70    }
71
72    fn is_file(&self, relative: &Path) -> bool {
73        self.root.join(relative).is_file()
74    }
75
76    fn is_dir(&self, relative: &Path) -> bool {
77        self.root.join(relative).is_dir()
78    }
79
80    fn remove(&self, relative: &Path) -> io::Result<()> {
81        fs::remove_file(self.root.join(relative))
82    }
83
84    fn remove_if_exists(&self, relative: &Path) -> io::Result<()> {
85        let path = self.root.join(relative);
86        if path.exists() {
87            fs::remove_file(path)?;
88        }
89        Ok(())
90    }
91
92    fn remove_dir_all(&self, relative: &Path) -> io::Result<()> {
93        fs::remove_dir_all(self.root.join(relative))
94    }
95
96    fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()> {
97        let path = self.root.join(relative);
98        if path.exists() {
99            fs::remove_dir_all(path)?;
100        }
101        Ok(())
102    }
103
104    fn create_dir_all(&self, relative: &Path) -> io::Result<()> {
105        fs::create_dir_all(self.root.join(relative))
106    }
107
108    fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>> {
109        let abs_path = self.root.join(relative);
110        let mut entries = Vec::new();
111        for entry in fs::read_dir(abs_path)? {
112            let entry = entry?;
113            let metadata = entry.metadata()?;
114            // Store relative path from workspace root
115            let rel_path = relative.join(entry.file_name());
116            let modified = metadata.modified().ok();
117            if let Some(mod_time) = modified {
118                entries.push(DirEntry::with_modified(
119                    rel_path,
120                    metadata.is_file(),
121                    metadata.is_dir(),
122                    mod_time,
123                ));
124            } else {
125                entries.push(DirEntry::new(
126                    rel_path,
127                    metadata.is_file(),
128                    metadata.is_dir(),
129                ));
130            }
131        }
132        Ok(entries)
133    }
134
135    fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
136        fs::rename(self.root.join(from), self.root.join(to))
137    }
138
139    fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()> {
140        use std::io::Write;
141        use tempfile::NamedTempFile;
142
143        let path = self.root.join(relative);
144
145        // Create parent directories if needed
146        if let Some(parent) = path.parent() {
147            fs::create_dir_all(parent)?;
148        }
149
150        // Create a NamedTempFile in the same directory as the target file.
151        // This ensures atomic rename works (same filesystem).
152        let parent_dir = path.parent().unwrap_or_else(|| Path::new("."));
153        let mut temp_file = NamedTempFile::new_in(parent_dir)?;
154
155        // Set restrictive permissions on temp file (0600 = owner read/write only)
156        // This prevents other users from reading the temp file before rename
157        #[cfg(unix)]
158        {
159            use std::os::unix::fs::PermissionsExt;
160            let mut perms = fs::metadata(temp_file.path())?.permissions();
161            perms.set_mode(0o600);
162            fs::set_permissions(temp_file.path(), perms)?;
163        }
164
165        // Write content to the temp file
166        temp_file.write_all(content.as_bytes())?;
167        temp_file.flush()?;
168        temp_file.as_file().sync_all()?;
169
170        // Persist the temp file to the target location (atomic rename)
171        temp_file.persist(&path).map_err(|e| e.error)?;
172
173        Ok(())
174    }
175
176    fn set_readonly(&self, relative: &Path) -> io::Result<()> {
177        let path = self.root.join(relative);
178        if !path.exists() {
179            return Ok(());
180        }
181
182        let metadata = fs::metadata(&path)?;
183        let mut perms = metadata.permissions();
184
185        #[cfg(unix)]
186        {
187            use std::os::unix::fs::PermissionsExt;
188            perms.set_mode(0o444);
189        }
190
191        #[cfg(windows)]
192        {
193            perms.set_readonly(true);
194        }
195
196        fs::set_permissions(path, perms)
197    }
198
199    fn set_writable(&self, relative: &Path) -> io::Result<()> {
200        let path = self.root.join(relative);
201        if !path.exists() {
202            return Ok(());
203        }
204
205        let metadata = fs::metadata(&path)?;
206        let mut perms = metadata.permissions();
207
208        #[cfg(unix)]
209        {
210            use std::os::unix::fs::PermissionsExt;
211            perms.set_mode(0o644);
212        }
213
214        #[cfg(windows)]
215        {
216            perms.set_readonly(false);
217        }
218
219        fs::set_permissions(path, perms)
220    }
221}