Add logic to filter out directories which can be deleted
- ID
720a78e- date
2024-06-18 20:34:54+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
efc16b5- message
Add logic to filter out directories which can be deleted- changed files
4 files, 363 additions, 2 deletions
Changed files
Cargo.lock (152) → Cargo.lock (5655)
diff --git a/Cargo.lock b/Cargo.lock
index c821844..c5ba4b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5,3 +5,198 @@ version = 3
[[package]]
name = "emptydir"
version = "0.1.0"
+dependencies = [
+ "tempdir",
+ "walkdir",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand",
+ "remove_dir_all",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
Cargo.toml (79) → Cargo.toml (111)
diff --git a/Cargo.toml b/Cargo.toml
index b7f1e85..00572cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,3 +4,5 @@ version = "0.1.0"
edition = "2021"
[dependencies]
+tempdir = "0.3.7"
+walkdir = "2"
src/can_be_deleted.rs (0) → src/can_be_deleted.rs (4241)
diff --git a/src/can_be_deleted.rs b/src/can_be_deleted.rs
new file mode 100644
index 0000000..815d74a
--- /dev/null
+++ b/src/can_be_deleted.rs
@@ -0,0 +1,147 @@
+use std::collections::HashSet;
+use std::fs;
+use std::io;
+use std::path::Path;
+
+/// Given a Result<DirEntry> from `fs::read_dir()`, try to get the
+/// filename of the entry.
+///
+/// Filenames will be lowercased for easy comparisons.
+///
+fn file_name(dir_entry: io::Result<fs::DirEntry>) -> Option<String> {
+ match dir_entry.map(|e| e.file_name().into_string()) {
+ Ok(Ok(s)) => Some(s.to_lowercase()),
+ _ => None,
+ }
+}
+
+pub fn can_be_deleted(path: &Path) -> bool {
+ // This is a folder where I put files that I explicitly don't want
+ // to include in my backups.
+ //
+ // It may sometimes be empty, but I never want to delete it.
+ // See https://overcast.fm/+R7DX9_W-Y/21:22 or my Obsidian note.
+ match path.canonicalize() {
+ Ok(p) if p == Path::new("/Users/alexwlchan/Desktop/do not back up") => return false,
+ _ => (),
+ };
+
+ // This is the list of entries which I consider safe to delete.
+ //
+ // * .DS_Store stores some folder attributes used for showing the folder
+ // in the Finder, which I don't need to keep
+ // * `__pycache__` is the bytecode cache in Python projects, which is
+ // pointless if the original Python files have been removed
+ // * `.venv` is the name I use for virtual environments, which I can
+ // easily regenerate if necessary
+ //
+ // A directory is safe to delete if the ONLY things it contains are these entries;
+ // any other entry should block the directory from being deleted.
+ //
+ let deletable_entries: HashSet<Option<String>> = [".DS_Store", "__pycache__", ".venv"]
+ .iter()
+ .map(|&s| Some(s.to_lowercase().to_owned()))
+ .collect();
+
+ match fs::read_dir(path) {
+ Ok(entries) => {
+ let names: HashSet<Option<String>> = HashSet::from_iter(entries.map(|e| file_name(e)));
+
+ names.is_subset(&deletable_entries)
+ }
+ _ => false,
+ }
+}
+
+#[cfg(test)]
+mod can_be_deleted_tests {
+ use std::fs;
+ use std::path::{Path, PathBuf};
+
+ use crate::can_be_deleted::can_be_deleted;
+
+ fn test_dir() -> PathBuf {
+ let tmp_dir = tempdir::TempDir::new("testing").unwrap();
+ let path = tmp_dir.path();
+ path.to_owned()
+ }
+
+ fn create_dir(path: &PathBuf) {
+ fs::create_dir_all(path).unwrap();
+ }
+
+ fn create_file(path: PathBuf) {
+ fs::write(&path, "this file is for testing").unwrap();
+ }
+
+ #[test]
+ fn it_doesnt_delete_my_do_not_backup() {
+ let path = Path::new("/Users/alexwlchan/Desktop/do not back up");
+ assert_eq!(can_be_deleted(&path), false);
+ }
+
+ #[test]
+ fn a_dir_cant_be_deleted_if_we_cant_read_the_contents() {
+ let path = Path::new("/does/not/exist");
+ assert_eq!(can_be_deleted(&path), false);
+ }
+
+ #[test]
+ fn an_empty_dir_can_be_deleted() {
+ let path = test_dir();
+
+ // Create the directory, but don't put anything in it
+ create_dir(&path);
+
+ assert_eq!(can_be_deleted(&path), true);
+ }
+
+ #[test]
+ fn a_directory_with_extra_entries_cannot_be_deleted() {
+ let path = test_dir();
+
+ // Create the directory, then add a text file
+ create_dir(&path);
+
+ create_file(path.join("greeting.txt"));
+
+ assert_eq!(can_be_deleted(&path), false);
+ }
+
+ #[test]
+ fn a_directory_with_only_safe_to_delete_entries_can_be_deleted() {
+ let path = test_dir();
+
+ // Create the directory, then add subdirectories
+ create_dir(&path);
+
+ create_dir(&path.join(".venv"));
+ create_dir(&path.join("__pycache__"));
+ create_file(path.join(".DS_Store"));
+
+ assert_eq!(can_be_deleted(&path), true);
+ }
+
+ #[test]
+ fn a_directory_with_mix_of_safe_and_unsafe_entries_cannot_be_deleted() {
+ let path = test_dir();
+
+ create_dir(&path);
+
+ create_file(path.join(".DS_Store"));
+ create_file(path.join("greeting.txt"));
+
+ assert_eq!(can_be_deleted(&path), false);
+ }
+
+ #[test]
+ fn safe_to_delete_entries_are_case_insensitive() {
+ let path = test_dir();
+
+ create_dir(&path);
+
+ create_file(path.join(".ds_store"));
+
+ assert_eq!(can_be_deleted(&path), true);
+ }
+}
src/main.rs (45) → src/main.rs (430)
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..68da85b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,20 @@
-fn main() {
- println!("Hello, world!");
+#![deny(warnings)]
+
+use walkdir::WalkDir;
+
+mod can_be_deleted;
+
+fn main() -> Result<(), std::io::Error> {
+ let iterator = WalkDir::new(".")
+ .contents_first(true)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.file_type().is_dir())
+ .filter(|e| can_be_deleted::can_be_deleted(e.path()));
+
+ for entry in iterator {
+ println!("{}", entry.path().display());
+ }
+
+ Ok(())
}