• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MilesCranmer / rip2 / #472

22 Oct 2024 10:37PM UTC coverage: 71.364% (-2.9%) from 74.306%
#472

Pull #60

MilesCranmer
refactor: cache some env vars
Pull Request #60: Speedup: cache env var

6 of 14 new or added lines in 2 files covered. (42.86%)

19 existing lines in 2 files now uncovered.

314 of 440 relevant lines covered (71.36%)

1503238552.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.16
/src/lib.rs
1
use clap::CommandFactory;
2
use fs_extra::dir::get_size;
3
use std::fs::Metadata;
4
use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
5
use std::path::{Path, PathBuf};
6
use std::{env, fs};
7
use walkdir::WalkDir;
8

9
// Platform-specific imports
10
#[cfg(unix)]
11
use std::os::unix::fs::{symlink, FileTypeExt, PermissionsExt};
12

13
#[cfg(target_os = "windows")]
14
use std::os::windows::fs::symlink_file as symlink;
15

16
pub mod args;
17
pub mod completions;
18
pub mod record;
19
pub mod util;
20

21
use args::Args;
22
use record::{Record, RecordItem, DEFAULT_FILE_LOCK};
23

24
const LINES_TO_INSPECT: usize = 6;
25
const FILES_TO_INSPECT: usize = 6;
26
pub const BIG_FILE_THRESHOLD: u64 = 500000000; // 500 MB
27

28
pub fn run(cli: Args, mode: impl util::TestingMode, stream: &mut impl Write) -> Result<(), Error> {
2,147,483,647✔
29
    args::validate_args(&cli)?;
2,147,483,647✔
30
    let graveyard: &PathBuf = &get_graveyard(cli.graveyard);
2,147,483,647✔
31

32
    if !graveyard.exists() {
2,147,483,647✔
33
        fs::create_dir_all(graveyard)?;
2,147,483,647✔
34

35
        #[cfg(unix)]
36
        {
UNCOV
37
            let metadata = graveyard.metadata()?;
×
UNCOV
38
            let mut permissions = metadata.permissions();
×
UNCOV
39
            permissions.set_mode(0o700);
×
40
        }
41
        // TODO: Default permissions on windows should be good, but need to double-check.
42
    }
43

44
    // Stores the deleted files
45
    let record = Record::<DEFAULT_FILE_LOCK>::new(graveyard);
2,147,483,647✔
46
    let cwd = &env::current_dir()?;
2,147,483,647✔
47

48
    // If the user wishes to restore everything
49
    if cli.decompose {
×
50
        if util::prompt_yes("Really unlink the entire graveyard?", &mode, stream)? {
2,147,483,647✔
51
            fs::remove_dir_all(graveyard)?;
2,147,483,647✔
52
        }
53
    } else if let Some(mut graves_to_exhume) = cli.unbury {
2,147,483,647✔
54
        // Vector to hold the grave path of items we want to unbury.
55
        // This will be used to determine which items to remove from the
56
        // record following the unbury.
57
        // Initialize it with the targets passed to -r
58

59
        // If -s is also passed, push all files found by seance onto
60
        // the graves_to_exhume.
61
        if cli.seance && record.open().is_ok() {
2,147,483,647✔
62
            let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd)?);
2,147,483,647✔
63
            for grave in record.seance(&gravepath)? {
2,147,483,647✔
64
                graves_to_exhume.push(grave.dest);
2,147,483,647✔
65
            }
66
        }
67

68
        // Otherwise, add the last deleted file
69
        if graves_to_exhume.is_empty() {
2,147,483,647✔
70
            if let Ok(s) = record.get_last_bury() {
2,147,483,647✔
71
                graves_to_exhume.push(s);
2,147,483,647✔
72
            }
73
        }
74

75
        let allow_rename = util::allow_rename();
2,147,483,647✔
76

77
        // Go through the graveyard and exhume all the graves
