Skip to main content

gen_utils/common/
fs.rs

1//! # File System Utils
2//! recommend to use this module to handle the file system operation instead of `std::fs` to control the error and unify
3//! ## Interfaces
4//! - exists
5//! - try_exists
6//! - read
7//! - write
8//! - append
9//! - create
10//! - create_new
11//! - delete
12//! - parse_to`<T> T: FromStr`
13use std::{
14    fs::{create_dir_all, File},
15    io::Write,
16    path::{Path, PathBuf},
17    str::FromStr,
18    thread,
19    time::Duration,
20};
21
22use crate::error::{Error, FsError, ParseError};
23
24use super::Source;
25
26pub fn exists_dir<P>(path: P) -> bool
27where
28    P: AsRef<Path>,
29{
30    let path = path.as_ref();
31    path.exists() && path.is_dir()
32}
33
34/// ## Check the file is exists ?
35/// return `true` if the file is exists or `false` if not exists
36/// if path is empty return false
37/// ### Also
38/// If you want to get the Error reason, use `try_exists` which will return `Result<bool, Error>`
39pub fn exists<P>(path: P) -> bool
40where
41    P: AsRef<Path>,
42{
43    path.as_ref().exists()
44}
45/// ## Check the file is exists ?
46/// - if the file is exists, return `Ok(true)`
47/// - if the file is not exists, return `Ok(false)` (empty)
48/// - if the file can not be sure exists or not, return `Err` (kind like you have no permission to access the file or the parent directory)
49pub fn try_exists<P>(path: P) -> Result<bool, Error>
50where
51    P: AsRef<Path>,
52{
53    path.as_ref()
54        .try_exists()
55        .map_err(|e| Error::Fs(FsError::UnExpected(e.to_string())))
56}
57/// ## Read the file
58/// - if the file is exists, return the content of the file as `String`
59/// - if the file is not exists, return `Err` (kind like the file can not be found or no permission)
60pub fn read<P>(path: P) -> Result<String, Error>
61where
62    P: AsRef<Path>,
63{
64    std::fs::read_to_string(path.as_ref()).map_err(|e| {
65        Error::Fs(FsError::Read {
66            path: path.as_ref().to_path_buf(),
67            reason: e.to_string(),
68        })
69    })
70}
71/// ## Write the file
72/// - if the file is exists, write the content to the file(which will overwrite the origin content)
73/// - if the file is not exists, create the file and write the content to the file
74/// - if the write process is success, return `Ok(())`
75/// - if the write process is fail, return `Err` (kind like the file can not be write or no permission)
76/// ### Also
77/// If you want to append the content to the file, use `append` method
78pub fn write<P>(path: P, content: &str) -> Result<(), Error>
79where
80    P: AsRef<Path>,
81{
82    // use create_file to create the file if not exists
83    if !path.as_ref().exists() {
84        create_file(path.as_ref())?;
85    }
86
87    std::fs::write(path.as_ref(), content).map_err(|e| {
88        Error::Fs(FsError::Write {
89            path: path.as_ref().to_path_buf(),
90            reason: e.to_string(),
91        })
92    })
93}
94/// ## Append the content to the file
95/// - if the file is exists, append the content to the file
96/// - if the file is not exists, create the file and write the content to the file
97pub fn append<P>(path: P, content: &str) -> Result<(), Error>
98where
99    P: AsRef<Path>,
100{
101    std::fs::OpenOptions::new()
102        .append(true)
103        .create(true)
104        .open(path.as_ref())
105        .and_then(|mut file| file.write_all(content.as_bytes()))
106        .map_err(|e| {
107            Error::Fs(FsError::Write {
108                path: path.as_ref().to_path_buf(),
109                reason: e.to_string(),
110            })
111        })
112}
113
114/// ## Create the directory
115/// use std::fs::create_dir to create the directory
116pub fn create_dir<P>(path: P) -> Result<(), Error>
117where
118    P: AsRef<Path>,
119{
120    let path = path.as_ref().to_path_buf();
121    std::fs::create_dir_all(path.as_path()).map_err(|e| {
122        FsError::Create {
123            path,
124            reason: e.to_string(),
125        }
126        .into()
127    })
128}
129
130pub fn exists_or_create_dir<P>(path: P) -> Result<(), Error>
131where
132    P: AsRef<Path>,
133{
134    if exists_dir(path.as_ref()) {
135        Ok(())
136    } else {
137        create_dir(path)
138    }
139}
140
141/// ## Create the file
142/// - if is exists, return `Err` (kind like the file is exists, back FsError::UnExpected)
143/// - if is not exists, create the file and return `Ok(())`
144/// - if the permission is not enough, return `Err` (kind like you have no permission to create the file)
145/// ### Also
146/// if you want to create a new file and do not care about the exists one, use `create_new` method
147pub fn create<P>(path: P) -> Result<(), Error>
148where
149    P: AsRef<Path>,
150{
151    std::fs::File::create(path.as_ref())
152        .map(|_| ())
153        .map_err(|e| Error::Fs(FsError::UnExpected(e.to_string())))
154}
155/// ## Remove the file
156/// - if the file is exists, remove the file and return `Ok(true)`
157/// - if the file is not exists, return `Ok(false)`
158/// - if the permission is not enough, return `Err` (kind like you have no permission to remove the file, back FsError::UnExpected)
159pub fn delete<P>(path: P) -> Result<bool, Error>
160where
161    P: AsRef<Path>,
162{
163    std::fs::remove_file(path.as_ref()).map_or_else(
164        |e| {
165            if e.kind() == std::io::ErrorKind::NotFound {
166                Ok(false)
167            } else {
168                Err(Error::Fs(FsError::UnExpected(e.to_string())))
169            }
170        },
171        |()| Ok(true),
172    )
173}
174
175/// ## Remove the directory
176pub fn delete_dir<P>(path: P) -> Result<(), Error>
177where
178    P: AsRef<Path>,
179{
180    if exists_dir(path.as_ref()) {
181        std::fs::remove_dir_all(path.as_ref())
182            .map(|_| ())
183            .map_err(|e| Error::Fs(FsError::UnExpected(e.to_string())))
184    } else {
185        Ok(())
186    }
187}
188
189/// ## Move the directory|file from `from` to `to`
190/// use walkdir to move the directory
191pub fn move_to<P, Q>(from: P, to: Q) -> Result<(), Error>
192where
193    P: AsRef<Path>,
194    Q: AsRef<Path>,
195{
196    copy(from.as_ref(), to.as_ref())?;
197    delete_dir(from)
198}
199
200/// ## Copy the directory|file from `from` to `to`
201pub fn copy<P, Q>(from: P, to: Q) -> Result<(), Error>
202where
203    P: AsRef<Path>,
204    Q: AsRef<Path>,
205{
206    for entry in walkdir::WalkDir::new(from.as_ref())
207        .into_iter()
208        .filter_map(|e| e.ok())
209    {
210        let path = entry.path();
211        let relative = path.strip_prefix(from.as_ref()).unwrap();
212        let target = to.as_ref().join(relative);
213        if path.is_dir() {
214            std::fs::create_dir_all(target)
215                .map_err(|e| Error::Fs(FsError::UnExpected(e.to_string())))?;
216        } else {
217            std::fs::copy(path, target)
218                .map_err(|e| Error::Fs(FsError::UnExpected(e.to_string())))?;
219        }
220    }
221    Ok(())
222}
223
224/// ## Create the new file
225/// - if is exists, remove the exists one and create a new file, return `Ok(())`
226/// - if is not exists, create the file and return `Ok(())`
227/// - if the permission is not enough, return `Err` (kind like you have no permission to create the file)
228/// ### Also
229/// if you want to create a file , but if exists one, return `Err`, use `create` method
230pub fn create_new<P>(path: P) -> Result<(), Error>
231where
232    P: AsRef<Path>,
233{
234    match delete(path.as_ref()) {
235        Ok(_) => create(path),
236        Err(e) => Err(e),
237    }
238}
239/// ## Parse the file to `T`
240/// - if the file is exists,read and then parse the content to `T`
241/// - if the file is not exists, return `Err` (kind like the file can not be found or no permission)
242pub fn parse_to<T, P>(path: P) -> Result<T, Error>
243where
244    T: FromStr,
245    P: AsRef<Path>,
246{
247    read(path).and_then(|content| {
248        content
249            .parse::<T>()
250            .map_err(|_| ParseError::template(std::any::type_name::<T>()).into())
251    })
252}
253
254/// ## Create the file
255/// use create_dir_all to create the parent directory if not exists then create the file
256/// ### Error
257/// Error
258/// This function will return an error in the following situations, but is not
259/// limited to just these cases:
260///
261/// * If any directory in the path specified by `path`
262/// does not already exist and it could not be created otherwise. The specific
263/// error conditions for when a directory is being created (after it is
264/// determined to not exist) are outlined by [`fs::create_dir`].
265///
266/// Notable exception is made for situations where any of the directories
267/// specified in the `path` could not be created as it was being created concurrently.
268/// Such cases are considered to be successful. That is, calling `create_dir_all`
269/// concurrently from multiple threads or processes is guaranteed not to fail
270/// due to a race condition with itself.
271pub fn create_file<P>(path: P) -> Result<File, Error>
272where
273    P: AsRef<Path>,
274{
275    if let Some(parent_dir) = path.as_ref().parent() {
276        if !try_exists(parent_dir)? {
277            match create_dir_all(parent_dir) {
278                Ok(_) => {}
279                Err(e) => {
280                    return Err(Error::Fs(FsError::Create {
281                        path: parent_dir.to_path_buf(),
282                        reason: e.to_string(),
283                    }))
284                }
285            };
286        }
287    } else {
288        return Err(Error::Fs(FsError::UnExpected(
289            "Path has no parent directory".to_string(),
290        )));
291    }
292
293    File::create(path.as_ref()).map_err(|e| {
294        Error::Fs(FsError::Create {
295            path: path.as_ref().to_path_buf(),
296            reason: e.to_string(),
297        })
298    })
299}
300
301/// ## Convert the PathBuf to the string (Prevents platform differences)
302/// all the path separator will be replaced by `/`
303/// if is windows, the prefix `//?/` will be removed
304pub fn path_to_str<P>(path: P) -> String
305where
306    P: AsRef<Path>,
307{
308    path.as_ref()
309        .to_str()
310        .unwrap()
311        .replace("\\", "/")
312        .replace("//?/", "")
313}
314
315pub trait GenUIFs {
316    /// ## check the file is gen file
317    fn is_gen_file(&self) -> bool;
318    /// ## convert file to compiled file
319    /// rules:
320    /// - is `.gen` file: source -> target/src
321    /// - not `.gen` file: source -> target
322    fn to_compiled<P>(
323        &self,
324        prefix: P,
325        source: P,
326        target: P,
327        is_delete: bool,
328    ) -> Result<PathBuf, Error>
329    where
330        P: AsRef<Path>;
331    /// ## convert file to compiled file from source
332    fn to_compiled_from_source(&self, source: &Source) -> Result<PathBuf, Error>;
333    /// ## convert file to compiled file from delete
334    fn to_compiled_from_delete(&self, source: &Source) -> Result<PathBuf, Error>;
335    /// ## back rs to gen file
336    fn back_gen(&self) -> PathBuf;
337
338    fn widget_source(&self, source: &Source) -> Result<Source, Error>;
339}
340
341impl<P> GenUIFs for P
342where
343    P: AsRef<Path>,
344{
345    fn is_gen_file(&self) -> bool {
346        // 后缀名需要处理一下,文件也可能没有后缀名
347        self.as_ref().is_file() && self.as_ref().extension().map_or(false, |ext| ext == "gen")
348    }
349
350    fn to_compiled<PT>(
351        &self,
352        prefix: PT,
353        source: PT,
354        target: PT,
355        is_delete: bool,
356    ) -> Result<PathBuf, Error>
357    where
358        PT: AsRef<Path>,
359    {
360        let mut path = self.as_ref().to_path_buf();
361        let mut compiled = prefix.as_ref().join(target.as_ref());
362
363        let flag = if is_delete {
364            self.as_ref().extension().map_or(false, |ext| ext == "gen")
365        } else {
366            self.is_gen_file()
367        };
368
369        if flag {
370            // add src and change extension to rs
371            compiled = compiled.join("src");
372            path = path.with_extension("rs");
373        }
374
375        Ok(compiled.join(
376            path.strip_prefix(prefix.as_ref().join(source.as_ref()))
377                .map_err(|e| Error::Fs(FsError::UnExpected(e.to_string())))?,
378        ))
379    }
380
381    fn to_compiled_from_source(&self, source: &Source) -> Result<PathBuf, Error> {
382        self.to_compiled(&source.path, &source.from, &source.to, false)
383    }
384
385    fn to_compiled_from_delete(&self, source: &Source) -> Result<PathBuf, Error> {
386        self.to_compiled(&source.path, &source.from, &source.to, true)
387    }
388
389    fn back_gen(&self) -> PathBuf {
390        if self.as_ref().is_file() && self.as_ref().extension().map_or(false, |ext| ext == "rs") {
391            return self.as_ref().with_extension("gen");
392        }
393        self.as_ref().to_path_buf()
394    }
395
396    fn widget_source(&self, source: &Source) -> Result<Source, Error> {
397        let compiled_path = self.as_ref().to_compiled_from_source(source)?;
398        Ok(Source::new(
399            source.path.as_path(),
400            self.as_ref(),
401            compiled_path.as_path(),
402        ))
403    }
404}
405
406/// ## File state enum
407/// which should be used to represent the state of a file
408///
409/// from notify::EventKind, this enum may change in the future if needed
410#[derive(Debug, Clone, Copy, PartialEq)]
411pub enum FileState {
412    Unchanged,
413    Modified,
414    Created,
415    Deleted,
416    Renamed,
417}
418
419impl FileState {
420    /// match state if is modified or created then do then function
421    ///
422    /// else do nothing
423    pub fn modify_then<T, F>(&self, default: T, f: F) -> Result<T, Error>
424    where
425        F: FnOnce() -> Result<T, Error>,
426    {
427        match self {
428            FileState::Modified | FileState::Created | FileState::Renamed | FileState::Deleted => {
429                f()
430            }
431            _ => Ok(default),
432        }
433    }
434    pub fn then<F>(&self, f: F) -> Result<(), Error>
435    where
436        F: FnOnce(&Self) -> Result<(), Error>,
437    {
438        f(&self)
439    }
440    pub fn is_modify(&self) -> bool {
441        !matches!(self, FileState::Unchanged)
442    }
443}
444
445/// copy file from source_path to compiled_path
446pub fn copy_file<P, Q>(from: P, to: Q) -> Result<(), Error>
447where
448    P: AsRef<Path>,
449    Q: AsRef<Path>,
450{
451    // Extract the directory part from the compiled_path
452    if let Some(parent_dir) = to.as_ref().parent() {
453        // Check if the directory exists, if not, create it
454        if !parent_dir.exists() {
455            // Create the directory and any necessary parent directories
456            create_dir_all(parent_dir).map_err(|e| {
457                Error::Fs(FsError::Create {
458                    path: parent_dir.to_path_buf(),
459                    reason: e.to_string(),
460                })
461            })?;
462        }
463    }
464
465    // Copy the file from source_path to compiled_path
466    // fs::copy(from, to).expect("Failed to copy file to compiled project");
467    copy_with_retries(from, to, 5, Duration::from_millis(200))
468}
469
470/// copy file from source_path to compiled_path with retries
471fn copy_with_retries<P, Q>(
472    from: P,
473    to: Q,
474    max_attempts: usize,
475    delay: Duration,
476) -> Result<(), Error>
477where
478    P: AsRef<Path>,
479    Q: AsRef<Path>,
480{
481    let mut attempts = 0;
482    loop {
483        match std::fs::copy(from.as_ref(), to.as_ref()) {
484            Ok(_) => return Ok(()),
485            Err(_) if attempts < max_attempts => {
486                attempts += 1;
487                thread::sleep(delay);
488            }
489            Err(e) => {
490                return Err(FsError::UnExpected(format!(
491                    "Failed to copy file to compiled project: {}",
492                    e.to_string()
493                ))
494                .into())
495            }
496        }
497    }
498}
499
500pub fn relative_with_prefix<P1, P2>(prefix: P1, path: P2) -> PathBuf
501where
502    P1: AsRef<Path>,
503    P2: AsRef<Path>,
504{
505    let path = path.as_ref();
506    if path.is_relative() {
507        prefix.as_ref().join(path)
508    } else {
509        path.to_path_buf()
510    }
511}
512
513#[cfg(test)]
514mod test_fs {
515    use std::path::PathBuf;
516
517    use super::*;
518
519    #[test]
520    fn test_create_file() {
521        let _res = create_file(
522            "E:/Rust/try/makepad/Gen-UI/examples/gen_makepad_simple/src_gen/src/views/root.rs",
523        );
524    }
525
526    #[test]
527    fn test_exists() {
528        let res = exists(PathBuf::new());
529        assert!(!res);
530    }
531    #[test]
532    fn test_try_exists() {
533        let res = try_exists(PathBuf::new());
534        assert!(res.is_err());
535    }
536}