rrc_lib/
files.rs

1use std::{
2    fs::{self, File},
3    io::{self, BufWriter, Seek, Write},
4    path::{Path, PathBuf},
5};
6use trash::{os_limited, TrashItem};
7
8use crate::{FileErr, RecursiveOperation};
9
10///Returns a losslessly converted string if possible, but if that errors return the lossy conversion.
11//This function is used pretty much everywhere. While it may cause issues in some edge case,
12// I'd rather avoid matching or unwrapping Options everywhere
13pub fn path_to_string<P: AsRef<Path>>(path: P) -> String {
14    match path.as_ref().to_str() {
15        Some(s) => s.to_string(),
16        None => path.as_ref().to_string_lossy().to_string(),
17    }
18}
19
20pub fn run_op_on_dir_recursive<T>(
21    operation: &mut T,
22    dir: &Path,
23    mut count: usize,
24) -> Result<usize, FileErr>
25where
26    T: RecursiveOperation,
27{
28    if dir.is_dir() {
29        for entry in fs::read_dir(dir).map_err(|e| FileErr::map(e, dir))? {
30            let entry = entry.map_err(|e| FileErr::map(e, dir))?;
31            let path = entry.path();
32            if path.is_dir() {
33                run_op_on_dir_recursive(operation, &path, count)?;
34            } else {
35                count += 1;
36                operation.display_cb(&path, false);
37                operation.cb(&path)?;
38            }
39        }
40        count += 1;
41        operation.display_cb(&PathBuf::from(dir), true);
42        operation.cb(&PathBuf::from(dir))?;
43    }
44    Ok(count)
45}
46
47pub fn select_from_trash(name: &String) -> Option<Vec<TrashItem>> {
48    let mut items: Vec<TrashItem> = Vec::new();
49
50    for item in os_limited::list().unwrap() {
51        if name == &item.name {
52            items.push(item);
53        }
54    }
55
56    if items.is_empty() {
57        return None;
58    }
59    Some(items)
60}
61
62pub fn get_existent_trash_items(
63    names: &Vec<String>,
64    s_cb: impl Fn(Vec<TrashItem>) -> TrashItem,
65    d_cb: impl Fn(String),
66) -> Vec<TrashItem> {
67    names
68        .iter()
69        .filter_map(|n| match select_from_trash(n) {
70            Some(i) => Some(s_cb(i)),
71            None => None,
72        })
73        .collect()
74}
75
76pub fn overwrite_file(file: &File, runs: usize) -> std::io::Result<()> {
77    const OW_BUFF_SIZE: usize = 10usize.pow(6);
78    let file_len = file.metadata()?.len();
79
80    if file.metadata()?.is_dir() {
81        return Ok(());
82    }
83
84    let mut writer = BufWriter::new(file);
85
86    let buf = vec![0u8; OW_BUFF_SIZE];
87
88    for _ in 0..runs {
89        writer.seek(io::SeekFrom::Start(0))?;
90
91        //Keep track of our position in the file ourselves based on the delusion that it might impact
92        //performace to seek on each loop iteration
93        loop {
94            let offset = writer.seek(io::SeekFrom::Current(0)).unwrap();
95            if (file_len - offset) >= OW_BUFF_SIZE.try_into().unwrap() {
96                writer.write_all(&buf)?;
97            } else {
98                writer.write_all(&vec![0u8; (file_len - offset).try_into().unwrap()])?;
99                break;
100            }
101        }
102    }
103
104    Ok(())
105}
106
107pub fn remove_file_or_dir(path: &PathBuf) -> std::io::Result<()> {
108    if !path.exists() {
109        return Err(std::io::ErrorKind::NotFound.into());
110    }
111
112    if path.is_dir() {
113        return fs::remove_dir(path);
114    }
115    fs::remove_file(path)
116}
117
118pub fn get_existent_paths<'a, T, U>(input_paths: &'a T, d_cb: impl Fn(U)) -> Vec<U>
119where
120    &'a T: IntoIterator<Item = U>,
121    U: AsRef<Path> + 'a,
122{
123    input_paths
124        .into_iter()
125        .filter_map(|p| {
126            if p.as_ref().exists() {
127                Some(p)
128            } else {
129                d_cb(p);
130                return None;
131            }
132        })
133        .collect()
134}
135
136//Unfortunately I'm yet to find a more functional way to do this
137pub fn path_vec_from_string_vec<'a>(strings: Vec<&'a String>) -> Vec<&'a Path> {
138    let mut ret_vec = Vec::<&Path>::new();
139    for s in strings {
140        ret_vec.push(Path::new(s));
141    }
142    return ret_vec;
143}
144
145#[cfg(test)]
146mod tests {
147    use rand::distributions::{Alphanumeric, DistString};
148    use std::{fs::OpenOptions, io::Read};
149
150    use super::*;
151    #[test]
152    fn test_select_from_trash_exists_single() {
153        let filename = generate_random_filename();
154
155        File::create(&filename).unwrap();
156        trash::delete(&filename).unwrap();
157
158        let selected = select_from_trash(&filename);
159
160        assert!(selected.is_some());
161        let selected_val = selected.unwrap();
162
163        assert!(selected_val.len() == 1);
164
165        os_limited::purge_all([&(selected_val[0])]).unwrap();
166    }
167
168    #[test]
169    fn test_select_from_trash_exists_multiple() {
170        let filename = generate_random_filename();
171
172        File::create(&filename).unwrap();
173        trash::delete(&filename).unwrap();
174
175        File::create(&filename).unwrap();
176        trash::delete(&filename).unwrap();
177
178        let selected = select_from_trash(&filename);
179
180        assert!(selected.is_some());
181        let selected_val = selected.unwrap();
182        assert!(selected_val.len() == 2);
183
184        os_limited::purge_all(selected_val).unwrap();
185    }
186
187    #[test]
188    fn test_select_from_trash_fails() {
189        assert!(select_from_trash(&generate_random_filename()).is_none());
190    }
191
192    fn is_file_of_single_byte(mut file: &File, byte: u8) -> bool {
193        let file_len: usize = file.metadata().unwrap().len().try_into().unwrap();
194        let mut buf = Vec::<u8>::with_capacity(file_len);
195        file.seek(io::SeekFrom::Start(0)).unwrap();
196        file.read_to_end(&mut buf).unwrap();
197
198        if buf != vec![byte; file_len] {
199            println!("{}/{}", buf.len(), file_len);
200            return false;
201        }
202        true
203    }
204
205    #[test]
206    fn test_is_file_of_single_byte() {
207        //can't just use rand file name, must use seperate paths to avoid weird issues with creating a file immediately after deleting it
208        let filename = generate_random_filename();
209
210        let mut file = OpenOptions::new()
211            .write(true)
212            .create(true)
213            .read(true)
214            .open(&filename)
215            .unwrap();
216
217        //write 1MiB of data to the test file
218        let ones = vec![1u8; 1024 ^ 2];
219        file.write_all(&ones).unwrap();
220        file.flush().unwrap();
221
222        if !is_file_of_single_byte(&file, 1u8) {
223            fs::remove_file(&filename).unwrap();
224
225            panic!()
226        } else {
227            fs::remove_file(&filename).unwrap();
228        }
229    }
230
231    #[test]
232    fn test_overwrite_file() {
233        let filename = generate_random_filename();
234
235        let mut file = OpenOptions::new()
236            .write(true)
237            .create(true)
238            .read(true)
239            .open(&filename)
240            .unwrap();
241
242        //write 1MB of data to the test file (MB not MiB)
243        let ones = vec![1u8; 10usize.pow(6)];
244        file.write_all(&ones).unwrap();
245        file.flush().unwrap();
246
247        overwrite_file(&file, 1).unwrap();
248
249        if !is_file_of_single_byte(&file, 0u8) {
250            fs::remove_file(&filename).unwrap();
251            panic!();
252        } else {
253            fs::remove_file(&filename).unwrap();
254        }
255    }
256
257    fn generate_random_filename() -> String {
258        Alphanumeric.sample_string(&mut rand::thread_rng(), 8)
259            + "."
260            + &Alphanumeric.sample_string(&mut rand::thread_rng(), 3)
261    }
262}