78
        for line in record.lines_of_graves(&graves_to_exhume) {
2,147,483,647✔
79
            let entry = RecordItem::new(&line);
2,147,483,647✔
80
            let orig: PathBuf = match util::symlink_exists(&entry.orig) {
2,147,483,647✔
81
                true => util::rename_grave(&entry.orig),
2,147,483,647✔
82
                false => PathBuf::from(&entry.orig),
2,147,483,647✔
83
            };
84
            move_target(&entry.dest, &orig, allow_rename, &mode, stream).map_err(|e| {
2,147,483,647✔
85
                Error::new(
×
86
                    e.kind(),
×
87
                    format!(
×
88
                        "Unbury failed: couldn't copy files from {} to {}",
×
89
                        entry.dest.display(),
×
90
                        orig.display()
×
91
                    ),
92
                )
93
            })?;
94
            writeln!(
2,147,483,647✔
95
                stream,
2,147,483,647✔
96
                "Returned {} to {}",
97
                entry.dest.display(),
2,147,483,647✔
98
                orig.display()
2,147,483,647✔
99
            )?;
100
        }
101
        record.log_exhumed_graves(&graves_to_exhume)?;
2,147,483,647✔
102
    } else if cli.seance {
2,147,483,647✔
103
        let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd)?);
2,147,483,647✔
104
        writeln!(stream, "{: <19}\tpath", "deletion_time")?;
×
105
        for grave in record.seance(&gravepath)? {
2,147,483,647✔
106
            let parsed_time = chrono::DateTime::parse_from_rfc3339(&grave.time)
2,147,483,647✔
107
                .expect("Failed to parse time from RFC3339 format")
108
                .format("%Y-%m-%dT%H:%M:%S")
109
                .to_string();
110
            // Get the path separator:
111
            writeln!(stream, "{}\t{}", parsed_time, grave.dest.display())?;
2,147,483,647✔
112
        }
113
    } else if cli.targets.is_empty() {
2,147,483,647✔
114
        Args::command().print_help()?;
2,147,483,647✔
115
    } else {
116
        let allow_rename = util::allow_rename();
2,147,483,647✔
117
        for target in cli.targets {
2,147,483,647✔
118
            bury_target(
NEW
119
                &target,
×
NEW
120
                graveyard,
×
NEW
121
                &record,
×
NEW
122
                cwd,
×
NEW
123
                cli.inspect,
×
NEW
124
                allow_rename,
×
NEW
125
                &mode,
×
NEW
126
                stream,
×
127
            )?;
128
        }
129
    }
130

131
    Ok(())
2,147,483,647✔
132
}
133

