1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::fs::{create_dir, read_dir, remove_dir_all};
4use std::io::prelude::*;
5use std::path::{Path, PathBuf};
6
7use anyhow::{anyhow, Context, Result};
8use chrono::{Local, NaiveDateTime};
9
10use crate::common::{
11 read_lines, tilde, TRASH_CONFIRM_LINE, TRASH_FOLDER_FILES, TRASH_FOLDER_INFO,
12 TRASH_INFO_EXTENSION,
13};
14use crate::config::Bindings;
15use crate::io::{CowStr, DrawMenu};
16use crate::{impl_content, impl_selectable, log_info, log_line};
17
18const TRASHINFO_DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
19
20#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Info {
29 origin: PathBuf,
30 dest_name: String,
31 deletion_date: String,
32}
33
34impl Info {
35 pub fn new(origin: &Path, dest_name: &str) -> Self {
38 let date = Local::now();
39 let deletion_date = format!("{}", date.format(TRASHINFO_DATETIME_FORMAT));
40 let dest_name = dest_name.to_owned();
41 Self {
42 origin: PathBuf::from(origin),
43 dest_name,
44 deletion_date,
45 }
46 }
47
48 fn format(&self) -> String {
49 format!(
50 "[Trash Info]
51Path={origin}
52DeletionDate={date}
53",
54 origin = url_escape::encode_fragment(&self.origin.to_string_lossy()),
55 date = self.deletion_date
56 )
57 }
58
59 pub fn write_trash_info(&self, dest: &Path) -> Result<()> {
72 log_info!("writing trash_info {} for {:?}", self, dest);
73
74 let mut file = std::fs::File::create(dest)?;
75 if let Err(e) = write!(file, "{}", self.format()) {
76 log_info!("Couldn't write to trash file: {}", e);
77 }
78 Ok(())
79 }
80
81 pub fn from_trash_info_file(trash_info_file: &Path) -> Result<Self> {
96 let (option_path, option_deleted_time) = Self::parse_trash_info_file(trash_info_file)?;
97
98 match (option_path, option_deleted_time) {
99 (Some(origin), Some(deletion_date)) => {
100 let dest_name = Self::get_dest_name(trash_info_file)?;
101 Ok(Self {
102 origin,
103 dest_name,
104 deletion_date,
105 })
106 }
107 _ => Err(anyhow!("Couldn't parse the trash info file")),
108 }
109 }
110
111 fn get_dest_name(trash_info_file: &Path) -> Result<String> {
112 if let Some(dest_name) = trash_info_file.file_name() {
113 let dest_name =
114 Self::remove_extension(dest_name.to_string_lossy().as_ref().to_owned())?;
115 Ok(dest_name)
116 } else {
117 Err(anyhow!("Couldn't parse the trash info filename"))
118 }
119 }
120
121 fn parse_trash_info_file(trash_info_file: &Path) -> Result<(Option<PathBuf>, Option<String>)> {
122 let mut option_path: Option<PathBuf> = None;
123 let mut option_deleted_time: Option<String> = None;
124
125 if let Ok(mut lines) = read_lines(trash_info_file) {
126 let Some(Ok(first_line)) = lines.next() else {
127 return Err(anyhow!("Unreadable TrashInfo file"));
128 };
129 if !first_line.starts_with("[Trash Info]") {
130 return Err(anyhow!("First line should start with [TrashInfo]"));
131 }
132
133 for line in lines {
134 let Ok(line) = line else {
135 continue;
136 };
137 if option_path.is_none() && line.starts_with("Path=") {
138 option_path = Some(Self::parse_option_path(&line));
139 continue;
140 }
141 if option_deleted_time.is_none() && line.starts_with("DeletionDate=") {
142 option_deleted_time = Some(Self::parse_deletion_date(&line)?);
143 }
144 }
145 }
146
147 Ok((option_path, option_deleted_time))
148 }
149
150 fn parse_option_path(line: &str) -> PathBuf {
151 let path_part = &line[5..];
152 let cow_path_str = url_escape::decode(path_part);
153 let path_str = cow_path_str.as_ref();
154 PathBuf::from(path_str)
155 }
156
157 fn parse_deletion_date(line: &str) -> Result<String> {
158 let deletion_date_str = &line[13..];
159 match parsed_date_from_path_info(deletion_date_str) {
160 Ok(()) => Ok(deletion_date_str.to_owned()),
161 Err(e) => Err(e),
162 }
163 }
164
165 fn remove_extension(mut destname: String) -> Result<String> {
166 if destname.ends_with(TRASH_INFO_EXTENSION) {
167 destname.truncate(destname.len() - 10);
168 Ok(destname)
169 } else {
170 Err(anyhow!(
171 "trahsinfo: filename doesn't contain {TRASH_INFO_EXTENSION}"
172 ))
173 }
174 }
175}
176
177impl Ord for Info {
178 fn cmp(&self, other: &Self) -> Ordering {
180 self.deletion_date.cmp(&other.deletion_date).reverse()
181 }
182}
183
184impl PartialOrd for Info {
185 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
186 Some(self.cmp(other))
187 }
188}
189
190impl std::fmt::Display for Info {
191 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
192 write!(
193 f,
194 "{} - trashed on {}",
195 &self.origin.display(),
196 self.deletion_date
197 )
198 }
199}
200
201#[derive(Clone)]
207pub struct Trash {
208 content: Vec<Info>,
210 index: usize,
211 pub trash_folder_files: String,
213 trash_folder_info: String,
214 pub help: String,
215}
216
217impl Trash {
218 pub fn new(binds: &Bindings) -> Result<Self> {
226 let trash_folder_files = tilde(TRASH_FOLDER_FILES).to_string();
227 let trash_folder_info = tilde(TRASH_FOLDER_INFO).to_string();
228 create_if_not_exists(&trash_folder_files)?;
229 create_if_not_exists(&trash_folder_info)?;
230 let empty_trash_binds = match binds.keybind_reversed().get("TrashEmpty") {
231 Some(s) => s.to_owned(),
232 None => "alt-x".to_owned(),
233 };
234
235 let help = format!("{TRASH_CONFIRM_LINE}{empty_trash_binds}: Empty the trash");
236
237 let index = 0;
238 let content = vec![];
239
240 Ok(Self {
241 content,
242 index,
243 trash_folder_files,
244 trash_folder_info,
245 help,
246 })
247 }
248
249 fn pick_dest_name(&self, origin: &Path) -> Result<String> {
250 if let Some(file_name) = origin.file_name() {
251 let mut dest = file_name
252 .to_str()
253 .context("pick_dest_name: Couldn't parse the origin filename into a string")?
254 .to_owned();
255 let mut dest_path = PathBuf::from(&self.trash_folder_files);
256 dest_path.push(&dest);
257 while dest_path.exists() {
258 dest.push_str(&rand_string());
259 dest_path = PathBuf::from(&self.trash_folder_files);
260 dest_path.push(&dest);
261 }
262 return Ok(dest);
263 }
264 Err(anyhow!("pick_dest_name: Couldn't extract the filename",))
265 }
266
267 fn parse_updated_content(trash_folder_info: &str) -> Result<Vec<Info>> {
268 match read_dir(trash_folder_info) {
269 Ok(read_dir) => {
270 let mut content: Vec<Info> = read_dir
271 .filter_map(std::result::Result::ok)
272 .filter(|direntry| direntry.path().extension().is_some())
273 .filter(|direntry| {
274 direntry.path().extension().unwrap().to_str().unwrap() == "trashinfo"
275 })
276 .map(|direntry| Info::from_trash_info_file(&direntry.path()))
277 .filter_map(std::result::Result::ok)
278 .collect();
279
280 content.sort_unstable();
281 Ok(content)
282 }
283 Err(error) => {
284 log_info!("Couldn't read path {:?} - {}", trash_folder_info, error);
285 Err(anyhow!(error))
286 }
287 }
288 }
289
290 pub fn update(&mut self) -> Result<()> {
298 self.index = 0;
299 self.content = Self::parse_updated_content(&self.trash_folder_info)?;
300 Ok(())
301 }
302
303 pub fn trash(&mut self, origin: &Path) -> Result<()> {
314 if origin.is_relative() {
315 return Err(anyhow!("trash: origin path should be absolute"));
316 }
317
318 let dest_file_name = self.pick_dest_name(origin)?;
319
320 self.trash_a_file(Info::new(origin, &dest_file_name), &dest_file_name)
321 }
322
323 fn concat_path(root: &str, filename: &str) -> PathBuf {
324 let mut concatened_path = PathBuf::from(root);
325 concatened_path.push(filename);
326 concatened_path
327 }
328
329 fn trashfile_path(&self, dest_file_name: &str) -> PathBuf {
330 Self::concat_path(&self.trash_folder_files, dest_file_name)
331 }
332
333 fn trashinfo_path(&self, dest_trashinfo_name: &str) -> PathBuf {
334 let mut dest_trashinfo_name = dest_trashinfo_name.to_owned();
335 dest_trashinfo_name.push_str(TRASH_INFO_EXTENSION);
336 Self::concat_path(&self.trash_folder_info, &dest_trashinfo_name)
337 }
338
339 fn trash_a_file(&mut self, trash_info: Info, dest_file_name: &str) -> Result<()> {
340 let trashfile_filename = &self.trashfile_path(dest_file_name);
341 if let Err(error) = std::fs::rename(&trash_info.origin, trashfile_filename) {
342 log_info!("Couldn't trash {trash_info}. Error: {error:?}");
343 } else {
344 Self::log_trash_add(&trash_info.origin, dest_file_name);
345 trash_info.write_trash_info(&self.trashinfo_path(dest_file_name))?;
346 self.content.push(trash_info);
347 }
348 Ok(())
349 }
350
351 fn log_trash_add(origin: &Path, dest_file_name: &str) {
352 log_info!("moved to trash {:?} -> {:?}", origin, dest_file_name);
353 log_line!("moved to trash {:?} -> {:?}", origin, dest_file_name);
354 }
355
356 pub fn empty_trash(&mut self) -> Result<()> {
369 self.empty_trash_dirs()?;
370 let number_of_elements = self.content.len();
371 self.content = vec![];
372 Self::log_trash_empty(number_of_elements);
373 Ok(())
374 }
375
376 fn empty_trash_dirs(&self) -> Result<(), std::io::Error> {
377 Self::empty_dir(&self.trash_folder_files)?;
378 Self::empty_dir(&self.trash_folder_info)
379 }
380
381 fn empty_dir(dir: &str) -> Result<(), std::io::Error> {
382 remove_dir_all(dir)?;
383 create_dir(dir)
384 }
385
386 fn log_trash_empty(number_of_elements: usize) {
387 log_line!("Emptied the trash: {number_of_elements} files permanently deleted");
388 log_info!("Emptied the trash: {number_of_elements} files permanently deleted");
389 }
390
391 fn remove_selected_file(&mut self) -> Result<(PathBuf, PathBuf, PathBuf)> {
392 if self.is_empty() {
393 return Err(anyhow!(
394 "remove selected file: Can't restore from an empty trash"
395 ));
396 }
397 let trashinfo = &self.content[self.index];
398 let origin = trashinfo.origin.clone();
399
400 let parent = find_parent(&trashinfo.origin)?;
401
402 let trashed_file_content = self.trashfile_path(&trashinfo.dest_name);
403 let trashed_file_info = self.trashinfo_path(&trashinfo.dest_name);
404
405 if !trashed_file_content.exists() {
406 return Err(anyhow!("trash restore: Couldn't find the trashed file"));
407 }
408
409 if !trashed_file_info.exists() {
410 return Err(anyhow!("trash restore: Couldn't find the trashed info"));
411 }
412
413 self.remove_from_content_and_delete_trashinfo(&trashed_file_info)?;
414
415 Ok((origin, trashed_file_content, parent))
416 }
417
418 fn remove_from_content_and_delete_trashinfo(&mut self, trashed_file_info: &Path) -> Result<()> {
419 self.content.remove(self.index);
420 std::fs::remove_file(trashed_file_info)?;
421 Ok(())
422 }
423
424 pub fn restore(&mut self) -> Result<()> {
437 if self.is_empty() {
438 return Ok(());
439 }
440 let (origin, trashed_file_content, parent) = self.remove_selected_file()?;
441 Self::execute_restore(&origin, &trashed_file_content, &parent)?;
442 Self::log_trash_restore(&origin);
443 Ok(())
444 }
445
446 fn execute_restore(origin: &Path, trashed_file_content: &Path, parent: &Path) -> Result<()> {
447 if !parent.exists() {
448 std::fs::create_dir_all(parent)?;
449 }
450 std::fs::rename(trashed_file_content, origin)?;
451 Ok(())
452 }
453
454 fn log_trash_restore(origin: &Path) {
455 log_line!("Trash restored: {origin}", origin = origin.display());
456 }
457
458 pub fn delete_permanently(&mut self) -> Result<()> {
466 if self.is_empty() {
467 return Ok(());
468 }
469
470 let (_, trashed_file_content, _) = self.remove_selected_file()?;
471
472 std::fs::remove_file(&trashed_file_content)?;
473 Self::log_trash_remove(&trashed_file_content);
474
475 if self.index > 0 {
476 self.index -= 1;
477 }
478 Ok(())
479 }
480
481 fn log_trash_remove(trashed_file_content: &Path) {
482 log_line!(
483 "Trash removed: {trashed_file_content}",
484 trashed_file_content = trashed_file_content.display()
485 );
486 }
487}
488
489impl_content!(Trash, Info);
490
491fn parsed_date_from_path_info(ds: &str) -> Result<()> {
492 NaiveDateTime::parse_from_str(ds, TRASHINFO_DATETIME_FORMAT)?;
493 Ok(())
494}
495
496fn rand_string() -> String {
497 crate::common::random_alpha_chars().take(2).collect()
498}
499
500fn find_parent(path: &Path) -> Result<PathBuf> {
501 Ok(path
502 .parent()
503 .ok_or_else(|| anyhow!("find_parent_as_string : Couldn't find parent of {path:?}"))?
504 .to_owned())
505}
506
507fn create_if_not_exists<P>(path: P) -> std::io::Result<()>
508where
509 std::path::PathBuf: From<P>,
510 P: std::convert::AsRef<std::path::Path> + std::marker::Copy,
511{
512 if !std::path::PathBuf::from(path).exists() {
513 std::fs::create_dir_all(path)?;
514 }
515 Ok(())
516}
517
518impl CowStr for Info {
519 fn cow_str(&self) -> Cow<'_, str> {
520 self.to_string().into()
521 }
522}
523
524impl DrawMenu<Info> for Trash {}