1use bids_core::error::{BidsError, Result};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ConflictStrategy {
22 #[default]
24 Fail,
25 Skip,
27 Overwrite,
29 Append,
31}
32
33impl std::fmt::Display for ConflictStrategy {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::Fail => write!(f, "fail"),
37 Self::Skip => write!(f, "skip"),
38 Self::Overwrite => write!(f, "overwrite"),
39 Self::Append => write!(f, "append"),
40 }
41 }
42}
43
44pub fn write_to_file(
54 path: &Path,
55 contents: Option<&[u8]>,
56 link_to: Option<&Path>,
57 copy_from: Option<&Path>,
58 root: Option<&Path>,
59 conflicts: ConflictStrategy,
60) -> Result<()> {
61 let mut full_path = match root {
62 Some(r) if !path.is_absolute() => r.join(path),
63 _ => path.to_path_buf(),
64 };
65
66 if full_path.exists() || full_path.is_symlink() {
67 match conflicts {
68 ConflictStrategy::Fail => {
69 return Err(BidsError::Io(std::io::Error::new(
70 std::io::ErrorKind::AlreadyExists,
71 format!("A file at path {} already exists", full_path.display()),
72 )));
73 }
74 ConflictStrategy::Skip => return Ok(()),
75 ConflictStrategy::Overwrite => {
76 if full_path.is_dir() {
77 return Ok(()); }
79 std::fs::remove_file(&full_path)?;
80 }
81 ConflictStrategy::Append => {
82 full_path = find_append_path(&full_path);
83 }
84 }
85 }
86
87 if let Some(parent) = full_path.parent() {
89 std::fs::create_dir_all(parent)?;
90 }
91
92 if let Some(link_target) = link_to {
93 #[cfg(unix)]
94 std::os::unix::fs::symlink(link_target, &full_path)?;
95 #[cfg(not(unix))]
96 std::fs::copy(link_target, &full_path)?;
97 } else if let Some(src) = copy_from {
98 if !src.exists() {
99 return Err(BidsError::Io(std::io::Error::new(
100 std::io::ErrorKind::NotFound,
101 format!("Source file '{}' does not exist", src.display()),
102 )));
103 }
104 std::fs::copy(src, &full_path)?;
105 } else if let Some(data) = contents {
106 std::fs::write(&full_path, data)?;
107 } else {
108 return Err(BidsError::Io(std::io::Error::new(
109 std::io::ErrorKind::InvalidInput,
110 "One of contents, copy_from or link_to must be provided",
111 )));
112 }
113
114 Ok(())
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::fs;
121
122 #[test]
123 fn test_write_contents() {
124 let dir = std::env::temp_dir().join("bids_writer_test_contents");
125 fs::create_dir_all(&dir).unwrap();
126 let path = dir.join("test.txt");
127
128 write_to_file(
129 &path,
130 Some(b"hello"),
131 None,
132 None,
133 None,
134 ConflictStrategy::Fail,
135 )
136 .unwrap();
137 assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
138
139 fs::remove_dir_all(&dir).unwrap();
140 }
141
142 #[test]
143 fn test_conflict_fail() {
144 let dir = std::env::temp_dir().join("bids_writer_test_fail");
145 fs::create_dir_all(&dir).unwrap();
146 let path = dir.join("test.txt");
147
148 write_to_file(
149 &path,
150 Some(b"first"),
151 None,
152 None,
153 None,
154 ConflictStrategy::Fail,
155 )
156 .unwrap();
157 let result = write_to_file(
158 &path,
159 Some(b"second"),
160 None,
161 None,
162 None,
163 ConflictStrategy::Fail,
164 );
165 assert!(result.is_err());
166
167 fs::remove_dir_all(&dir).unwrap();
168 }
169
170 #[test]
171 fn test_conflict_skip() {
172 let dir = std::env::temp_dir().join("bids_writer_test_skip");
173 fs::create_dir_all(&dir).unwrap();
174 let path = dir.join("test.txt");
175
176 write_to_file(
177 &path,
178 Some(b"first"),
179 None,
180 None,
181 None,
182 ConflictStrategy::Fail,
183 )
184 .unwrap();
185 write_to_file(
186 &path,
187 Some(b"second"),
188 None,
189 None,
190 None,
191 ConflictStrategy::Skip,
192 )
193 .unwrap();
194 assert_eq!(fs::read_to_string(&path).unwrap(), "first"); fs::remove_dir_all(&dir).unwrap();
197 }
198
199 #[test]
200 fn test_conflict_overwrite() {
201 let dir = std::env::temp_dir().join("bids_writer_test_overwrite");
202 fs::create_dir_all(&dir).unwrap();
203 let path = dir.join("test.txt");
204
205 write_to_file(
206 &path,
207 Some(b"first"),
208 None,
209 None,
210 None,
211 ConflictStrategy::Fail,
212 )
213 .unwrap();
214 write_to_file(
215 &path,
216 Some(b"second"),
217 None,
218 None,
219 None,
220 ConflictStrategy::Overwrite,
221 )
222 .unwrap();
223 assert_eq!(fs::read_to_string(&path).unwrap(), "second");
224
225 fs::remove_dir_all(&dir).unwrap();
226 }
227
228 #[test]
229 fn test_conflict_append() {
230 let dir = std::env::temp_dir().join("bids_writer_test_append");
231 fs::create_dir_all(&dir).unwrap();
232 let path = dir.join("test.txt");
233
234 write_to_file(
235 &path,
236 Some(b"first"),
237 None,
238 None,
239 None,
240 ConflictStrategy::Fail,
241 )
242 .unwrap();
243 write_to_file(
244 &path,
245 Some(b"second"),
246 None,
247 None,
248 None,
249 ConflictStrategy::Append,
250 )
251 .unwrap();
252
253 assert_eq!(fs::read_to_string(&path).unwrap(), "first");
255 assert_eq!(
256 fs::read_to_string(dir.join("test_1.txt")).unwrap(),
257 "second"
258 );
259
260 fs::remove_dir_all(&dir).unwrap();
261 }
262
263 #[test]
264 fn test_creates_parent_dirs() {
265 let dir = std::env::temp_dir().join("bids_writer_test_parents");
266 let path = dir.join("a").join("b").join("c").join("test.txt");
267
268 write_to_file(
269 &path,
270 Some(b"deep"),
271 None,
272 None,
273 None,
274 ConflictStrategy::Fail,
275 )
276 .unwrap();
277 assert_eq!(fs::read_to_string(&path).unwrap(), "deep");
278
279 fs::remove_dir_all(&dir).unwrap();
280 }
281
282 #[test]
283 fn test_copy_from() {
284 let dir = std::env::temp_dir().join("bids_writer_test_copy");
285 fs::create_dir_all(&dir).unwrap();
286 let src = dir.join("source.txt");
287 let dst = dir.join("dest.txt");
288
289 fs::write(&src, b"source content").unwrap();
290 write_to_file(&dst, None, None, Some(&src), None, ConflictStrategy::Fail).unwrap();
291 assert_eq!(fs::read_to_string(&dst).unwrap(), "source content");
292
293 fs::remove_dir_all(&dir).unwrap();
294 }
295
296 #[test]
297 fn test_no_source_errors() {
298 let dir = std::env::temp_dir().join("bids_writer_test_nosrc");
299 fs::create_dir_all(&dir).unwrap();
300 let path = dir.join("test.txt");
301
302 let result = write_to_file(&path, None, None, None, None, ConflictStrategy::Fail);
303 assert!(result.is_err());
304
305 fs::remove_dir_all(&dir).unwrap();
306 }
307}
308
309fn find_append_path(path: &Path) -> PathBuf {
310 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
311 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
312 let parent = path.parent().unwrap_or(Path::new("."));
313
314 for i in 1..i32::MAX {
315 let new_name = if ext.is_empty() {
316 format!("{stem}_{i}")
317 } else {
318 format!("{stem}_{i}.{ext}")
319 };
320 let candidate = parent.join(new_name);
321 if !candidate.exists() {
322 return candidate;
323 }
324 }
325 path.to_path_buf() }