134
#[allow(clippy::too_many_arguments)]
135
fn bury_target<const FILE_LOCK: bool>(
2,147,483,647✔
136
    target: &PathBuf,
137
    graveyard: &PathBuf,
138
    record: &Record<FILE_LOCK>,
139
    cwd: &Path,
140
    inspect: bool,
141
    allow_rename: bool,
142
    mode: &impl util::TestingMode,
143
    stream: &mut impl Write,
144
) -> Result<(), Error> {
145
    // Check if source exists
146
    let metadata = &fs::symlink_metadata(target).map_err(|_| {
2,147,483,647✔
147
        Error::new(
2,147,483,647✔
148
            ErrorKind::NotFound,
2,147,483,647✔
149
            format!(
2,147,483,647✔
150
                "Cannot remove {}: no such file or directory",
2,147,483,647✔
151
                target.to_str().unwrap()
2,147,483,647✔
152
            ),
153
        )
154
    })?;
155
    // Canonicalize the path unless it's a symlink
156
    let source = &if !metadata.file_type().is_symlink() {
2,147,483,647✔
157
        dunce::canonicalize(cwd.join(target))
2,147,483,647✔
158
            .map_err(|e| Error::new(e.kind(), "Failed to canonicalize path"))?
2,147,483,647✔
159
    } else {
160
        cwd.join(target)
×
161
    };
162

163
    if inspect && !should_we_bury_this(target, source, metadata, mode, stream)? {
2,147,483,647✔
164
        // User chose to not bury the file
165
    } else if source.starts_with(graveyard) {
2,147,483,647✔
166
        // If rip is called on a file already in the graveyard, prompt
167
        // to permanently delete it instead.
168
        writeln!(stream, "{} is already in the graveyard.", source.display())?;
×
169
        if util::prompt_yes("Permanently unlink it?", mode, stream)? {
×
170
            if fs::remove_dir_all(source).is_err() {
×
171
                fs::remove_file(source).map_err(|e| {
×
172
                    Error::new(e.kind(), format!("Couldn't unlink {}", source.display()))
×
173
                })?;
174
            }
175
        } else {
176
            writeln!(stream, "Skipping {}", source.display())?;
×
177
            // TODO: In the original code, this was a hard return from the entire
178
            // method (i.e., `run`). I think it should just be a return from the bury
179
            // (meaning a `continue` in the original code's loop). But I'm not sure.
180
        }
181
    } else {
182
        let dest: &Path = &{
2,147,483,647✔
183
            let dest = util::join_absolute(graveyard, source);
×
184
            // Resolve a name conflict if necessary
185
            if util::symlink_exists(&dest) {
×
186
                util::rename_grave(dest)
2,147,483,647✔
187
            } else {
188
                dest
2,147,483,647✔
189
            }
190
        };
191

192
        let moved = move_target(source, dest, allow_rename, mode, stream).map_err(|e| {
2,147,483,647✔
193
            fs::remove_dir_all(dest).ok();
×
194
            Error::new(e.kind(), "Failed to bury file")
×
195
        })?;
196

197
        if moved {
×
198
            // Clean up any partial buries due to permission error
199
            record.write_log(source, dest)?;
2,147,483,647✔
200
        }
201
    }
202

203
    Ok(())
2,147,483,647✔
204
}
205

206
fn should_we_bury_this(
2,147,483,647✔
207
    target: &Path,
208
    source: &PathBuf,
209
    metadata: &Metadata,
210
    mode: &impl util::TestingMode,
211
    stream: &mut impl Write,
212
) -> Result<bool, Error> {
213
    if metadata.is_dir() {
2,147,483,647✔
214
        // Get the size of the directory and all its contents
215
        {
216
            let num_bytes = get_size(source).map_err(|_| {
2,147,483,647✔
217
                Error::new(
×
218
                    ErrorKind::Other,
×
219
                    format!("Failed to get size of directory: {}", source.display()),
×
220
                )
221
            })?;
222
            writeln!(
×
223
                stream,
×
224
                "{}: directory, {} including:",
225
                target.to_str().unwrap(),
×
226
                util::humanize_bytes(num_bytes)
×
227
            )?;
228
        }
229

230
        // Print the first few top-level files in the directory
231
        for entry in WalkDir::new(source)
2,147,483,647✔
232
            .sort_by(|a, b| a.cmp(b))
2,147,483,647✔
233
            .min_depth(1)
×
234
            .max_depth(1)
×
235
            .into_iter()
×
236
            .filter_map(|entry| entry.ok())
2,147,483,647✔
237
            .take(FILES_TO_INSPECT)
×
238
        {
239
            writeln!(stream, "{}", entry.path().display())?;
2,147,483,647✔
240
        }
241
    } else {
242
        writeln!(
2,147,483,647✔
243
            stream,
2,147,483,647✔
244
            "{}: file, {}",
245
            &target.to_str().unwrap(),
2,147,483,647✔
246
            util::humanize_bytes(metadata.len())
2,147,483,647✔
247
        )?;
248
        // Read the file and print the first few lines
249
        if let Ok(source_file) = fs::File::open(source) {
2,147,483,647✔
250
            for line in BufReader::new(source_file)
2,147,483,647✔
251
                .lines()
×
252
                .take(LINES_TO_INSPECT)
×
253
                .filter_map(|line| line.ok())
2,147,483,647✔
254
            {
255
                writeln!(stream, "> {}", line)?;
2,147,483,647✔
256
            }
257
        } else {
258
            writeln!(stream, "Error reading {}", source.display())?;
×
259
        }
260
    }
261
    util::prompt_yes(
262
        format!("Send {} to the graveyard?", target.to_str().unwrap()),
2,147,483,647✔
263
        mode,
2,147,483,647✔
264
        stream,
2,147,483,647✔
265
    )
266
}
267

