zoxide/src/store/mod.rs

277 lines
7.9 KiB
Rust
Raw Normal View History

mod dir;
mod query;
2020-10-18 09:22:13 +00:00
2021-01-08 15:15:47 +00:00
pub use dir::{Dir, DirList, Epoch, Rank};
pub use query::Query;
use anyhow::{Context, Result};
2020-10-18 09:22:13 +00:00
use ordered_float::OrderedFloat;
2020-10-20 06:17:12 +00:00
use tempfile::{NamedTempFile, PersistError};
2020-10-18 09:22:13 +00:00
2021-01-08 15:15:47 +00:00
use std::borrow::Cow;
2020-10-18 09:22:13 +00:00
use std::cmp::Reverse;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
2021-01-08 15:15:47 +00:00
pub struct Store<'a> {
pub dirs: DirList<'a>,
2020-10-18 09:22:13 +00:00
pub modified: bool,
2021-01-08 15:15:47 +00:00
data_dir: &'a Path,
2020-10-18 09:22:13 +00:00
}
2021-01-08 15:15:47 +00:00
impl<'a> Store<'a> {
2020-10-18 09:22:13 +00:00
pub fn save(&mut self) -> Result<()> {
if !self.modified {
return Ok(());
}
2021-01-08 15:15:47 +00:00
let buffer = self.dirs.to_bytes()?;
2020-10-20 06:17:12 +00:00
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
format!(
"could not create temporary store in: {}",
self.data_dir.display()
)
})?;
2021-01-08 15:15:47 +00:00
// Preallocate enough space on the file, preventing copying later on.
// This optimization may fail on some filesystems, but it is safe to
// ignore it and proceed.
let _ = file.as_file().set_len(buffer.len() as _);
2020-10-20 06:17:12 +00:00
file.write_all(&buffer).with_context(|| {
format!(
"could not write to temporary store: {}",
file.path().display()
)
})?;
2021-01-08 15:15:47 +00:00
let path = store_path(&self.data_dir);
2020-10-20 06:17:12 +00:00
persist(file, &path)
.with_context(|| format!("could not replace store: {}", path.display()))?;
2020-10-18 09:22:13 +00:00
self.modified = false;
Ok(())
}
pub fn add<S: AsRef<str>>(&mut self, path: S, now: Epoch) {
let path = path.as_ref();
debug_assert!(Path::new(path).is_absolute());
match self.dirs.iter_mut().find(|dir| dir.path == path) {
None => self.dirs.push(Dir {
2021-01-08 15:15:47 +00:00
path: Cow::Owned(path.into()),
2020-10-18 09:22:13 +00:00
last_accessed: now,
rank: 1.0,
}),
Some(dir) => {
dir.last_accessed = now;
dir.rank += 1.0;
}
};
self.modified = true;
}
2021-01-08 15:15:47 +00:00
pub fn iter_matches<'b>(
&'b mut self,
query: &'b Query,
2020-10-18 09:22:13 +00:00
now: Epoch,
2021-01-08 15:15:47 +00:00
) -> impl DoubleEndedIterator<Item = &'b Dir> {
2020-10-18 09:22:13 +00:00
self.dirs
2021-01-08 15:15:47 +00:00
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
2020-10-18 09:22:13 +00:00
self.dirs.iter().filter(move |dir| dir.is_match(&query))
}
pub fn remove<S: AsRef<str>>(&mut self, path: S) -> bool {
let path = path.as_ref();
if let Some(idx) = self.dirs.iter().position(|dir| dir.path == path) {
self.dirs.swap_remove(idx);
self.modified = true;
return true;
}
false
}
pub fn age(&mut self, max_age: Rank) {
let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::<Rank>();
if sum_age > max_age {
let factor = 0.9 * max_age / sum_age;
for idx in (0..self.dirs.len()).rev() {
let dir = &mut self.dirs[idx];
dir.rank *= factor;
if dir.rank < 1.0 {
self.dirs.swap_remove(idx);
}
}
self.modified = true;
}
}
}
2021-01-08 15:15:47 +00:00
impl Drop for Store<'_> {
2020-10-18 09:22:13 +00:00
fn drop(&mut self) {
2021-01-08 15:15:47 +00:00
// Since the error can't be properly handled here,
// pretty-print it instead.
2020-10-18 09:22:13 +00:00
if let Err(e) = self.save() {
println!("Error: {}", e)
}
}
}
#[cfg(windows)]
2020-10-20 06:17:12 +00:00
fn persist<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> {
use rand::distributions::{Distribution, Uniform};
use std::thread;
use std::time::Duration;
// File renames on Windows are not atomic and sometimes fail with `PermissionDenied`.
// This is extremely unlikely unless it's running in a loop on multiple threads.
// Nevertheless, we guard against it by retrying the rename a fixed number of times.
const MAX_TRIES: usize = 10;
let mut rng = None;
for _ in 0..MAX_TRIES {
match file.persist(&path) {
Ok(_) => break,
Err(e) if e.error.kind() == io::ErrorKind::PermissionDenied => {
let mut rng = rng.get_or_insert_with(rand::thread_rng);
let between = Uniform::from(50..150);
let duration = Duration::from_millis(between.sample(&mut rng));
thread::sleep(duration);
file = e.file;
2020-10-20 06:17:12 +00:00
}
Err(e) => return Err(e),
2020-10-20 06:17:12 +00:00
}
}
Ok(())
}
#[cfg(unix)]
fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistError> {
file.persist(&path)?;
Ok(())
}
2021-01-08 15:15:47 +00:00
pub struct StoreBuilder {
data_dir: PathBuf,
buffer: Vec<u8>,
}
impl StoreBuilder {
pub fn new<P: Into<PathBuf>>(data_dir: P) -> StoreBuilder {
StoreBuilder {
data_dir: data_dir.into(),
buffer: Vec::new(),
}
}
pub fn build(&mut self) -> Result<Store> {
// Read the entire store to memory. For smaller files, this is faster
// than mmap / streaming, and allows for zero-copy deserialization.
let path = store_path(&self.data_dir);
match fs::read(&path) {
Ok(buffer) => {
self.buffer = buffer;
let dirs = DirList::from_bytes(&self.buffer)
.with_context(|| format!("could not deserialize store: {}", path.display()))?;
Ok(Store {
dirs,
modified: false,
data_dir: &self.data_dir,
})
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Create data directory, but don't create any file yet.
// The file will be created later by [`Store::save`]
// if any data is modified.
fs::create_dir_all(&self.data_dir).with_context(|| {
format!(
"unable to create data directory: {}",
self.data_dir.display()
)
})?;
Ok(Store {
dirs: DirList::new(),
modified: false,
data_dir: &self.data_dir,
})
}
Err(e) => {
Err(e).with_context(|| format!("could not read from store: {}", path.display()))
}
}
}
}
fn store_path<P: AsRef<Path>>(data_dir: P) -> PathBuf {
const STORE_FILENAME: &str = "db.zo";
data_dir.as_ref().join(STORE_FILENAME)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
2020-11-10 19:11:26 +00:00
fn add() {
2020-10-26 19:55:02 +00:00
let path = if cfg!(windows) {
r"C:\foo\bar"
} else {
"/foo/bar"
};
let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
{
2021-01-08 15:15:47 +00:00
let mut store = StoreBuilder::new(data_dir.path());
let mut store = store.build().unwrap();
store.add(path, now);
store.add(path, now);
}
{
2021-01-08 15:15:47 +00:00
let mut store = StoreBuilder::new(data_dir.path());
let store = store.build().unwrap();
assert_eq!(store.dirs.len(), 1);
let dir = &store.dirs[0];
assert_eq!(dir.path, path);
assert_eq!(dir.last_accessed, now);
}
}
#[test]
2020-11-10 19:11:26 +00:00
fn remove() {
2020-10-26 19:55:02 +00:00
let path = if cfg!(windows) {
r"C:\foo\bar"
} else {
"/foo/bar"
};
let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
{
2021-01-08 15:15:47 +00:00
let mut store = StoreBuilder::new(data_dir.path());
let mut store = store.build().unwrap();
store.add(path, now);
}
{
2021-01-08 15:15:47 +00:00
let mut store = StoreBuilder::new(data_dir.path());
let mut store = store.build().unwrap();
assert!(store.remove(path));
}
{
2021-01-08 15:15:47 +00:00
let mut store = StoreBuilder::new(data_dir.path());
let mut store = store.build().unwrap();
assert!(store.dirs.is_empty());
assert!(!store.remove(path));
}
}
}