~iptq/garbage

ref: 45fc4f1ee1bf4703e50eaed3e5f61ee06a7f6f81 garbage/src/ops/put.rs -rw-r--r-- 8.6 KiB
45fc4f1e — Michael Zhang add sourcehut build badge and remove deps.rs one 1 year, 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
use std::env;
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use anyhow::Result;
use chrono::Local;

use crate::utils;
use crate::{TrashDir, TrashInfo};
use crate::{HOME_MOUNT, MOUNTS};

#[derive(Debug, Error)]
pub enum Error {
    #[error("Refusing to remove directory {0} without '-r' option")]
    MissingRecursiveOption(PathBuf),

    #[error("Refusing to remove '.' or '..', skipping...")]
    CannotTrashDotDirs,

    #[error("Cancelled by user.")]
    CancelledByUser,
}

/// Options to pass to put
#[derive(StructOpt)]
pub struct PutOptions {
    /// The target path to be trashed
    #[structopt(parse(from_os_str))]
    paths: Vec<PathBuf>,

    /// Don't actually move anything, just print the files to be removed
    #[structopt(long = "dry")]
    dry: bool,

    /// Prompt before every removal
    #[structopt(long = "prompt", short = "i")]
    prompt: bool,

    /// Trashes directories recursively (ignored)
    #[structopt(long = "recursive", short = "r")]
    _recursive: bool,

    /// Suppress prompts/messages
    #[structopt(long = "force", short = "f")]
    force: bool,

    /// Put all the trashed files into this trash directory
    /// regardless of what filesystem is on.
    ///
    /// If a copy is required to copy the file, a prompt will be raised,
    /// which can be bypassed by passing --force.
    ///
    /// If this option is not passed, the best strategy will be chosen
    /// automatically for each file.
    #[structopt(long = "trash-dir", parse(from_os_str))]
    trash_dir: Option<PathBuf>,
}

/// Throw some files into the trash.
pub fn put(options: PutOptions) -> Result<()> {
    for path in options.paths.iter() {
        // don't allow deleting '.' or '..'
        let current_dir = env::current_dir()?;
        ensure!(
            !(utils::into_absolute(&path)? == current_dir.as_path()
                || (current_dir.parent().is_some()
                    && utils::into_absolute(&path)? == current_dir.parent().unwrap())),
            Error::CannotTrashDotDirs
        );

        // pick the best strategy for deleting this particular file
        let strategy = if let Some(ref trash_dir) = options.trash_dir {
            DeletionStrategy::Fixed(TrashDir::from(trash_dir))
        } else {
            DeletionStrategy::pick_strategy(path)?
        };
        // println!("Strategy: {:?}", strategy);

        if options.dry {
            eprintln!("Dry-deleting: {}", path.to_str().unwrap());
        } else if let Err(err) = strategy.delete(path, &options) {
            eprintln!("{}", err);
        }
    }

    Ok(())
}

/// DeletionStrategy describes a strategy by which a file is deleted
#[derive(Debug)]
enum DeletionStrategy {
    /// move or copy the file to this particular trash
    Fixed(TrashDir),

    /// move the candidate files/directories to the trash directory
    /// (this requires that both the candidate and the trash directories be on the same filesystem)
    MoveTo(TrashDir),

    /// recursively copy the candidate files/directories to the trash directory
    CopyTo(TrashDir),
}

impl DeletionStrategy {
    /// This method picks the ideal strategy
    pub fn pick_strategy(target: impl AsRef<Path>) -> Result<DeletionStrategy> {
        let target = target.as_ref();
        let target_mount = MOUNTS
            .get_mount_point(target)
            .ok_or_else(|| anyhow!("couldn't get mount point"))?;

        // first, are we on the home mount?
        if target_mount == *HOME_MOUNT {
            return Ok(DeletionStrategy::MoveTo(TrashDir::get_home_trash()));
        }

        // try to use the $topdir/.Trash directory
        if should_use_topdir_trash(&target_mount) {
            let topdir_trash_dir = target_mount
                .join(".Trash")
                .join(utils::get_uid().to_string());
            let trash_dir = TrashDir::from(topdir_trash_dir);
            trash_dir.create()?;
            return Ok(DeletionStrategy::MoveTo(trash_dir));
        }

        // try to use the $topdir/.Trash-$uid directory
        if should_use_topdir_trash_uid(&target_mount) {
            let topdir_trash_uid = target_mount.join(format!(".Trash-{}", utils::get_uid()));
            let trash_dir = TrashDir::from(topdir_trash_uid);
            trash_dir.create()?;
            return Ok(DeletionStrategy::MoveTo(trash_dir));
        }

        // it's not on the home mount, but we'll copy into it anyway
        Ok(DeletionStrategy::CopyTo(TrashDir::get_home_trash()))
    }