268
/// Move a target to a given destination, copying if necessary.
269
/// Returns true if the target was moved, false if it was not (due to
270
/// user input)
271
pub fn move_target(
2,147,483,647✔
272
    target: &Path,
273
    dest: &Path,
274
    allow_rename: bool,
275
    mode: &impl util::TestingMode,
276
    stream: &mut impl Write,
277
) -> Result<bool, Error> {
278
    // Try a simple rename, which will only work within the same mount point.
279
    // Trying to rename across filesystems will throw errno 18.
280
    if allow_rename && fs::rename(target, dest).is_ok() {
2,147,483,647✔
281
        return Ok(true);
2,147,483,647✔
282
    }
283

284
    // If that didn't work, then we need to copy and rm.
285
    fs::create_dir_all(
286
        dest.parent()
2,147,483,647✔
287
            .ok_or_else(|| Error::new(ErrorKind::NotFound, "Could not get parent of dest!"))?,
2,147,483,647✔
288
    )?;
289

290
    if fs::symlink_metadata(target)?.is_dir() {
2,147,483,647✔
291
        move_dir(target, dest, mode, stream)
2,147,483,647✔
292
    } else {
293
        let moved = copy_file(target, dest, mode, stream).map_err(|e| {
2,147,483,647✔
294
            Error::new(
×
295
                e.kind(),
×
296
                format!(
×
297
                    "Failed to copy file from {} to {}",
×
298
                    target.display(),
×
299
                    dest.display()
×
300
                ),
301
            )
302
        })?;
303
        fs::remove_file(target).map_err(|e| {
×
304
            Error::new(
×
305
                e.kind(),
×
306
                format!("Failed to remove file: {}", target.display()),
×
307
            )
308
        })?;
309
        Ok(moved)
2,147,483,647✔
310
    }
311
}
312

313
/// Move a target which is a directory to a given destination, copying if necessary.
314
/// Returns true *always*, as the creation of the directory is enough to mark it as successful.
315
pub fn move_dir(
2,147,483,647✔
316
    target: &Path,
317
    dest: &Path,
318
    mode: &impl util::TestingMode,
319
    stream: &mut impl Write,
320
) -> Result<bool, Error> {
321
    // Walk the source, creating directories and copying files as needed
322
    for entry in WalkDir::new(target).into_iter().filter_map(|e| e.ok()) {
2,147,483,647✔
323
        // Path without the top-level directory
324
        let orphan = entry.path().strip_prefix(target).map_err(|_| {
2,147,483,647✔
325
            Error::new(
×
326
                ErrorKind::Other,
×
327
                "Parent directory isn't a prefix of child directories?",
×
328
            )
329
        })?;
330

331
        if entry.file_type().is_dir() {
×
332
            fs::create_dir_all(dest.join(orphan)).map_err(|e| {
2,147,483,647✔
333
                Error::new(
1✔
334
                    e.kind(),
1✔
335
                    format!(
1✔
336
                        "Failed to create dir: {} in {}",
1✔
337
                        entry.path().display(),
1✔
338
                        dest.join(orphan).display()
1✔
339
                    ),
340
                )
341
            })?;
342
        } else {
343
            copy_file(entry.path(), &dest.join(orphan), mode, stream).map_err(|e| {
2,147,483,647✔
344
                Error::new(
×
345
                    e.kind(),
×
346
                    format!(
×
347
                        "Failed to copy file from {} to {}",
×
348
                        entry.path().display(),
×
349
                        dest.join(orphan).display()
×
350
                    ),
351
                )
352
            })?;
353
        }
354
    }
355
    fs::remove_dir_all(target).map_err(|e| {
2,147,483,647✔
356
        Error::new(
2,147,483,647✔
357
            e.kind(),
2,147,483,647✔
358
            format!("Failed to remove dir: {}", target.display()),
2,147,483,647✔
359
        )
360
    })?;
361

362
    Ok(true)
2,147,483,647✔
363
}
364

