trash_utils/
lib.rs

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; // TODO: why?
13use 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	// TODO: errors
30	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			// TODO: what is this?
70			.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		// check if given file contains the trash-can
93		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
225// TODO
226impl 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			// TODO figure out how to convert nom::error to failure::error while preserving its error message.
232			// Was having issues since failure::error requires a static lifetime and nom::error contains a &str.
233			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}