Commit ac8d444a authored by Nick Mathewson's avatar Nick Mathewson 🦞
Browse files

Merge branch 'ticket_282' into 'main'

Remove obsolete files from our state directory.

Closes #282

See merge request !457
parents 41dd6825 2847a57a
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -3595,6 +3595,7 @@ dependencies = [
 "tempfile",
 "tempfile",
 "thiserror",
 "thiserror",
 "tor-error",
 "tor-error",
 "tracing",
]
]


[[package]]
[[package]]
+5 −4
Original line number Original line Diff line number Diff line
@@ -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]
+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;
@@ -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 {
@@ -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)
+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");
    }
}