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
//!
use std::path::{Path, PathBuf};

/// A special iterator which communicates its operation through results where…
///
/// * `Some(Ok(removed_directory))` is yielded once or more success, followed by `None`
/// * `Some(Err(std::io::Error))` is yielded exactly once on failure.
pub struct Iter<'a> {
    cursor: Option<&'a Path>,
    boundary: &'a Path,
}

/// Construction
impl<'a> Iter<'a> {
    /// Create a new instance that deletes `target` but will stop at `boundary`, without deleting the latter.
    /// Returns an error if `boundary` doesn't contain `target`
    ///
    /// **Note** that we don't canonicalize the path for performance reasons.
    pub fn new(target: &'a Path, boundary: &'a Path) -> std::io::Result<Self> {
        if !target.starts_with(boundary) {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!(
                    "Removal target {:?} must be contained in boundary {:?}",
                    target, boundary
                ),
            ));
        }
        let cursor = if target == boundary {
            None
        } else if target.exists() {
            Some(target)
        } else {
            None
        };
        Ok(Iter { cursor, boundary })
    }
}

impl<'a> Iterator for Iter<'a> {
    type Item = std::io::Result<&'a Path>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.cursor.take() {
            Some(dir) => {
                let next = match std::fs::remove_dir(dir) {
                    Ok(()) => Some(Ok(dir)),
                    Err(err) => match err.kind() {
                        std::io::ErrorKind::NotFound => Some(Ok(dir)),
                        _other_error_kind => return Some(Err(err)),
                    },
                };
                self.cursor = match dir.parent() {
                    Some(parent) => (parent != self.boundary).then(|| parent),
                    None => {
                        unreachable!("directory {:?} ran out of parents, this really shouldn't happen before hitting the boundary {:?}")
                    }
                };
                next
            }
            None => None,
        }
    }
}

/// Delete all empty directories from `delete_dir` upward and until (not including) the `boundary_dir`.
///
/// Note that `boundary_dir` must contain `delete_dir` or an error is returned, otherwise `delete_dir` is returned on success.
pub fn empty_upward_until_boundary<'a>(delete_dir: &'a Path, boundary_dir: &'a Path) -> std::io::Result<&'a Path> {
    for item in Iter::new(delete_dir, boundary_dir)? {
        match item {
            Ok(_dir) => continue,
            Err(err) => return Err(err),
        }
    }
    Ok(delete_dir)
}

/// Delete all empty directories reachable from `delete_dir` from empty leaves moving upward to and including `delete_dir`.
///
/// If any encountered directory contains a file the entire operation is aborted.
/// Please note that this is inherently racy and no attempts are made to counter that, which will allow creators to win
/// as long as they retry.
pub fn empty_depth_first(delete_dir: impl Into<PathBuf>) -> std::io::Result<()> {
    let delete_dir = delete_dir.into();
    if let Ok(()) = std::fs::remove_dir(&delete_dir) {
        return Ok(());
    }

    let mut stack = vec![delete_dir];
    let mut next_to_push = Vec::new();
    while let Some(dir_to_delete) = stack.pop() {
        let mut num_entries = 0;
        for entry in std::fs::read_dir(&dir_to_delete)? {
            num_entries += 1;
            let entry = entry?;
            if entry.file_type()?.is_dir() {
                next_to_push.push(entry.path());
            } else {
                return Err(std::io::Error::new(std::io::ErrorKind::Other, "Directory not empty"));
            }
        }
        if num_entries == 0 {
            std::fs::remove_dir(&dir_to_delete)?;
        } else {
            stack.push(dir_to_delete);
            stack.append(&mut next_to_push);
        }
    }
    Ok(())
}