Skip to main content

pistonite_cu/fs/
file.rs

1use std::path::{Path, PathBuf};
2
3use super::Time;
4
5use crate::pre::*;
6
7/// `std::env::current_exe()` with error reporting
8///
9/// ```rust
10/// # use pistonite_cu as cu;
11/// # fn main() -> cu::Result<()> {
12/// let path = cu::fs::current_exe()?;
13/// assert!(path.is_absolute());
14/// # Ok(()) }
15/// ```
16pub fn current_exe() -> crate::Result<PathBuf> {
17    std::env::current_exe().context("failed to get current exe path")
18}
19
20/// Copy a file from one to another.
21///
22/// Directories will be created for the target location if not already exists.
23///
24/// if from and to is the pointing to the same, it might be truncated.
25///
26/// The number of bytes in `to` is returned.
27#[inline(always)]
28pub fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<u64> {
29    copy_impl(from.as_ref(), to.as_ref())
30}
31fn copy_impl(from: &Path, to: &Path) -> crate::Result<u64> {
32    crate::trace!("copy from='{}' to='{}'", from.display(), to.display());
33    if to.is_dir() {
34        crate::bail!(
35            "cannot copy from '{}' to '{}': target is directory",
36            from.display(),
37            to.display()
38        );
39    }
40    let copy_error = match std::fs::copy(from, to) {
41        Ok(v) => return Ok(v),
42        Err(e) => e,
43    };
44    if !from.exists() {
45        crate::rethrow!(
46            copy_error,
47            "cannot copy from '{}' to '{}': source does not exist",
48            from.display(),
49            to.display()
50        );
51    }
52    if from.is_dir() {
53        crate::rethrow!(
54            copy_error,
55            "cannot copy from '{}' to '{}': source is directory",
56            from.display(),
57            to.display()
58        );
59    }
60    if copy_error.kind() == std::io::ErrorKind::NotFound {
61        // ensure parent dir exists and try again
62        if let Ok(parent) = to.parent_abs()
63            && !parent.exists()
64        {
65            crate::trace!(
66                "retrying with parent creation: copy from='{}' to='{}'",
67                from.display(),
68                to.display()
69            );
70            crate::check!(
71                super::make_dir(&parent),
72                "could not automatically create parent directory for '{}'",
73                to.display()
74            )?;
75            // try again
76            return crate::check!(
77                std::fs::copy(from, to),
78                "failed to copy file from '{}' to '{}'",
79                from.display(),
80                to.display()
81            );
82        }
83    }
84    crate::rethrow!(
85        copy_error,
86        "failed to copy file from '{}' to '{}'",
87        from.display(),
88        to.display()
89    );
90}
91
92/// Get the modified time for a file.
93///
94/// If the file doesn't exist, None is returned
95#[inline(always)]
96pub fn get_mtime(path: impl AsRef<Path>) -> crate::Result<Option<Time>> {
97    get_mtime_impl(path.as_ref())
98}
99fn get_mtime_impl(path: &Path) -> crate::Result<Option<Time>> {
100    match path.metadata() {
101        Ok(meta) => Ok(Some(Time::from_last_modification_time(&meta))),
102        Err(e) => {
103            if !path.exists() {
104                return Ok(None);
105            }
106            crate::rethrow!(
107                e,
108                "failed to get modification time for '{}'",
109                path.display()
110            );
111        }
112    }
113}
114
115/// Set the modified time for a file
116#[inline(always)]
117pub fn set_mtime(path: impl AsRef<Path>, time: Time) -> crate::Result<()> {
118    set_mtime_impl(path.as_ref(), time)
119}
120fn set_mtime_impl(path: &Path, time: Time) -> crate::Result<()> {
121    crate::check!(
122        filetime::set_file_mtime(path, time),
123        "failed to set modification time for '{}'",
124        path.display()
125    )
126}
127
128/// Remove `path` as either a file or empty directory.
129///
130/// No-op if the path does not exist.
131/// Error if the path is a non-empty directory.
132#[inline(always)]
133pub fn remove(path: impl AsRef<Path>) -> crate::Result<()> {
134    remove_impl(path.as_ref())
135}
136fn remove_impl(path: &Path) -> crate::Result<()> {
137    if !path.exists() {
138        crate::trace!("remove: is absent: '{}'", path.display());
139        return Ok(());
140    }
141    crate::trace!("remove '{}'", path.display());
142    if path.is_dir() {
143        return crate::check!(
144            std::fs::remove_dir(path),
145            "failed to remove directory '{}'",
146            path.display()
147        );
148    }
149    crate::check!(
150        std::fs::remove_file(path),
151        "failed to remove file '{}'",
152        path.display()
153    )
154}
155
156/// Async version of [`remove`]
157#[cfg(feature = "coroutine")]
158#[inline(always)]
159pub async fn co_remove(path: impl AsRef<Path>) -> crate::Result<()> {
160    co_remove_impl(path.as_ref()).await
161}
162#[cfg(feature = "coroutine")]
163async fn co_remove_impl(path: &Path) -> crate::Result<()> {
164    if !path.exists() {
165        crate::trace!("co_remove: is absent: '{}'", path.display());
166        return Ok(());
167    }
168    crate::trace!("co_remove '{}'", path.display());
169    if path.is_dir() {
170        return crate::check!(
171            tokio::fs::remove_dir(path).await,
172            "failed to remove directory '{}'",
173            path.display()
174        );
175    }
176    crate::check!(
177        tokio::fs::remove_file(path).await,
178        "failed to remove file '{}'",
179        path.display()
180    )
181}
182
183/// Move a file or directory.
184///
185/// Unlike `std::fs::rename`, this will fallback to less efficient methods
186/// of moving if the paths are on separate file systems. Currently, renaming
187/// directories on separate filesystems is not supported to prevent unintended
188/// behavior.
189///
190/// # If `from` is file
191/// - will error if `to` is a directory
192/// - `to` will be replaced if exists
193///
194/// # If `from` is a directory
195/// - will error if `to` exists and is not a directory or is a non-empty directory (use `cu::fs::rec_remove(to)` to delete
196///   it first), or is on a different filesystem than `from`.
197#[inline(always)]
198pub fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
199    rename_impl(from.as_ref(), to.as_ref())
200}
201#[crate::context("failed to rename '{}' to '{}'", from.display(), to.display())]
202fn rename_impl(from: &Path, to: &Path) -> crate::Result<()> {
203    crate::trace!("rename: '{}' to '{}'", from.display(), to.display());
204    let Ok(from_meta) = from.metadata() else {
205        crate::bail!("source does not exist");
206    };
207    if from_meta.is_file() {
208        if to.is_dir() {
209            crate::bail!("target is a directory");
210        }
211        let Err(e) = std::fs::rename(from, to) else {
212            return Ok(());
213        };
214        crate::trace!("rename failed: {e}, trying fallback");
215        // fallback is copy and delete old
216        crate::fs::copy(from, to)?;
217        crate::fs::remove(from)?;
218        return Ok(());
219    }
220    match to.metadata().ok() {
221        None => {
222            // does not exist, OK
223        }
224        Some(to_meta) => {
225            if !to_meta.is_dir() {
226                // exists and is not directory
227                crate::bail!("target is not a directory");
228            }
229            if !crate::fs::is_empty_dir(to)? {
230                crate::bail!("target is a non-empty directory");
231            }
232            // remove old
233            crate::fs::remove(to)?;
234        }
235    }
236    // no fallback if std rename fails
237    std::fs::rename(from, to)?;
238    Ok(())
239}