Loading Cargo.lock +1 −0 Original line number Original line Diff line number Diff line Loading @@ -3595,6 +3595,7 @@ dependencies = [ "tempfile", "tempfile", "thiserror", "thiserror", "tor-error", "tor-error", "tracing", ] ] [[package]] [[package]] Loading crates/tor-persist/Cargo.toml +5 −4 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,7 @@ serde = { version = "1.0.103", features = ["derive"] } serde_json = "1.0.50" serde_json = "1.0.50" sanitize-filename = "0.3.0" sanitize-filename = "0.3.0" thiserror = "1" thiserror = "1" tracing = "0.1.18" tor-error = { path = "../tor-error", version = "0.2.0" } tor-error = { path = "../tor-error", version = "0.2.0" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] Loading crates/tor-persist/src/fs.rs +17 −0 Original line number Original line Diff line number Diff line //! Filesystem + JSON implementation of StateMgr. //! Filesystem + JSON implementation of StateMgr. mod clean; use crate::{load_error, store_error}; use crate::{load_error, store_error}; use crate::{Error, LockStatus, Result, StateMgr}; use crate::{Error, LockStatus, Result, StateMgr}; use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize}; use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tracing::{info, warn}; #[cfg(target_family = "unix")] #[cfg(target_family = "unix")] use std::os::unix::fs::DirBuilderExt; use std::os::unix::fs::DirBuilderExt; Loading Loading @@ -92,6 +96,18 @@ impl FsStateMgr { .parent() .parent() .expect("No parent directory even after path.join?") .expect("No parent directory even after path.join?") } } /// Remove old and/or obsolete items from this storage manager. /// /// Requires that we hold the lock. fn clean(&self) { for fname in clean::files_to_delete(&self.inner.statepath, SystemTime::now()) { info!("Deleting obsolete file {}", fname.display()); if let Err(e) = std::fs::remove_file(&fname) { warn!("Unable to delete {}: {}", fname.display(), e); } } } } } impl StateMgr for FsStateMgr { impl StateMgr for FsStateMgr { Loading @@ -112,6 +128,7 @@ impl StateMgr for FsStateMgr { if lockfile.owns_lock() { if lockfile.owns_lock() { Ok(LockStatus::AlreadyHeld) Ok(LockStatus::AlreadyHeld) } else if lockfile.try_lock()? { } else if lockfile.try_lock()? { self.clean(); Ok(LockStatus::NewlyAcquired) Ok(LockStatus::NewlyAcquired) } else { } else { Ok(LockStatus::NoLock) Ok(LockStatus::NoLock) Loading crates/tor-persist/src/fs/clean.rs 0 → 100644 +154 −0 Original line number Original line Diff line number Diff line //! Code to remove obsolete and extraneous files from a filesystem-based state //! directory. use std::{ path::{Path, PathBuf}, time::{Duration, SystemTime}, }; use tracing::warn; /// Return true if `path` looks like a filename we'd like to remove from our /// state directory. fn fname_looks_obsolete(path: &Path) -> bool { if let Some(extension) = path.extension() { if extension == "toml" { // We don't make toml files any more. We migrated to json because // toml isn't so good for serializing arbitrary objects. return true; } } if let Some(stem) = path.file_stem() { if stem == "default_guards" { // This file type is obsolete and was removed around 0.0.4. return true; } } false } /// How old must an obsolete-looking file be before we're willing to remove it? // // TODO: This could someday be configurable, if there are in fact users who want // to keep obsolete files around in their state directories for months or years, // or who need to get rid of them immediately. const CUTOFF: Duration = Duration::from_secs(4 * 24 * 60 * 60); /// Return true if `entry` is very old relative to `now` and therefore safe to delete. fn very_old(entry: &std::fs::DirEntry, now: SystemTime) -> std::io::Result<bool> { Ok(match now.duration_since(entry.metadata()?.modified()?) { Ok(age) => age > CUTOFF, Err(_) => { // If duration_since failed, this file is actually from the future, and so it definitely isn't older than the cutoff. false } }) } /// Implementation helper for [`FsStateMgr::clean()`](super::FsStateMgr::clean): /// list all files in `statepath` that are ready to delete as of `now`. pub(super) fn files_to_delete(statepath: &Path, now: SystemTime) -> Vec<PathBuf> { let mut result = Vec::new(); let dir_read_failed = |err: std::io::Error| { use std::io::ErrorKind as EK; match err.kind() { EK::NotFound => {} _ => warn!( "Failed to scan directory {} for obsolete files: {}", statepath.display(), err, ), } }; let entries = std::fs::read_dir(statepath) .map_err(dir_read_failed) // Result from fs::read_dir .into_iter() .flatten() // TODO: Use map_while once we are on Rust >= 1.57 .map(|result| result.map_err(dir_read_failed).ok()) // Result from dir.next() .take_while(|result| result.is_some()) .flatten(); for entry in entries { let path = entry.path(); let basename = entry.file_name(); if fname_looks_obsolete(Path::new(&basename)) { match very_old(&entry, now) { Ok(true) => result.push(path), Ok(false) => { warn!( "Found obsolete file {}; will delete it when it is older.", entry.path().display(), ); } Err(err) => { warn!( "Found obsolete file {} but could not access its modification time: {}", entry.path().display(), err, ); } } } } result } #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] use super::*; #[test] fn fnames() { let examples = vec![ ("guards", false), ("default_guards.json", true), ("guards.toml", true), ("marzipan.toml", true), ("marzipan.json", false), ]; for (name, obsolete) in examples { assert_eq!(fname_looks_obsolete(Path::new(name)), obsolete); } } #[test] fn age() { let dir = tempfile::TempDir::new().unwrap(); let fname1 = dir.path().join("quokka"); let now = SystemTime::now(); std::fs::write(&fname1, "hello world").unwrap(); let mut r = std::fs::read_dir(dir.path()).unwrap(); let ent = r.next().unwrap().unwrap(); assert!(!very_old(&ent, now).unwrap()); assert!(very_old(&ent, now + CUTOFF * 2).unwrap()); } #[test] fn list() { let dir = tempfile::TempDir::new().unwrap(); let now = SystemTime::now(); let fname1 = dir.path().join("quokka.toml"); std::fs::write(&fname1, "hello world").unwrap(); let fname2 = dir.path().join("wombat.json"); std::fs::write(&fname2, "greetings").unwrap(); let removable_now = files_to_delete(dir.path(), now); assert!(removable_now.is_empty()); let removable_later = files_to_delete(dir.path(), now + CUTOFF * 2); assert_eq!(removable_later.len(), 1); assert_eq!(removable_later[0].file_stem().unwrap(), "quokka"); } } Loading
Cargo.lock +1 −0 Original line number Original line Diff line number Diff line Loading @@ -3595,6 +3595,7 @@ dependencies = [ "tempfile", "tempfile", "thiserror", "thiserror", "tor-error", "tor-error", "tracing", ] ] [[package]] [[package]] Loading
crates/tor-persist/Cargo.toml +5 −4 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,7 @@ serde = { version = "1.0.103", features = ["derive"] } serde_json = "1.0.50" serde_json = "1.0.50" sanitize-filename = "0.3.0" sanitize-filename = "0.3.0" thiserror = "1" thiserror = "1" tracing = "0.1.18" tor-error = { path = "../tor-error", version = "0.2.0" } tor-error = { path = "../tor-error", version = "0.2.0" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] Loading
crates/tor-persist/src/fs.rs +17 −0 Original line number Original line Diff line number Diff line //! Filesystem + JSON implementation of StateMgr. //! Filesystem + JSON implementation of StateMgr. mod clean; use crate::{load_error, store_error}; use crate::{load_error, store_error}; use crate::{Error, LockStatus, Result, StateMgr}; use crate::{Error, LockStatus, Result, StateMgr}; use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize}; use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tracing::{info, warn}; #[cfg(target_family = "unix")] #[cfg(target_family = "unix")] use std::os::unix::fs::DirBuilderExt; use std::os::unix::fs::DirBuilderExt; Loading Loading @@ -92,6 +96,18 @@ impl FsStateMgr { .parent() .parent() .expect("No parent directory even after path.join?") .expect("No parent directory even after path.join?") } } /// Remove old and/or obsolete items from this storage manager. /// /// Requires that we hold the lock. fn clean(&self) { for fname in clean::files_to_delete(&self.inner.statepath, SystemTime::now()) { info!("Deleting obsolete file {}", fname.display()); if let Err(e) = std::fs::remove_file(&fname) { warn!("Unable to delete {}: {}", fname.display(), e); } } } } } impl StateMgr for FsStateMgr { impl StateMgr for FsStateMgr { Loading @@ -112,6 +128,7 @@ impl StateMgr for FsStateMgr { if lockfile.owns_lock() { if lockfile.owns_lock() { Ok(LockStatus::AlreadyHeld) Ok(LockStatus::AlreadyHeld) } else if lockfile.try_lock()? { } else if lockfile.try_lock()? { self.clean(); Ok(LockStatus::NewlyAcquired) Ok(LockStatus::NewlyAcquired) } else { } else { Ok(LockStatus::NoLock) Ok(LockStatus::NoLock) Loading
crates/tor-persist/src/fs/clean.rs 0 → 100644 +154 −0 Original line number Original line Diff line number Diff line //! Code to remove obsolete and extraneous files from a filesystem-based state //! directory. use std::{ path::{Path, PathBuf}, time::{Duration, SystemTime}, }; use tracing::warn; /// Return true if `path` looks like a filename we'd like to remove from our /// state directory. fn fname_looks_obsolete(path: &Path) -> bool { if let Some(extension) = path.extension() { if extension == "toml" { // We don't make toml files any more. We migrated to json because // toml isn't so good for serializing arbitrary objects. return true; } } if let Some(stem) = path.file_stem() { if stem == "default_guards" { // This file type is obsolete and was removed around 0.0.4. return true; } } false } /// How old must an obsolete-looking file be before we're willing to remove it? // // TODO: This could someday be configurable, if there are in fact users who want // to keep obsolete files around in their state directories for months or years, // or who need to get rid of them immediately. const CUTOFF: Duration = Duration::from_secs(4 * 24 * 60 * 60); /// Return true if `entry` is very old relative to `now` and therefore safe to delete. fn very_old(entry: &std::fs::DirEntry, now: SystemTime) -> std::io::Result<bool> { Ok(match now.duration_since(entry.metadata()?.modified()?) { Ok(age) => age > CUTOFF, Err(_) => { // If duration_since failed, this file is actually from the future, and so it definitely isn't older than the cutoff. false } }) } /// Implementation helper for [`FsStateMgr::clean()`](super::FsStateMgr::clean): /// list all files in `statepath` that are ready to delete as of `now`. pub(super) fn files_to_delete(statepath: &Path, now: SystemTime) -> Vec<PathBuf> { let mut result = Vec::new(); let dir_read_failed = |err: std::io::Error| { use std::io::ErrorKind as EK; match err.kind() { EK::NotFound => {} _ => warn!( "Failed to scan directory {} for obsolete files: {}", statepath.display(), err, ), } }; let entries = std::fs::read_dir(statepath) .map_err(dir_read_failed) // Result from fs::read_dir .into_iter() .flatten() // TODO: Use map_while once we are on Rust >= 1.57 .map(|result| result.map_err(dir_read_failed).ok()) // Result from dir.next() .take_while(|result| result.is_some()) .flatten(); for entry in entries { let path = entry.path(); let basename = entry.file_name(); if fname_looks_obsolete(Path::new(&basename)) { match very_old(&entry, now) { Ok(true) => result.push(path), Ok(false) => { warn!( "Found obsolete file {}; will delete it when it is older.", entry.path().display(), ); } Err(err) => { warn!( "Found obsolete file {} but could not access its modification time: {}", entry.path().display(), err, ); } } } } result } #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] use super::*; #[test] fn fnames() { let examples = vec![ ("guards", false), ("default_guards.json", true), ("guards.toml", true), ("marzipan.toml", true), ("marzipan.json", false), ]; for (name, obsolete) in examples { assert_eq!(fname_looks_obsolete(Path::new(name)), obsolete); } } #[test] fn age() { let dir = tempfile::TempDir::new().unwrap(); let fname1 = dir.path().join("quokka"); let now = SystemTime::now(); std::fs::write(&fname1, "hello world").unwrap(); let mut r = std::fs::read_dir(dir.path()).unwrap(); let ent = r.next().unwrap().unwrap(); assert!(!very_old(&ent, now).unwrap()); assert!(very_old(&ent, now + CUTOFF * 2).unwrap()); } #[test] fn list() { let dir = tempfile::TempDir::new().unwrap(); let now = SystemTime::now(); let fname1 = dir.path().join("quokka.toml"); std::fs::write(&fname1, "hello world").unwrap(); let fname2 = dir.path().join("wombat.json"); std::fs::write(&fname2, "greetings").unwrap(); let removable_now = files_to_delete(dir.path(), now); assert!(removable_now.is_empty()); let removable_later = files_to_delete(dir.path(), now + CUTOFF * 2); assert_eq!(removable_later.len(), 1); assert_eq!(removable_later[0].file_stem().unwrap(), "quokka"); } }