ragit_fs/
lib.rs

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