1use crate::error::HasOtherError;
23use anyhow::Error as AnyhowError;
24use std::error::Error as StdError;
25use std::fmt::{Debug, Display};
26use std::fs::{create_dir_all, write, File, OpenOptions};
27use std::io::{Error as IOError, Write};
28use std::path::{Path, PathBuf};
29use std::result::Result as StdResult;
30use thiserror::Error;
31
32#[allow(unused)]
33#[derive(Debug, PartialEq)]
34#[non_exhaustive]
35pub enum FileWriteErrorKind {
36 AlreadyExists,
37 Other,
38}
39
40#[derive(Debug, Error)]
41#[error(transparent)]
42pub struct FileWriteError(#[from] FileWriteErrorImpl);
43
44impl FileWriteError {
45 #[allow(unused)]
46 #[must_use]
47 pub const fn kind(&self) -> FileWriteErrorKind {
48 match self.0 {
49 FileWriteErrorImpl::AlreadyExists(_) => FileWriteErrorKind::AlreadyExists,
50 _ => FileWriteErrorKind::Other,
51 }
52 }
53
54 #[allow(unused)]
55 #[must_use]
56 pub fn is_already_exists(&self) -> bool {
57 self.kind() == FileWriteErrorKind::AlreadyExists
58 }
59
60 #[allow(unused)]
61 #[must_use]
62 pub fn is_other(&self) -> bool {
63 self.kind() == FileWriteErrorKind::Other
64 }
65
66 fn other<E>(e: E) -> Self
67 where
68 E: StdError + Send + Sync + 'static,
69 {
70 Self(FileWriteErrorImpl::Other(AnyhowError::new(e)))
71 }
72
73 fn convert(e: IOError, path: &Path) -> Self {
74 use std::io::ErrorKind::*;
75 match e.kind() {
76 AlreadyExists => Self(FileWriteErrorImpl::AlreadyExists(path.to_path_buf())),
77 _ => Self::other(e),
78 }
79 }
80}
81
82impl HasOtherError for FileWriteError {
83 fn is_other(&self) -> bool {
84 self.is_other()
85 }
86
87 fn downcast_other_ref<E>(&self) -> Option<&E>
88 where
89 E: Display + Debug + Send + Sync + 'static,
90 {
91 if let FileWriteErrorImpl::Other(ref inner) = self.0 {
92 inner.downcast_ref::<E>()
93 } else {
94 None
95 }
96 }
97}
98
99#[derive(Debug, Error)]
100enum FileWriteErrorImpl {
101 #[error("File {0} already exists")]
102 AlreadyExists(PathBuf),
103 #[error(transparent)]
104 Other(AnyhowError),
105}
106
107#[allow(unused)]
108pub fn safe_create_file(path: &Path, overwrite: bool) -> StdResult<File, FileWriteError> {
109 ensure_dir(path)?;
110
111 let mut options = OpenOptions::new();
112 options.write(true);
113 if overwrite {
114 options.create(true);
115 } else {
116 options.create_new(true);
117 }
118
119 options
120 .open(path)
121 .map_err(|e| FileWriteError::convert(e, path))
122}
123
124#[allow(unused)]
125pub fn safe_write_file<C>(
126 path: &Path,
127 contents: C,
128 overwrite: bool,
129) -> StdResult<(), FileWriteError>
130where
131 C: AsRef<[u8]>,
132{
133 ensure_dir(path)?;
134
135 if overwrite {
136 write(path, contents).map_err(|e| FileWriteError::convert(e, path))?;
137 } else {
138 let mut file = safe_create_file(path, overwrite)?;
139 file.write_all(contents.as_ref())
140 .map_err(|e| FileWriteError::convert(e, path))?;
141 }
142
143 Ok(())
144}
145
146fn ensure_dir(file_path: &Path) -> StdResult<(), FileWriteError> {
147 let mut dir = PathBuf::new();
148 dir.push(file_path);
149 dir.pop();
150 create_dir_all(&dir).map_err(FileWriteError::other)?;
151 Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156 use super::{safe_create_file, safe_write_file, FileWriteErrorKind};
157 use anyhow::Result;
158 use std::fs::{read_to_string, write};
159 use std::io::Write;
160 use tempdir::TempDir;
161
162 #[test]
163 fn test_safe_create_file_no_overwrite_succeeds() -> Result<()> {
164 let temp_dir = TempDir::new("joatmon-test")?;
166 let path = temp_dir.path().join("file.txt");
167
168 let mut file = safe_create_file(&path, false)?;
170 file.write_all(b"hello-world")?;
171
172 assert_eq!("hello-world", read_to_string(&path)?);
174 Ok(())
175 }
176
177 #[test]
178 fn test_safe_create_file_overwrite_succeeds() -> Result<()> {
179 let temp_dir = TempDir::new("joatmon-test")?;
181 let path = temp_dir.path().join("file.txt");
182
183 let mut file = safe_create_file(&path, true)?;
185 file.write_all(b"hello-world")?;
186
187 assert_eq!("hello-world", read_to_string(&path)?);
189 Ok(())
190 }
191
192 #[test]
193 fn test_safe_create_file_exists_no_overwrite_fails() -> Result<()> {
194 let temp_dir = TempDir::new("joatmon-test")?;
196 let path = temp_dir.path().join("file.txt");
197 write(&path, "hello-world")?;
198
199 let e = match safe_create_file(&path, false) {
201 Ok(_) => panic!("safe_create_file must fail"),
202 Err(e) => e,
203 };
204
205 assert_eq!(FileWriteErrorKind::AlreadyExists, e.kind());
207 assert!(e.is_already_exists());
208 assert!(!e.is_other());
209 let message = format!("{e}");
210 assert!(message.contains(path.to_str().expect("must be valid string")));
211 assert_eq!("hello-world", read_to_string(&path)?);
212 Ok(())
213 }
214
215 #[test]
216 fn test_safe_create_file_exists_overwrite_succeeds() -> Result<()> {
217 let temp_dir = TempDir::new("joatmon-test")?;
219 let path = temp_dir.path().join("file.txt");
220 write(&path, "hello-world")?;
221
222 let mut file = safe_create_file(&path, true)?;
224 file.write_all(b"something-else")?;
225
226 assert_eq!("something-else", read_to_string(&path)?);
228 Ok(())
229 }
230
231 #[test]
232 fn test_safe_write_file_no_overwrite_succeeds() -> Result<()> {
233 let temp_dir = TempDir::new("joatmon-test")?;
235 let path = temp_dir.path().join("file.txt");
236
237 safe_write_file(&path, "hello-world", false)?;
239
240 assert_eq!("hello-world", read_to_string(&path)?);
242 Ok(())
243 }
244
245 #[test]
246 fn test_safe_write_file_overwrite_succeeds() -> Result<()> {
247 let temp_dir = TempDir::new("joatmon-test")?;
249 let path = temp_dir.path().join("file.txt");
250
251 safe_write_file(&path, "hello-world", true)?;
253
254 assert_eq!("hello-world", read_to_string(&path)?);
256 Ok(())
257 }
258
259 #[test]
260 fn test_safe_write_file_exists_no_overwrite_fails() -> Result<()> {
261 let temp_dir = TempDir::new("joatmon-test")?;
263 let path = temp_dir.path().join("file.txt");
264 write(&path, "hello-world")?;
265
266 let e = match safe_write_file(&path, "something-else", false) {
268 Ok(_) => panic!("safe_write_file must fail"),
269 Err(e) => e,
270 };
271
272 assert_eq!(FileWriteErrorKind::AlreadyExists, e.kind());
274 assert!(e.is_already_exists());
275 assert!(!e.is_other());
276 let message = format!("{e}");
277 assert!(message.contains(path.to_str().expect("must be valid string")));
278 assert_eq!("hello-world", read_to_string(&path)?);
279 Ok(())
280 }
281
282 #[test]
283 fn test_safe_write_file_exists_overwrite_succeeds() -> Result<()> {
284 let temp_dir = TempDir::new("joatmon-test")?;
286 let path = temp_dir.path().join("file.txt");
287 write(&path, "hello-world")?;
288
289 safe_write_file(&path, "something-else", true)?;
291
292 assert_eq!("something-else", read_to_string(&path)?);
294 Ok(())
295 }
296}