    fn get_target_trash(&self) -> (&TrashDir, bool) {
        match self {
            DeletionStrategy::Fixed(trash) => {
                // TODO: finish
                (trash, true)
            }
            DeletionStrategy::MoveTo(trash) => (trash, false),
            DeletionStrategy::CopyTo(trash) => (trash, true),
        }
    }

    /// The actual deletion happens here
    pub fn delete(&self, target: impl AsRef<Path>, options: &PutOptions) -> Result<()> {
        let target = target.as_ref();

        // this will be None if target isn't a symlink
        let link_info = target.read_link().ok();

        // file is a directory
        // if !link_info.is_some() && target.is_dir() && !options.recursive {
        //     bail!(Error::MissingRecursiveOption(target.to_path_buf()));
        // }

        let (trash_dir, requires_copy) = self.get_target_trash();

        // prompt if not suppressed
        // TODO: streamline this logic better
        if !options.force && (requires_copy || options.prompt) {
            // TODO: actually handle prompting instead of manually flushing
            if requires_copy {
                eprint!(
                    "Removing file '{}' requires potentially expensive copying. Continue? [Y/n] ",
                    target.to_str().unwrap()
                );
            } else if options.prompt {
                eprint!("Remove file '{}'? [Y/n] ", target.to_str().unwrap());
            }
            io::stderr().flush()?;

            let should_continue = loop {
                let stdin = io::stdin();
                let mut s = String::new();
                stdin.read_line(&mut s).unwrap();
                match s.trim().to_lowercase().as_str() {
                    "yes" | "y" => break true,
                    "no" | "n" => break false,
                    _ => {
                        eprint!("Invalid response. Please type yes or no: ");
                    }
                }
            };
            if !should_continue {
                bail!(Error::CancelledByUser);
            }
        }

        // preparing metadata
        let now = Local::now();
        let elapsed = now.timestamp_millis();
        let file_name = format!(
            "{}.{}",
            elapsed,
            target.file_name().unwrap().to_str().unwrap()
        );

        let trash_file_path = trash_dir.files_dir()?.join(&file_name);
        let trash_info_path = trash_dir.info_dir()?.join(file_name + ".trashinfo");

        let trash_info = TrashInfo {
            path: utils::into_absolute(target)?,
            deletion_date: now,
            deleted_path: trash_file_path.clone(),
            info_path: trash_info_path.clone(),
        };
        {
            let trash_info_file = File::create(trash_info_path)?;
            trash_info.write(&trash_info_file)?;
        }

        // copy the file over
        if requires_copy {
            utils::recursive_copy(&target, &trash_file_path)?;
            fs::remove_dir_all(&target)?;
        } else {
            fs::rename(&target, &trash_file_path)?;
        }

        Ok(())
    }
}

/// Can we use $topdir/.Trash?
///
/// 1. If it doesn't exist, don't create it.
/// 2. All users should be able to write to it
/// 3. It must have sticky-bit permissions if the filesystem supports it.
/// 4. The directory must not be a symbolic link.
fn should_use_topdir_trash(mount: impl AsRef<Path>) -> bool {
    let mount = mount.as_ref();
    let trash_dir = mount.join(".Trash");

    if !trash_dir.exists() {
        return false;
    }

    let dir = match File::open(trash_dir) {
        Ok(file) => file,
        Err(_) => return false,
    };
    let meta = match dir.metadata() {
        Ok(meta) => meta,
        Err(_) => return false,
    };
    if meta.file_type().is_symlink() {
        return false;
    }
    let perms = meta.permissions();

    perms.mode() & 0o1000 > 0
}

/// Can we use $topdir/.Trash-uid?
fn should_use_topdir_trash_uid(path: impl AsRef<Path>) -> bool {
    let path = path.as_ref();
    if !path.exists() {
        match fs::create_dir(path) {
            Ok(_) => (),
            Err(_) => return false,
        };
    }
    return true;
}