1mod error;
2mod utils;
3
4use std::cmp::Ordering;
5use std::fmt;
6use std::fs;
7use std::io;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10
11use chrono::prelude::{DateTime, Local, TimeZone};
12use itertools::Itertools; use nom::bytes::complete::{tag, take_until};
14use nom::combinator::map_res;
15use nom::error::VerboseError;
16use nom::IResult;
17use path_clean::PathClean;
18use platform_dirs::{AppDirs, AppUI};
19use snafu::{ensure, OptionExt, ResultExt};
20
21pub use error::*;
22use utils::{move_file, AbsolutePath};
23
24pub struct Trash {
25 home_trash: PathBuf,
26}
27
28impl Trash {
29 pub fn new() -> io::Result<Trash> {
31 let home_trash = AppDirs::new::<PathBuf>(None, AppUI::CommandLine)
32 .unwrap()
33 .data_dir
34 .join("Trash");
35 fs::create_dir_all(home_trash.join("files"))?;
36 fs::create_dir_all(home_trash.join("info"))?;
37 Ok(Trash { home_trash })
38 }
39
40 pub fn get_trashed_files(&self) -> Result<Vec<Result<TrashEntry>>> {
41 let results = self
42 .home_trash
43 .join("info")
44 .read_dir()
45 .context(ReadTrashInfoDir)?
46 .map(|dir_entry| {
47 let trash_info_path = dir_entry.context(ReadTrashInfoDir)?.path();
48 let trashed_path =
49 self.home_trash
50 .join("files")
51 .join(trash_info_path.file_stem().context({
52 InvalidTrashInfoPath {
53 path: &trash_info_path,
54 }
55 })?);
56 let trash_info = fs::read_to_string(&trash_info_path)
57 .context(ReadTrashInfo {
58 path: &trash_info_path,
59 })?
60 .parse::<TrashInfo>()
61 .context(ParseTrashInfo {
62 path: &trash_info_path,
63 })?;
64 Ok(TrashEntry {
65 trashed_path,
66 trash_info,
67 })
68 })
69 .sorted_by(|result1, result2| match result1 {
71 Ok(x) => match result2 {
72 Ok(y) => Ord::cmp(&x.trash_info.deletion_date, &y.trash_info.deletion_date),
73 Err(_) => Ordering::Less,
74 },
75 Err(_) => Ordering::Less,
76 })
77 .collect();
78
79 Ok(results)
80 }
81
82 pub fn trash_file<P>(&self, path: P) -> Result<PathBuf>
83 where
84 P: AsRef<Path>,
85 {
86 let path = path.as_ref();
87 let path = path.absolute_path().map_err(|_| Error::InvalidPath {
88 path: path.to_path_buf(),
89 })?;
90 ensure!(path.exists(), InvalidPath { path });
91
92 ensure!(
94 !self.home_trash.starts_with(&path),
95 TrashingTrashCan { path }
96 );
97
98 let trashed_path = move_file(
99 &path,
100 &self
101 .home_trash
102 .join("files")
103 .join(path.file_name().expect("path is clean")),
104 )
105 .context(MoveFile { path: &path })?;
106
107 let trash_info_path = get_trash_info_path(&self.home_trash, &trashed_path);
108 let trash_info = TrashInfo {
109 original_path: path.to_path_buf(),
110 deletion_date: Local::now(),
111 };
112 fs::write(trash_info_path, format!("{}\n", trash_info)).context(WriteTrashInfo { path })?;
113
114 Ok(trashed_path)
115 }
116
117 pub fn restore_trashed_file<P>(&self, path: P) -> Result<PathBuf>
118 where
119 P: AsRef<Path>,
120 {
121 let path = path.as_ref();
122 let path = path.absolute_path().map_err(|_| Error::InvalidPath {
123 path: path.to_path_buf(),
124 })?;
125 ensure!(path.exists(), InvalidPath { path });
126
127 let trash_info_path = get_trash_info_path(&self.home_trash, &path);
128 let original_path = fs::read_to_string(&trash_info_path)
129 .context(ReadTrashInfo {
130 path: &trash_info_path,
131 })?
132 .parse::<TrashInfo>()
133 .context(ParseTrashInfo { path: &path })?
134 .original_path;
135 let restored_path = move_file(&path, &original_path).context(MoveFile { path: &path })?;
136 fs::remove_file(trash_info_path).context(RemoveFile { path })?;
137
138 Ok(restored_path)
139 }
140
141 pub fn erase_file<P>(&self, path: P) -> Result<()>
142 where
143 P: AsRef<Path>,
144 {
145 let path = path.as_ref();
146 let path = path.absolute_path().map_err(|_| Error::InvalidPath {
147 path: path.to_path_buf(),
148 })?;
149 ensure!(path.exists(), InvalidPath { path });
150
151 if self.is_file_trashed(&path).expect("file exists") {
152 fs::remove_file(get_trash_info_path(&self.home_trash, &path))
153 .context(RemoveFile { path: &path })?;
154 }
155 if path.is_dir() {
156 fs::remove_dir_all(&path).context(RemoveFile { path: &path })?;
157 } else {
158 fs::remove_file(&path).context(RemoveFile { path: &path })?;
159 }
160
161 Ok(())
162 }
163
164 pub fn is_file_trashed<P>(&self, path: P) -> Result<bool>
165 where
166 P: AsRef<Path>,
167 {
168 let path = path.as_ref();
169 let path = path.absolute_path().map_err(|_| Error::InvalidPath {
170 path: path.to_path_buf(),
171 })?;
172 ensure!(path.exists(), InvalidPath { path });
173
174 Ok(path.starts_with(&self.home_trash.join("files")))
175 }
176}
177
178fn get_trash_info_path(trash_path: &Path, file: &Path) -> PathBuf {
179 trash_path.join("info").join(format!(
180 "{}.trashinfo",
181 file.file_name().expect("path is clean").to_string_lossy()
182 ))
183}
184
185#[derive(Debug, PartialEq, Eq)]
186pub struct TrashEntry {
187 pub trashed_path: PathBuf,
188 pub trash_info: TrashInfo,
189}
190
191#[derive(Debug, PartialEq, Eq)]
192pub struct TrashInfo {
193 pub original_path: PathBuf,
194 pub deletion_date: DateTime<Local>,
195}
196
197fn parse_trash_info<'a>(input: &'a str) -> IResult<&'a str, TrashInfo, VerboseError<&'a str>> {
198 let (input, _) = tag("[Trash Info]\n")(input)?;
199
200 let (input, _) = tag("Path=")(input)?;
201 let (input, original_path) = map_res(take_until("\n"), |input| {
202 let path = PathBuf::from(input);
203 if path.is_relative() || path.parent().is_none() || path != path.clean() {
204 Err(io::Error::new(io::ErrorKind::InvalidData, "invalid path"))
205 } else {
206 Ok(path)
207 }
208 })(input)?;
209 let (input, _) = tag("\n")(input)?;
210
211 let (input, _) = tag("DeletionDate=")(input)?;
212 let (input, deletion_date) = map_res(take_until("\n"), |input| {
213 Local.datetime_from_str(input, "%Y-%m-%dT%H:%M:%S")
214 })(input)?;
215
216 Ok((
217 input,
218 TrashInfo {
219 original_path,
220 deletion_date,
221 },
222 ))
223}
224
225impl FromStr for TrashInfo {
227 type Err = io::Error;
228
229 fn from_str(s: &str) -> io::Result<Self> {
230 parse_trash_info(s).map(|x| x.1).map_err(|_| {
231 io::Error::new(io::ErrorKind::InvalidData, "failed to parse TrashInfo").into()
234 })
235 }
236}
237
238impl fmt::Display for TrashInfo {
239 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
240 write!(
241 f,
242 "[Trash Info]\nPath={}\nDeletionDate={}",
243 self.original_path.display(),
244 self.deletion_date.format("%Y-%m-%dT%H:%M:%S"),
245 )
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_get_trashed_files() {
255 use std::fs::File;
256
257 let trash = Trash {
258 home_trash: PathBuf::from("test2"),
259 };
260 fs::create_dir_all(&trash.home_trash.join("files"));
261 fs::create_dir_all(&trash.home_trash.join("info"));
262 let trash_info = TrashInfo {
263 original_path: PathBuf::from("/asdf/123"),
264 deletion_date: Local.ymd(2014, 7, 8).and_hms(9, 10, 11),
265 };
266
267 fs::remove_dir_all(trash.home_trash).unwrap();
268 }
269
270 #[test]
271 fn test_trash_file() {}
272
273 #[test]
274 fn test_restore_trashed_file() {}
275
276 #[test]
277 fn test_erase_file() {
278 use std::fs::File;
279
280 let trash = Trash {
281 home_trash: PathBuf::from("test"),
282 };
283 let files_dir = trash.home_trash.join("files");
284 let info_dir = trash.home_trash.join("info");
285 let in_trash = files_dir.join("in_trash");
286 let in_trash_trash_info = info_dir.join("in_trash.trashinfo");
287
288 fs::create_dir_all(&files_dir);
289 fs::create_dir_all(&info_dir);
290
291 assert!(&files_dir.exists());
292 assert!(&info_dir.exists());
293
294 File::create(&in_trash);
295 File::create(&in_trash_trash_info);
296
297 assert!((&in_trash).exists());
298 assert!((&in_trash_trash_info).exists());
299
300 trash.erase_file(&in_trash);
301
302 assert!(!(&in_trash).exists());
303 assert!(!(&in_trash_trash_info).exists());
304
305 let out_trash = trash.home_trash.join("asdf");
306 File::create(&out_trash);
307 assert!(&out_trash.exists());
308 trash.erase_file(&out_trash);
309 assert!(!&out_trash.exists());
310
311 fs::remove_dir_all(trash.home_trash).unwrap();
312 }
313
314 #[test]
315 fn test_is_file_trashed() {
316 let trash = Trash {
317 home_trash: PathBuf::from("/test/trash"),
318 };
319 let file1 = PathBuf::from("/test/trash/files/foo");
320 let file2 = PathBuf::from("/test/trash/info/foo");
321 assert!(trash.is_file_trashed(file1).unwrap());
322 assert!(!trash.is_file_trashed(file2).unwrap());
323 }
324
325 #[test]
326 fn test_trash_info_parsing() {
327 let trash_info = TrashInfo {
328 original_path: PathBuf::from("/asdf/123"),
329 deletion_date: Local.ymd(2014, 7, 8).and_hms(9, 10, 11),
330 };
331 let trash_info_to_str = "[Trash Info]\nPath=/asdf/123\nDeletionDate=2014-07-08T09:10:11";
332 assert_eq!(trash_info, trash_info_to_str.parse::<TrashInfo>().unwrap());
333 }
334
335 #[test]
336 fn test_trash_info_display() {
337 let trash_info = TrashInfo {
338 original_path: PathBuf::from("/asdf/123"),
339 deletion_date: Local.ymd(2014, 7, 8).and_hms(9, 10, 11),
340 };
341 let trash_info_to_str = "[Trash Info]\nPath=/asdf/123\nDeletionDate=2014-07-08T09:10:11";
342 assert_eq!(trash_info.to_string(), trash_info_to_str);
343 }
344}