ralph_workflow/workspace/files/
io.rs1use std::fs;
8use std::path::{Path, PathBuf};
9
10use crate::workspace::{DirEntry, Workspace};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum AtomicWriteSync {
14 Full,
15 SkipInterrupt,
16}
17
18pub fn decide_atomic_write_sync(interrupted: bool) -> AtomicWriteSync {
19 if interrupted {
20 AtomicWriteSync::SkipInterrupt
21 } else {
22 AtomicWriteSync::Full
23 }
24}
25
26pub fn sync_temp_file(file: &std::fs::File, policy: AtomicWriteSync) -> std::io::Result<()> {
27 match policy {
28 AtomicWriteSync::Full => {
29 file.sync_all()?;
30 Ok(())
31 }
32 AtomicWriteSync::SkipInterrupt => Ok(()),
33 }
34}
35
36#[cfg(unix)]
37pub fn set_restrictive_permissions(path: &std::path::Path) -> std::io::Result<()> {
38 use std::os::unix::fs::PermissionsExt;
39 let metadata = fs::metadata(path)?;
40 let mut perms = metadata.permissions();
41 perms.set_mode(0o600);
42 fs::set_permissions(path, perms)
43}
44
45#[cfg(not(unix))]
46pub fn set_restrictive_permissions(_path: &std::path::Path) -> std::io::Result<()> {
47 Ok(())
48}
49
50#[derive(Debug, Clone)]
54pub struct WorkspaceFs {
55 root: PathBuf,
56}
57
58impl WorkspaceFs {
59 #[must_use]
65 pub const fn new(repo_root: PathBuf) -> Self {
66 Self { root: repo_root }
67 }
68}
69
70impl Workspace for WorkspaceFs {
71 fn root(&self) -> &Path {
72 &self.root
73 }
74
75 fn read(&self, relative: &Path) -> std::io::Result<String> {
76 fs::read_to_string(self.root.join(relative))
77 }
78
79 fn read_bytes(&self, relative: &Path) -> std::io::Result<Vec<u8>> {
80 fs::read(self.root.join(relative))
81 }
82
83 fn write(&self, relative: &Path, content: &str) -> std::io::Result<()> {
84 let path = self.root.join(relative);
85 if let Some(parent) = path.parent() {
86 fs::create_dir_all(parent)?;
87 }
88 fs::write(path, content)
89 }
90
91 fn write_bytes(&self, relative: &Path, content: &[u8]) -> std::io::Result<()> {
92 let path = self.root.join(relative);
93 if let Some(parent) = path.parent() {
94 fs::create_dir_all(parent)?;
95 }
96 fs::write(path, content)
97 }
98
99 fn append_bytes(&self, relative: &Path, content: &[u8]) -> std::io::Result<()> {
100 let path = self.root.join(relative);
101 if let Some(parent) = path.parent() {
102 fs::create_dir_all(parent)?;
103 }
104 let mut file = fs::OpenOptions::new()
105 .create(true)
106 .append(true)
107 .open(path)?;
108 std::io::Write::write_all(&mut file, content)?;
109 std::io::Write::flush(&mut file)
110 }
111
112 fn exists(&self, relative: &Path) -> bool {
113 self.root.join(relative).exists()
114 }
115
116 fn is_file(&self, relative: &Path) -> bool {
117 self.root.join(relative).is_file()
118 }
119
120 fn is_dir(&self, relative: &Path) -> bool {
121 self.root.join(relative).is_dir()
122 }
123
124 fn remove(&self, relative: &Path) -> std::io::Result<()> {
125 fs::remove_file(self.root.join(relative))
126 }
127
128 fn remove_if_exists(&self, relative: &Path) -> std::io::Result<()> {
129 let path = self.root.join(relative);
130 if path.exists() {
131 fs::remove_file(path)?;
132 }
133 Ok(())
134 }
135
136 fn remove_dir_all(&self, relative: &Path) -> std::io::Result<()> {
137 fs::remove_dir_all(self.root.join(relative))
138 }
139
140 fn remove_dir_all_if_exists(&self, relative: &Path) -> std::io::Result<()> {
141 let path = self.root.join(relative);
142 if path.exists() {
143 fs::remove_dir_all(path)?;
144 }
145 Ok(())
146 }
147
148 fn create_dir_all(&self, relative: &Path) -> std::io::Result<()> {
149 fs::create_dir_all(self.root.join(relative))
150 }
151
152 fn read_dir(&self, relative: &Path) -> std::io::Result<Vec<DirEntry>> {
153 let abs_path = self.root.join(relative);
154 let entries: Vec<DirEntry> = fs::read_dir(abs_path)?
155 .map(|entry| -> std::io::Result<DirEntry> {
156 let entry = entry?;
157 let metadata = entry.metadata()?;
158 let rel_path = relative.join(entry.file_name());
159 let modified = metadata.modified().ok();
160 Ok(if let Some(mod_time) = modified {
161 DirEntry::with_modified(
162 rel_path,
163 metadata.is_file(),
164 metadata.is_dir(),
165 mod_time,
166 )
167 } else {
168 DirEntry::new(rel_path, metadata.is_file(), metadata.is_dir())
169 })
170 })
171 .collect::<std::io::Result<Vec<_>>>()?;
172 Ok(entries)
173 }
174
175 fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
176 fs::rename(self.root.join(from), self.root.join(to))
177 }
178
179 fn write_atomic(&self, relative: &Path, content: &str) -> std::io::Result<()> {
180 use tempfile::NamedTempFile;
181
182 let path = self.root.join(relative);
183
184 if let Some(parent) = path.parent() {
185 fs::create_dir_all(parent)?;
186 }
187
188 let parent_dir = path.parent().unwrap_or_else(|| Path::new("."));
189 let mut temp_file = NamedTempFile::new_in(parent_dir)?;
190
191 #[cfg(unix)]
192 set_restrictive_permissions(temp_file.path())?;
193
194 std::io::Write::write_all(&mut temp_file, content.as_bytes())?;
195 std::io::Write::flush(&mut temp_file)?;
196
197 let policy = decide_atomic_write_sync(crate::interrupt::user_interrupted_occurred());
198 sync_temp_file(temp_file.as_file(), policy)?;
199
200 temp_file.persist(&path).map_err(|e| e.error)?;
201
202 Ok(())
203 }
204
205 fn set_readonly(&self, relative: &Path) -> std::io::Result<()> {
206 let path = self.root.join(relative);
207 if !path.exists() {
208 return Ok(());
209 }
210
211 let metadata = fs::metadata(&path)?;
212 let mut perms = metadata.permissions();
213
214 #[cfg(unix)]
215 {
216 use std::os::unix::fs::PermissionsExt;
217 perms.set_mode(0o444);
218 }
219
220 #[cfg(windows)]
221 {
222 perms.set_readonly(true);
223 }
224
225 fs::set_permissions(path, perms)
226 }
227
228 fn set_writable(&self, relative: &Path) -> std::io::Result<()> {
229 let path = self.root.join(relative);
230 if !path.exists() {
231 return Ok(());
232 }
233
234 let metadata = fs::metadata(&path)?;
235 let mut perms = metadata.permissions();
236
237 #[cfg(unix)]
238 {
239 use std::os::unix::fs::PermissionsExt;
240 perms.set_mode(0o644);
241 }
242
243 #[cfg(windows)]
244 {
245 perms.set_readonly(false);
246 }
247
248 fs::set_permissions(path, perms)
249 }
250}