365
pub fn copy_file(
2,147,483,647✔
366
    source: &Path,
367
    dest: &Path,
368
    mode: &impl util::TestingMode,
369
    stream: &mut impl Write,
370
) -> Result<bool, Error> {
371
    let metadata = fs::symlink_metadata(source)?;
2,147,483,647✔
372
    let filetype = metadata.file_type();
×
373

374
    if metadata.len() > BIG_FILE_THRESHOLD {
×
375
        writeln!(
2,147,483,647✔
376
            stream,
2,147,483,647✔
377
            "About to copy a big file ({} is {})",
378
            source.display(),
2,147,483,647✔
379
            util::humanize_bytes(metadata.len())
2,147,483,647✔
380
        )?;
381
        if util::prompt_yes("Permanently delete this file instead?", mode, stream)? {
2,147,483,647✔
382
            return Ok(false);
2,147,483,647✔
383
        }
384
    }
385

386
    if filetype.is_file() {
2,147,483,647✔
387
        fs::copy(source, dest)?;
2,147,483,647✔
388
        return Ok(true);
2,147,483,647✔
389
    }
390

391
    #[cfg(unix)]
2,147,483,647✔
392
    if filetype.is_fifo() {
2,147,483,647✔
393
        let metadata_mode = metadata.permissions().mode();
2,147,483,647✔
394
        std::process::Command::new("mkfifo")
2,147,483,647✔
395
            .arg(dest)
2,147,483,647✔
396
            .arg("-m")
397
            .arg(metadata_mode.to_string())
2,147,483,647✔
398
            .output()?;
399
        return Ok(true);
2,147,483,647✔
400
    }
401

402
    if filetype.is_symlink() {
2,147,483,647✔
403
        let target = fs::read_link(source)?;
2,147,483,647✔
404
        symlink(target, dest)?;
×
405
        return Ok(true);
2,147,483,647✔
406
    }
407

UNCOV
408
    match fs::copy(source, dest) {
×
UNCOV
409
        Err(e) => {
×
410
            // Special file: Try copying it as normal, but this probably won't work
UNCOV
411
            writeln!(
×
UNCOV
412
                stream,
×
413
                "Non-regular file or directory: {}",
UNCOV
414
                source.display()
×
415
            )?;
416

UNCOV
417
            if util::prompt_yes("Permanently delete the file?", mode, stream)? {
×
UNCOV
418
                Ok(false)
×
419
            } else {
420
                Err(e)
×
421
            }
422
        }
423
        Ok(_) => Ok(true),
×
424
    }
425
}
426

427
pub fn get_graveyard(graveyard: Option<PathBuf>) -> PathBuf {
2,147,483,647✔
428
    if let Some(flag) = graveyard {
2,147,483,647✔
429
        flag
430
    } else if let Ok(env_graveyard) = env::var("RIP_GRAVEYARD") {
2,147,483,647✔
431
        PathBuf::from(env_graveyard)
2,147,483,647✔
432
    } else if let Ok(mut env_graveyard) = env::var("XDG_DATA_HOME") {
2,147,483,647✔
433
        if !env_graveyard.ends_with(std::path::MAIN_SEPARATOR) {
2,147,483,647✔
434
            env_graveyard.push(std::path::MAIN_SEPARATOR);
2,147,483,647✔
435
        }
436
        env_graveyard.push_str("graveyard");
2,147,483,647✔
437
        PathBuf::from(env_graveyard)
2,147,483,647✔
438
    } else {
439
        let user = util::get_user();
2,147,483,647✔
440
        env::temp_dir().join(format!("graveyard-{}", user))
2,147,483,647✔
441
    }
442
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc