ragit_fs/
lib.rs

1#![allow(dead_code)]
2
3#[cfg(feature = "log")]
4mod log;
5
6#[cfg(feature = "log")]
7pub use crate::log::{initialize_log, write_log};
8
9use std::collections::hash_map;
10use std::ffi::OsString;
11use std::fmt;
12use std::fs::{self, File, OpenOptions};
13use std::hash::{Hash, Hasher};
14use std::io::{self, Read, Seek, SeekFrom, Write};
15use std::path::{Path, PathBuf};
16use std::str::FromStr;
17
18/// ```nohighlight
19///       File Already Exists    File Does not Exist
20///
21///     AA       Append                  Dies
22///    AoC       Append                 Create
23///    CoT      Truncate                Create
24///     AC        Dies                  Create
25/// ```
26///
27/// `Atomic` is like `CreateOrTruncate`, but it tries to be more atomic.
28/// It first creates a tmp file with a different name, then renames the tmp file.
29/// If it fails, it might leave a tmp file. But you'll never have a partially
30/// written file.
31#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
32pub enum WriteMode {
33    AlwaysAppend,
34    AppendOrCreate,
35    CreateOrTruncate,
36    AlwaysCreate,
37    Atomic,
38}
39
40impl From<WriteMode> for OpenOptions {
41    fn from(m: WriteMode) -> OpenOptions {
42        let mut result = OpenOptions::new();
43
44        match m {
45            WriteMode::AlwaysAppend => { result.append(true); },
46            WriteMode::AppendOrCreate => { result.append(true).create(true); },
47            WriteMode::CreateOrTruncate | WriteMode::Atomic => { result.write(true).truncate(true).create(true); },
48            WriteMode::AlwaysCreate => { result.write(true).create_new(true); },
49        }
50
51        result
52    }
53}
54
55/// It never reads more than `to - from` bytes.
56/// If it fails to read from `from`, that's an error.
57/// If it fails to read to `to`, that's not an error.
58pub fn read_bytes_offset(path: &str, from: u64, to: u64) -> Result<Vec<u8>, FileError> {
59    assert!(to >= from);
60
61    match File::open(path) {
62        Err(e) => Err(FileError::from_std(e, path)),
63        Ok(mut f) => match f.seek(SeekFrom::Start(from)) {
64            Err(e) => Err(FileError::from_std(e, path)),
65            Ok(_) => {
66                let mut handle = f.take(to - from);
67                let mut buffer = Vec::with_capacity((to - from) as usize);
68
69                if let Err(e) = handle.read_to_end(&mut buffer) {
70                    return Err(FileError::from_std(e, path));
71                }
72
73                Ok(buffer)
74            },
75        },
76    }
77}
78
79pub fn read_bytes(path: &str) -> Result<Vec<u8>, FileError> {
80    fs::read(path).map_err(|e| FileError::from_std(e, path))
81}
82
83pub fn read_string(path: &str) -> Result<String, FileError> {
84    let mut s = String::new();
85
86    match File::open(path) {
87        Err(e) => Err(FileError::from_std(e, path)),
88        Ok(mut f) => match f.read_to_string(&mut s) {
89            Ok(_) => Ok(s),
90            Err(e) => Err(FileError::from_std(e, path)),
91        }
92    }
93}
94
95pub fn write_bytes(path: &str, bytes: &[u8], write_mode: WriteMode) -> Result<(), FileError> {
96    let option: OpenOptions = write_mode.into();
97
98    if let WriteMode::Atomic = write_mode {
99        // it has to create a unique name in extreme cases (e.g. 1k processes trying to write the same file)
100        // I cannot come up with better idea than this
101        let tmp_path = format!("{path}_tmp__{:x}", rand::random::<u64>());
102
103        match option.open(&tmp_path) {
104            Ok(mut f) => match f.write_all(bytes) {
105                Ok(_) => match rename(&tmp_path, path) {
106                    Ok(_) => Ok(()),
107                    Err(e) => {
108                        remove_file(&tmp_path)?;
109                        Err(e)
110                    },
111                },
112                Err(e) => {
113                    remove_file(&tmp_path)?;
114                    Err(FileError::from_std(e, path))
115                },
116            },
117            Err(e) => Err(FileError::from_std(e, path)),
118        }
119    } else {
120        match option.open(path) {
121            Ok(mut f) => match f.write_all(bytes) {
122                Ok(_) => Ok(()),
123                Err(e) => Err(FileError::from_std(e, path)),
124            },
125            Err(e) => Err(FileError::from_std(e, path)),
126        }
127    }
128}
129
130pub fn write_string(path: &str, s: &str, write_mode: WriteMode) -> Result<(), FileError> {
131    write_bytes(path, s.as_bytes(), write_mode)
132}
133
134/// `a/b/c.d` -> `c`
135pub fn file_name(path: &str) -> Result<String, FileError> {
136    let path_buf = PathBuf::from_str(path).unwrap();  // it's infallible
137
138    match path_buf.file_stem() {
139        None => Ok(String::new()),
140        Some(s) => match s.to_str() {
141            Some(ext) => Ok(ext.to_string()),
142            None => Err(FileError::os_str_err(s.to_os_string())),
143        }
144    }
145}
146
147/// `a/b/c.d` -> `d`
148pub fn extension(path: &str) -> Result<Option<String>, FileError> {
149    let path_buf = PathBuf::from_str(path).unwrap();  // it's infallible
150
151    match path_buf.extension() {
152        None => Ok(None),
153        Some(s) => match s.to_str() {
154            Some(ext) => Ok(Some(ext.to_string())),
155            None => Err(FileError::os_str_err(s.to_os_string())),
156        }
157    }
158}
159
160/// `a/b/c.d` -> `c.d`
161pub fn basename(path: &str) -> Result<String, FileError> {
162    let path_buf = PathBuf::from_str(path).unwrap();  // it's infallible
163
164    match path_buf.file_name() {
165        None => Ok(String::new()),  // when the path terminates in `..`
166        Some(s) => match s.to_str() {
167            Some(ext) => Ok(ext.to_string()),
168            None => Err(FileError::os_str_err(s.to_os_string())),
169        }
170    }
171}
172
173/// `a/b/`, `c.d` -> `a/b/c.d`
174pub fn join(path: &str, child: &str) -> Result<String, FileError> {
175    let mut path_buf = PathBuf::from_str(path).unwrap();  // Infallible
176    let child = PathBuf::from_str(child).unwrap();  // Infallible
177
178    path_buf.push(child);
179
180    match path_buf.to_str() {
181        Some(result) => Ok(result.to_string()),
182        None => Err(FileError::os_str_err(path_buf.into_os_string())),
183    }
184}
185
186pub fn temp_dir() -> Result<String, FileError> {
187    let temp_dir = std::env::temp_dir();
188
189    match temp_dir.to_str() {
190        Some(result) => Ok(result.to_string()),
191        None => Err(FileError::os_str_err(temp_dir.into_os_string())),
192    }
193}
194
195/// alias for `join`
196#[inline]
197pub fn join2(path: &str, child: &str) -> Result<String, FileError> {
198    join(path, child)
199}
200
201pub fn join3(path1: &str, path2: &str, path3: &str) -> Result<String, FileError> {
202    join(
203        path1,
204        &join(path2, path3)?,
205    )
206}
207
208pub fn join4(path1: &str, path2: &str, path3: &str, path4: &str) -> Result<String, FileError> {
209    join(
210        &join(path1, path2)?,
211        &join(path3, path4)?,
212    )
213}
214
215pub fn join5(path1: &str, path2: &str, path3: &str, path4: &str, path5: &str) -> Result<String, FileError> {
216    join(
217        &join(path1, path2)?,
218        &join(path3, &join(path4, path5)?)?,
219    )
220}
221
222/// `a/b/c.d, e` -> `a/b/c.e`
223pub fn set_extension(path: &str, ext: &str) -> Result<String, FileError> {
224    let mut path_buf = PathBuf::from_str(path).unwrap();  // Infallible
225
226    if path_buf.set_extension(ext) {
227        match path_buf.to_str() {
228            Some(result) => Ok(result.to_string()),
229            None => Err(FileError::os_str_err(path_buf.into_os_string())),
230        }
231    } else {
232        // has no filename
233        Ok(path.to_string())
234    }
235}
236
237/// It returns `false` if `path` doesn't exist
238pub fn is_dir(path: &str) -> bool {
239    PathBuf::from_str(path).map(|path| path.is_dir()).unwrap_or(false)
240}
241
242/// It returns `false` if `path` doesn't exist
243pub fn is_symlink(path: &str) -> bool {
244    PathBuf::from_str(path).map(|path| path.is_symlink()).unwrap_or(false)
245}
246
247pub fn exists(path: &str) -> bool {
248    PathBuf::from_str(path).map(|path| path.exists()).unwrap_or(false)
249}
250
251/// `a/b/c.d` -> `a/b/`
252pub fn parent(path: &str) -> Result<String, FileError> {
253    let std_path = Path::new(path);
254
255    std_path.parent().map(
256        |p| p.to_string_lossy().to_string()
257    ).ok_or_else(
258        || FileError::unknown(
259            String::from("function `parent` died"),
260            Some(path.to_string()),
261        )
262    )
263}
264
265/// It's like `create_dir` but does not raise an error if `path` already exists
266pub fn try_create_dir(path: &str) -> Result<(), FileError> {
267    match fs::create_dir(path) {
268        Ok(()) => Ok(()),
269        Err(e) => match e.kind() {
270            io::ErrorKind::AlreadyExists => Ok(()),
271            _ => Err(FileError::from_std(e, path)),
272        },
273    }
274}
275
276pub fn create_dir(path: &str) -> Result<(), FileError> {
277    fs::create_dir(path).map_err(|e| FileError::from_std(e, path))
278}
279
280pub fn create_dir_all(path: &str) -> Result<(), FileError> {
281    fs::create_dir_all(path).map_err(|e| FileError::from_std(e, path))
282}
283
284pub fn rename(from: &str, to: &str) -> Result<(), FileError> {
285    fs::rename(from, to).map_err(|e| FileError::from_std(e, from))
286}
287
288pub fn copy_dir(src: &str, dst: &str) -> Result<(), FileError> {
289    create_dir_all(dst)?;
290
291    // TODO: how about links?
292    for e in read_dir(src, false)? {
293        let new_dst = join(dst, &basename(&e)?)?;
294
295        if is_dir(&e) {
296            create_dir_all(&new_dst)?;
297            copy_dir(&e, &new_dst)?;
298        }
299
300        else {
301            copy_file(&e, &new_dst)?;
302        }
303    }
304
305    Ok(())
306}
307
308/// It returns the total number of bytes copied.
309pub fn copy_file(src: &str, dst: &str) -> Result<u64, FileError> {
310    std::fs::copy(src, dst).map_err(|e| FileError::from_std(e, src))  // TODO: how about dst?
311}
312
313// it only returns the hash value of the modified time
314pub fn last_modified(path: &str) -> Result<u64, FileError> {
315    match fs::metadata(path) {
316        Ok(m) => match m.modified() {
317            Ok(m) => {
318                let mut hasher = hash_map::DefaultHasher::new();
319                m.hash(&mut hasher);
320                let hash = hasher.finish();
321
322                Ok(hash)
323            },
324            Err(e) => Err(FileError::from_std(e, path)),
325        },
326        Err(e) => Err(FileError::from_std(e, path)),
327    }
328}
329
330pub fn file_size(path: &str) -> Result<u64, FileError> {
331    match fs::metadata(path) {
332        Ok(m) => Ok(m.len()),
333        Err(e) => Err(FileError::from_std(e, path)),
334    }
335}
336
337pub fn read_dir(path: &str, sort: bool) -> Result<Vec<String>, FileError> {
338    match fs::read_dir(path) {
339        Err(e) => Err(FileError::from_std(e, path)),
340        Ok(entries) => {
341            let mut result = vec![];
342
343            for entry in entries {
344                match entry {
345                    Err(e) => {
346                        return Err(FileError::from_std(e, path));
347                    },
348                    Ok(e) => {
349                        if let Some(ee) = e.path().to_str() {
350                            result.push(ee.to_string());
351                        }
352                    },
353                }
354            }
355
356            if sort {
357                result.sort();
358            }
359
360            Ok(result)
361        }
362    }
363}
364
365pub fn remove_file(path: &str) -> Result<(), FileError> {
366    fs::remove_file(path).map_err(|e| FileError::from_std(e, path))
367}
368
369pub fn remove_dir(path: &str) -> Result<(), FileError> {
370    fs::remove_dir(path).map_err(|e| FileError::from_std(e, path))
371}
372
373pub fn remove_dir_all(path: &str) -> Result<(), FileError> {
374    fs::remove_dir_all(path).map_err(|e| FileError::from_std(e, path))
375}
376
377pub fn into_abs_path(path: &str) -> Result<String, FileError> {
378    let std_path = Path::new(path);
379
380    if std_path.is_absolute() {
381        Ok(path.to_string())
382    }
383
384    else {
385        Ok(join(
386            &current_dir()?,
387            path,
388        )?)
389    }
390}
391
392pub fn current_dir() -> Result<String, FileError> {
393    let cwd = std::env::current_dir().map_err(|e| FileError::from_std(e, "."))?;
394
395    match cwd.to_str() {
396        Some(cwd) => Ok(cwd.to_string()),
397        None => Err(FileError::os_str_err(cwd.into_os_string())),
398    }
399}
400
401pub fn set_current_dir(path: &str) -> Result<(), FileError> {
402    std::env::set_current_dir(path).map_err(|e| FileError::from_std(e, path))
403}
404
405#[cfg(feature = "diff")]
406pub fn diff(path: &str, base: &str) -> Result<String, FileError> {
407    match pathdiff::diff_paths(path, base) {
408        Some(path) => match path.to_str() {
409            Some(path) => Ok(path.to_string()),
410            None => Err(FileError::os_str_err(path.into_os_string())),
411        },
412        None => Err(FileError::cannot_diff_path(path.to_string(), base.to_string())),
413    }
414}
415
416#[cfg(feature = "diff")]
417/// It calcs diff and normalizes.
418pub fn get_relative_path(base: &str, path: &str) -> Result<String, FileError> {
419    // It has to normalize the output because `diff` behaves differently on windows and unix.
420    Ok(normalize(&diff(
421        // in order to calc diff, it needs a full path
422        &normalize(
423            &into_abs_path(path)?,
424        )?,
425        &normalize(
426            &into_abs_path(base)?,
427        )?,
428    )?)?)
429}
430
431pub fn normalize(path: &str) -> Result<String, FileError> {
432    let mut result = vec![];
433    let path = path.replace("\\", "/");
434
435    for component in path.split("/") {
436        match component {
437            c if c == "." => {},
438
439            // this branch is messy and that's a design decision
440            // It's obvious that `normalize("./foo")` is `"foo"` and
441            // `normalize("./foo/../bar/")` is `"bar"`. But what about
442            // `normalize("../foo")`? Is that an error or just `"../foo"`?
443            // I chose `"../foo"` and that's just a design decision.
444            c if c == ".." => if result.is_empty() {
445                result.push(c.to_string());
446            } else {
447                let p = result.pop().unwrap().to_string();
448
449                if p == ".." {
450                    result.push(p);
451                }
452            },
453
454            c => { result.push(c.to_string()); },
455        }
456    }
457
458    Ok(result.join("/"))
459}
460
461#[derive(Clone,  PartialEq)]
462pub struct FileError {
463    pub kind: FileErrorKind,
464    pub given_path: Option<String>,
465}
466
467impl FileError {
468    pub fn from_std(e: io::Error, given_path: &str) -> Self {
469        let kind = match e.kind() {
470            io::ErrorKind::NotFound => FileErrorKind::FileNotFound,
471            io::ErrorKind::PermissionDenied => FileErrorKind::PermissionDenied,
472            io::ErrorKind::AlreadyExists => FileErrorKind::AlreadyExists,
473            e => FileErrorKind::Unknown(format!("unknown error: {e:?}")),
474        };
475
476        FileError {
477            kind,
478            given_path: Some(given_path.to_string()),
479        }
480    }
481
482    pub(crate) fn os_str_err(os_str: OsString) -> Self {
483        FileError {
484            kind: FileErrorKind::OsStrErr(os_str),
485            given_path: None,
486        }
487    }
488
489    pub(crate) fn cannot_diff_path(path: String, base: String) -> Self {
490        FileError {
491            kind: FileErrorKind::CannotDiffPath(path.to_string(), base),
492            given_path: Some(path),
493        }
494    }
495
496    pub fn unknown(msg: String, path: Option<String>) -> Self {
497        FileError {
498            kind: FileErrorKind::Unknown(msg),
499            given_path: path,
500        }
501    }
502
503    pub fn render_error(&self) -> String {
504        let path = self.given_path.as_ref().map(|p| p.to_string()).unwrap_or(String::new());
505
506        match &self.kind {
507            FileErrorKind::FileNotFound => format!(
508                "file not found: `{path}`"
509            ),
510            FileErrorKind::PermissionDenied => format!(
511                "permission denied: `{path}`"
512            ),
513            FileErrorKind::AlreadyExists => format!(
514                "file already exists: `{path}`"
515            ),
516            FileErrorKind::CannotDiffPath(path, base) => format!(
517                "cannot calc diff: `{path}` and `{base}`"
518            ),
519            FileErrorKind::Unknown(msg) => format!(
520                "unknown file error: `{msg}`"
521            ),
522            FileErrorKind::OsStrErr(os_str) => format!(
523                "error converting os_str: `{os_str:?}`"
524            ),
525        }
526    }
527}
528
529impl fmt::Debug for FileError {
530    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
531        write!(fmt, "{}", self.render_error())
532    }
533}
534
535impl fmt::Display for FileError {
536    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
537        write!(fmt, "{}", self.render_error())
538    }
539}
540
541#[derive(Clone, Debug, PartialEq)]
542pub enum FileErrorKind {
543    FileNotFound,
544    PermissionDenied,
545    AlreadyExists,
546    CannotDiffPath(String, String),
547    Unknown(String),
548    OsStrErr(OsString),
549}