mirror of
https://github.com/Llewellynvdm/zoxide.git
synced 2024-11-17 10:35:12 +00:00
Handle write errors gracefully (#143)
This commit is contained in:
parent
d89605ffef
commit
1a5d14a825
@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Removed unnecessary backtraces on Rust nightly.
|
||||||
|
- Fixed generated shell code to avoid accidentally using aliased builtins.
|
||||||
|
- Handle broken pipe errors gracefully when writing to streams.
|
||||||
|
|
||||||
## [0.5.0] - 2020-10-30
|
## [0.5.0] - 2020-10-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -114,12 +114,18 @@ zoxide for interactive selection. Installation instructions can be found
|
|||||||
### Step 3: Add zoxide to your shell
|
### Step 3: Add zoxide to your shell
|
||||||
|
|
||||||
If you currently use `z`, `z.lua`, or `zsh-z`, you may want to first import
|
If you currently use `z`, `z.lua`, or `zsh-z`, you may want to first import
|
||||||
your existing database into `zoxide`:
|
your existing entries into `zoxide`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
zoxide import /path/to/db
|
zoxide import /path/to/db
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Alternatively, for `autojump`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
zoxide import --from autojump /path/to/db
|
||||||
|
```
|
||||||
|
|
||||||
<!-- omit in toc -->
|
<!-- omit in toc -->
|
||||||
#### bash
|
#### bash
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::Cmd;
|
use super::Cmd;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::store::StoreBuilder;
|
use crate::db::DatabaseFile;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -40,10 +40,10 @@ impl Cmd for Add {
|
|||||||
let data_dir = config::zo_data_dir()?;
|
let data_dir = config::zo_data_dir()?;
|
||||||
let max_age = config::zo_maxage()?;
|
let max_age = config::zo_maxage()?;
|
||||||
|
|
||||||
let mut store = StoreBuilder::new(data_dir);
|
let mut db = DatabaseFile::new(data_dir);
|
||||||
let mut store = store.build()?;
|
let mut db = db.open()?;
|
||||||
store.add(path, now);
|
db.add(path, now);
|
||||||
store.age(max_age);
|
db.age(max_age);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@ use crate::config;
|
|||||||
use crate::import::{Autojump, Import as _, Z};
|
use crate::import::{Autojump, Import as _, Z};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
use crate::store::StoreBuilder;
|
use crate::db::DatabaseFile;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use clap::{ArgEnum, Clap};
|
use clap::{ArgEnum, Clap};
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Import entries from another database
|
/// Import entries from another application
|
||||||
#[derive(Clap, Debug)]
|
#[derive(Clap, Debug)]
|
||||||
pub struct Import {
|
pub struct Import {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@ -27,10 +27,10 @@ impl Cmd for Import {
|
|||||||
fn run(&self) -> Result<()> {
|
fn run(&self) -> Result<()> {
|
||||||
let data_dir = config::zo_data_dir()?;
|
let data_dir = config::zo_data_dir()?;
|
||||||
|
|
||||||
let mut store = StoreBuilder::new(data_dir);
|
let mut db = DatabaseFile::new(data_dir);
|
||||||
let mut store = store.build()?;
|
let mut db = db.open()?;
|
||||||
if !self.merge && !store.dirs.is_empty() {
|
if !self.merge && !db.dirs.is_empty() {
|
||||||
bail!("zoxide database is not empty, specify --merge to continue anyway")
|
bail!("current database is not empty, specify --merge to continue anyway")
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolve_symlinks = config::zo_resolve_symlinks();
|
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||||
@ -39,8 +39,8 @@ impl Cmd for Import {
|
|||||||
resolve_symlinks,
|
resolve_symlinks,
|
||||||
now: util::current_time()?,
|
now: util::current_time()?,
|
||||||
}
|
}
|
||||||
.import(&mut store, &self.path),
|
.import(&mut db, &self.path),
|
||||||
From::Z => Z { resolve_symlinks }.import(&mut store, &self.path),
|
From::Z => Z { resolve_symlinks }.import(&mut db, &self.path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use super::Cmd;
|
use super::Cmd;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
use crate::error::WriteErrorHandler;
|
||||||
use crate::shell::{self, Hook, Opts};
|
use crate::shell::{self, Hook, Opts};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@ -7,6 +8,8 @@ use askama::Template;
|
|||||||
use clap::{ArgEnum, Clap};
|
use clap::{ArgEnum, Clap};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
/// Generates shell configuration
|
/// Generates shell configuration
|
||||||
#[derive(Clap, Debug)]
|
#[derive(Clap, Debug)]
|
||||||
#[clap(after_help(env_help()))]
|
#[clap(after_help(env_help()))]
|
||||||
@ -54,9 +57,7 @@ impl Cmd for Init {
|
|||||||
Shell::Zsh => shell::Zsh(opts).render(),
|
Shell::Zsh => shell::Zsh(opts).render(),
|
||||||
}
|
}
|
||||||
.context("could not render template")?;
|
.context("could not render template")?;
|
||||||
println!("{}", source);
|
writeln!(io::stdout(), "{}", source).handle_err("stdout")
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +76,8 @@ fn env_help() -> &'static str {
|
|||||||
ENV_HELP.get_or_init(|| {
|
ENV_HELP.get_or_init(|| {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
const PATH_SPLIT_SEPARATOR: u8 = b':';
|
const PATH_SPLIT_SEPARATOR: u8 = b':';
|
||||||
#[cfg(any(target_os = "redox", target_os = "windows"))]
|
#[cfg(windows)]
|
||||||
const PATH_SPLIT_SEPARATOR: u8 = b'\\';
|
const PATH_SPLIT_SEPARATOR: u8 = b';';
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"\
|
"\
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use super::Cmd;
|
use super::Cmd;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
use crate::db::{self, DatabaseFile};
|
||||||
|
use crate::error::WriteErrorHandler;
|
||||||
use crate::fzf::Fzf;
|
use crate::fzf::Fzf;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
use crate::store::{self, StoreBuilder};
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
|
|
||||||
@ -30,19 +31,20 @@ pub struct Query {
|
|||||||
impl Cmd for Query {
|
impl Cmd for Query {
|
||||||
fn run(&self) -> Result<()> {
|
fn run(&self) -> Result<()> {
|
||||||
let data_dir = config::zo_data_dir()?;
|
let data_dir = config::zo_data_dir()?;
|
||||||
let mut store = StoreBuilder::new(data_dir);
|
let mut db = DatabaseFile::new(data_dir);
|
||||||
let mut store = store.build()?;
|
let mut db = db.open()?;
|
||||||
|
|
||||||
let query = store::Query::new(&self.keywords);
|
let query = db::Query::new(&self.keywords);
|
||||||
let now = util::current_time()?;
|
let now = util::current_time()?;
|
||||||
|
|
||||||
let mut matches = store.iter_matches(&query, now);
|
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||||
|
let mut matches = db.iter_matches(&query, now, resolve_symlinks);
|
||||||
|
|
||||||
if self.interactive {
|
if self.interactive {
|
||||||
let mut fzf = Fzf::new()?;
|
let mut fzf = Fzf::new()?;
|
||||||
let handle = fzf.stdin();
|
let handle = fzf.stdin();
|
||||||
for dir in matches {
|
for dir in matches {
|
||||||
writeln!(handle, "{}", dir.display_score(now)).context("could not write to fzf")?;
|
writeln!(handle, "{}", dir.display_score(now)).handle_err("fzf")?;
|
||||||
}
|
}
|
||||||
let selection = fzf.wait_select()?;
|
let selection = fzf.wait_select()?;
|
||||||
if self.score {
|
if self.score {
|
||||||
@ -62,15 +64,16 @@ impl Cmd for Query {
|
|||||||
} else {
|
} else {
|
||||||
writeln!(handle, "{}", dir.display())
|
writeln!(handle, "{}", dir.display())
|
||||||
}
|
}
|
||||||
.unwrap()
|
.handle_err("stdout")?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let dir = matches.next().context("no match found")?;
|
let dir = matches.next().context("no match found")?;
|
||||||
if self.score {
|
if self.score {
|
||||||
println!("{}", dir.display_score(now))
|
writeln!(io::stdout(), "{}", dir.display_score(now))
|
||||||
} else {
|
} else {
|
||||||
println!("{}", dir.display())
|
writeln!(io::stdout(), "{}", dir.display())
|
||||||
}
|
}
|
||||||
|
.handle_err("stdout")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use super::Cmd;
|
use super::Cmd;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
use crate::db::{DatabaseFile, Query};
|
||||||
|
use crate::error::WriteErrorHandler;
|
||||||
use crate::fzf::Fzf;
|
use crate::fzf::Fzf;
|
||||||
use crate::store::{Query, StoreBuilder};
|
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
@ -24,8 +25,8 @@ pub struct Remove {
|
|||||||
impl Cmd for Remove {
|
impl Cmd for Remove {
|
||||||
fn run(&self) -> Result<()> {
|
fn run(&self) -> Result<()> {
|
||||||
let data_dir = config::zo_data_dir()?;
|
let data_dir = config::zo_data_dir()?;
|
||||||
let mut store = StoreBuilder::new(data_dir);
|
let mut db = DatabaseFile::new(data_dir);
|
||||||
let mut store = store.build()?;
|
let mut db = db.open()?;
|
||||||
|
|
||||||
let selection;
|
let selection;
|
||||||
let path = match &self.interactive {
|
let path = match &self.interactive {
|
||||||
@ -35,9 +36,9 @@ impl Cmd for Remove {
|
|||||||
|
|
||||||
let mut fzf = Fzf::new()?;
|
let mut fzf = Fzf::new()?;
|
||||||
let handle = fzf.stdin();
|
let handle = fzf.stdin();
|
||||||
for dir in store.iter_matches(&query, now) {
|
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||||
writeln!(handle, "{}", dir.display_score(now))
|
for dir in db.iter_matches(&query, now, resolve_symlinks) {
|
||||||
.context("could not write to fzf")?;
|
writeln!(handle, "{}", dir.display_score(now)).handle_err("fzf")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
selection = fzf.wait_select()?;
|
selection = fzf.wait_select()?;
|
||||||
@ -48,11 +49,11 @@ impl Cmd for Remove {
|
|||||||
None => self.path.as_ref().unwrap(),
|
None => self.path.as_ref().unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !store.remove(path) {
|
if !db.remove(path) {
|
||||||
let path = util::resolve_path(&path)?;
|
let path = util::resolve_path(&path)?;
|
||||||
let path = util::path_to_str(&path)?;
|
let path = util::path_to_str(&path)?;
|
||||||
if !store.remove(path) {
|
if !db.remove(path) {
|
||||||
bail!("path not found in store: {}", &path)
|
bail!("path not found in database: {}", &path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::store::Rank;
|
use crate::db::Rank;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use dirs_next as dirs;
|
use dirs_next as dirs;
|
||||||
@ -15,7 +15,7 @@ pub fn zo_data_dir() -> Result<PathBuf> {
|
|||||||
data_dir.push("zoxide");
|
data_dir.push("zoxide");
|
||||||
data_dir
|
data_dir
|
||||||
}
|
}
|
||||||
None => bail!("could not find database directory, please set _ZO_DATA_DIR manually"),
|
None => bail!("could not find data directory, please set _ZO_DATA_DIR manually"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::fs;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct DirList<'a>(#[serde(borrow)] Vec<Dir<'a>>);
|
pub struct DirList<'a>(#[serde(borrow)] Vec<Dir<'a>>);
|
||||||
@ -20,9 +20,9 @@ impl DirList<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<DirList> {
|
pub fn from_bytes(bytes: &[u8]) -> Result<DirList> {
|
||||||
// Assume a maximum size for the store. This prevents bincode from throwing strange
|
// Assume a maximum size for the database. This prevents bincode from
|
||||||
// errors when it encounters invalid data.
|
// throwing strange errors when it encounters invalid data.
|
||||||
const MAX_SIZE: u64 = 8 << 20; // 8 MiB
|
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
|
||||||
let deserializer = &mut bincode::options()
|
let deserializer = &mut bincode::options()
|
||||||
.with_fixint_encoding()
|
.with_fixint_encoding()
|
||||||
.with_limit(MAX_SIZE);
|
.with_limit(MAX_SIZE);
|
||||||
@ -30,7 +30,7 @@ impl DirList<'_> {
|
|||||||
// Split bytes into sections.
|
// Split bytes into sections.
|
||||||
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
|
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
|
||||||
if bytes.len() < version_size {
|
if bytes.len() < version_size {
|
||||||
bail!("could not deserialize store: corrupted data");
|
bail!("could not deserialize database: corrupted data");
|
||||||
}
|
}
|
||||||
let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
|
let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ impl DirList<'_> {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
.context("could not deserialize store")
|
.context("could not deserialize database")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||||
@ -62,7 +62,7 @@ impl DirList<'_> {
|
|||||||
bincode::serialize_into(&mut buffer, &self)?;
|
bincode::serialize_into(&mut buffer, &self)?;
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
})()
|
})()
|
||||||
.context("could not serialize store")
|
.context("could not serialize database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,8 +95,14 @@ pub struct Dir<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Dir<'_> {
|
impl Dir<'_> {
|
||||||
pub fn is_match(&self, query: &Query) -> bool {
|
pub fn is_match(&self, query: &Query, resolve_symlinks: bool) -> bool {
|
||||||
query.matches(&self.path) && Path::new(self.path.as_ref()).is_dir()
|
let resolver = if resolve_symlinks {
|
||||||
|
fs::symlink_metadata
|
||||||
|
} else {
|
||||||
|
fs::metadata
|
||||||
|
};
|
||||||
|
let path = self.path.as_ref();
|
||||||
|
query.matches(path) && resolver(path).map(|m| m.is_dir()).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn score(&self, now: Epoch) -> Rank {
|
pub fn score(&self, now: Epoch) -> Rank {
|
@ -14,13 +14,13 @@ use std::fs;
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub struct Store<'a> {
|
pub struct Database<'a> {
|
||||||
pub dirs: DirList<'a>,
|
pub dirs: DirList<'a>,
|
||||||
pub modified: bool,
|
pub modified: bool,
|
||||||
data_dir: &'a Path,
|
data_dir: &'a Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Store<'a> {
|
impl<'a> Database<'a> {
|
||||||
pub fn save(&mut self) -> Result<()> {
|
pub fn save(&mut self) -> Result<()> {
|
||||||
if !self.modified {
|
if !self.modified {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -29,7 +29,7 @@ impl<'a> Store<'a> {
|
|||||||
let buffer = self.dirs.to_bytes()?;
|
let buffer = self.dirs.to_bytes()?;
|
||||||
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
|
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"could not create temporary store in: {}",
|
"could not create temporary database in: {}",
|
||||||
self.data_dir.display()
|
self.data_dir.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
@ -40,14 +40,14 @@ impl<'a> Store<'a> {
|
|||||||
let _ = file.as_file().set_len(buffer.len() as _);
|
let _ = file.as_file().set_len(buffer.len() as _);
|
||||||
file.write_all(&buffer).with_context(|| {
|
file.write_all(&buffer).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"could not write to temporary store: {}",
|
"could not write to temporary database: {}",
|
||||||
file.path().display()
|
file.path().display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let path = store_path(&self.data_dir);
|
let path = db_path(&self.data_dir);
|
||||||
persist(file, &path)
|
persist(file, &path)
|
||||||
.with_context(|| format!("could not replace store: {}", path.display()))?;
|
.with_context(|| format!("could not replace database: {}", path.display()))?;
|
||||||
|
|
||||||
self.modified = false;
|
self.modified = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -76,10 +76,13 @@ impl<'a> Store<'a> {
|
|||||||
&'b mut self,
|
&'b mut self,
|
||||||
query: &'b Query,
|
query: &'b Query,
|
||||||
now: Epoch,
|
now: Epoch,
|
||||||
|
resolve_symlinks: bool,
|
||||||
) -> impl DoubleEndedIterator<Item = &'b Dir> {
|
) -> impl DoubleEndedIterator<Item = &'b Dir> {
|
||||||
self.dirs
|
self.dirs
|
||||||
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
|
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
|
||||||
self.dirs.iter().filter(move |dir| dir.is_match(&query))
|
self.dirs
|
||||||
|
.iter()
|
||||||
|
.filter(move |dir| dir.is_match(&query, resolve_symlinks))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove<S: AsRef<str>>(&mut self, path: S) -> bool {
|
pub fn remove<S: AsRef<str>>(&mut self, path: S) -> bool {
|
||||||
@ -114,12 +117,12 @@ impl<'a> Store<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Store<'_> {
|
impl Drop for Database<'_> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Since the error can't be properly handled here,
|
// Since the error can't be properly handled here,
|
||||||
// pretty-print it instead.
|
// pretty-print it instead.
|
||||||
if let Err(e) = self.save() {
|
if let Err(e) = self.save() {
|
||||||
println!("Error: {}", e)
|
let _ = writeln!(io::stderr(), "zoxide: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,29 +162,31 @@ fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistEr
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StoreBuilder {
|
pub struct DatabaseFile {
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoreBuilder {
|
impl DatabaseFile {
|
||||||
pub fn new<P: Into<PathBuf>>(data_dir: P) -> StoreBuilder {
|
pub fn new<P: Into<PathBuf>>(data_dir: P) -> DatabaseFile {
|
||||||
StoreBuilder {
|
DatabaseFile {
|
||||||
data_dir: data_dir.into(),
|
data_dir: data_dir.into(),
|
||||||
buffer: Vec::new(),
|
buffer: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&mut self) -> Result<Store> {
|
pub fn open(&mut self) -> Result<Database> {
|
||||||
// Read the entire store to memory. For smaller files, this is faster
|
// Read the entire database to memory. For smaller files, this is
|
||||||
// than mmap / streaming, and allows for zero-copy deserialization.
|
// faster than mmap / streaming, and allows for zero-copy
|
||||||
let path = store_path(&self.data_dir);
|
// deserialization.
|
||||||
|
let path = db_path(&self.data_dir);
|
||||||
match fs::read(&path) {
|
match fs::read(&path) {
|
||||||
Ok(buffer) => {
|
Ok(buffer) => {
|
||||||
self.buffer = buffer;
|
self.buffer = buffer;
|
||||||
let dirs = DirList::from_bytes(&self.buffer)
|
let dirs = DirList::from_bytes(&self.buffer).with_context(|| {
|
||||||
.with_context(|| format!("could not deserialize store: {}", path.display()))?;
|
format!("could not deserialize database: {}", path.display())
|
||||||
Ok(Store {
|
})?;
|
||||||
|
Ok(Database {
|
||||||
dirs,
|
dirs,
|
||||||
modified: false,
|
modified: false,
|
||||||
data_dir: &self.data_dir,
|
data_dir: &self.data_dir,
|
||||||
@ -189,7 +194,7 @@ impl StoreBuilder {
|
|||||||
}
|
}
|
||||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||||
// Create data directory, but don't create any file yet.
|
// Create data directory, but don't create any file yet.
|
||||||
// The file will be created later by [`Store::save`]
|
// The file will be created later by [`Database::save`]
|
||||||
// if any data is modified.
|
// if any data is modified.
|
||||||
fs::create_dir_all(&self.data_dir).with_context(|| {
|
fs::create_dir_all(&self.data_dir).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@ -197,22 +202,22 @@ impl StoreBuilder {
|
|||||||
self.data_dir.display()
|
self.data_dir.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
Ok(Store {
|
Ok(Database {
|
||||||
dirs: DirList::new(),
|
dirs: DirList::new(),
|
||||||
modified: false,
|
modified: false,
|
||||||
data_dir: &self.data_dir,
|
data_dir: &self.data_dir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
Err(e).with_context(|| format!("could not read from store: {}", path.display()))
|
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_path<P: AsRef<Path>>(data_dir: P) -> PathBuf {
|
fn db_path<P: AsRef<Path>>(data_dir: P) -> PathBuf {
|
||||||
const STORE_FILENAME: &str = "db.zo";
|
const DB_FILENAME: &str = "db.zo";
|
||||||
data_dir.as_ref().join(STORE_FILENAME)
|
data_dir.as_ref().join(DB_FILENAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -230,17 +235,17 @@ mod tests {
|
|||||||
|
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
{
|
{
|
||||||
let mut store = StoreBuilder::new(data_dir.path());
|
let mut db = DatabaseFile::new(data_dir.path());
|
||||||
let mut store = store.build().unwrap();
|
let mut db = db.open().unwrap();
|
||||||
store.add(path, now);
|
db.add(path, now);
|
||||||
store.add(path, now);
|
db.add(path, now);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut store = StoreBuilder::new(data_dir.path());
|
let mut db = DatabaseFile::new(data_dir.path());
|
||||||
let store = store.build().unwrap();
|
let db = db.open().unwrap();
|
||||||
assert_eq!(store.dirs.len(), 1);
|
assert_eq!(db.dirs.len(), 1);
|
||||||
|
|
||||||
let dir = &store.dirs[0];
|
let dir = &db.dirs[0];
|
||||||
assert_eq!(dir.path, path);
|
assert_eq!(dir.path, path);
|
||||||
assert_eq!(dir.last_accessed, now);
|
assert_eq!(dir.last_accessed, now);
|
||||||
}
|
}
|
||||||
@ -257,20 +262,20 @@ mod tests {
|
|||||||
|
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
{
|
{
|
||||||
let mut store = StoreBuilder::new(data_dir.path());
|
let mut db = DatabaseFile::new(data_dir.path());
|
||||||
let mut store = store.build().unwrap();
|
let mut db = db.open().unwrap();
|
||||||
store.add(path, now);
|
db.add(path, now);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut store = StoreBuilder::new(data_dir.path());
|
let mut db = DatabaseFile::new(data_dir.path());
|
||||||
let mut store = store.build().unwrap();
|
let mut db = db.open().unwrap();
|
||||||
assert!(store.remove(path));
|
assert!(db.remove(path));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut store = StoreBuilder::new(data_dir.path());
|
let mut db = DatabaseFile::new(data_dir.path());
|
||||||
let mut store = store.build().unwrap();
|
let mut db = db.open().unwrap();
|
||||||
assert!(store.dirs.is_empty());
|
assert!(db.dirs.is_empty());
|
||||||
assert!(!store.remove(path));
|
assert!(!db.remove(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
19
src/error.rs
19
src/error.rs
@ -1,5 +1,9 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
// Custom error type for early exit.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SilentExit {
|
pub struct SilentExit {
|
||||||
pub code: i32,
|
pub code: i32,
|
||||||
@ -10,3 +14,16 @@ impl Display for SilentExit {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait WriteErrorHandler {
|
||||||
|
fn handle_err(self, device: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteErrorHandler for io::Result<()> {
|
||||||
|
fn handle_err(self, device: &str) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }),
|
||||||
|
result => result.with_context(|| format!("could not write to {}", device)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::Import;
|
use super::Import;
|
||||||
|
|
||||||
use crate::store::{Dir, Epoch, Store};
|
use crate::db::{Database, Dir, Epoch};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@ -13,7 +13,7 @@ pub struct Autojump {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Import for Autojump {
|
impl Import for Autojump {
|
||||||
fn import<P: AsRef<Path>>(&self, store: &mut Store, path: P) -> Result<()> {
|
fn import<P: AsRef<Path>>(&self, db: &mut Database, path: P) -> Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let buffer = fs::read_to_string(path)
|
let buffer = fs::read_to_string(path)
|
||||||
.with_context(|| format!("could not open autojump database: {}", path.display()))?;
|
.with_context(|| format!("could not open autojump database: {}", path.display()))?;
|
||||||
@ -45,13 +45,13 @@ impl Import for Autojump {
|
|||||||
|
|
||||||
let rank_sum = entries.iter().map(|(_, rank)| rank).sum::<f64>();
|
let rank_sum = entries.iter().map(|(_, rank)| rank).sum::<f64>();
|
||||||
for &(path, rank) in entries.iter() {
|
for &(path, rank) in entries.iter() {
|
||||||
if store.dirs.iter_mut().find(|dir| dir.path == path).is_none() {
|
if db.dirs.iter_mut().find(|dir| dir.path == path).is_none() {
|
||||||
store.dirs.push(Dir {
|
db.dirs.push(Dir {
|
||||||
path: Cow::Owned(path.into()),
|
path: Cow::Owned(path.into()),
|
||||||
rank: rank / rank_sum,
|
rank: rank / rank_sum,
|
||||||
last_accessed: self.now,
|
last_accessed: self.now,
|
||||||
});
|
});
|
||||||
store.modified = true;
|
db.modified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
mod autojump;
|
mod autojump;
|
||||||
mod z;
|
mod z;
|
||||||
|
|
||||||
use crate::store::Store;
|
use crate::db::Database;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@ -10,5 +10,5 @@ pub use autojump::Autojump;
|
|||||||
pub use z::Z;
|
pub use z::Z;
|
||||||
|
|
||||||
pub trait Import {
|
pub trait Import {
|
||||||
fn import<P: AsRef<Path>>(&self, store: &mut Store, path: P) -> Result<()>;
|
fn import<P: AsRef<Path>>(&self, db: &mut Database, path: P) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::Import;
|
use super::Import;
|
||||||
|
|
||||||
use crate::store::{Dir, Store};
|
use crate::db::{Database, Dir};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@ -13,7 +13,7 @@ pub struct Z {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Import for Z {
|
impl Import for Z {
|
||||||
fn import<P: AsRef<Path>>(&self, store: &mut Store, path: P) -> Result<()> {
|
fn import<P: AsRef<Path>>(&self, db: &mut Database, path: P) -> Result<()> {
|
||||||
let file = File::open(path).context("could not open z database")?;
|
let file = File::open(path).context("could not open z database")?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
@ -41,18 +41,18 @@ impl Import for Z {
|
|||||||
.parse()
|
.parse()
|
||||||
.with_context(|| format!("invalid epoch: {}", last_accessed))?;
|
.with_context(|| format!("invalid epoch: {}", last_accessed))?;
|
||||||
|
|
||||||
match store.dirs.iter_mut().find(|dir| dir.path == path) {
|
match db.dirs.iter_mut().find(|dir| dir.path == path) {
|
||||||
Some(dir) => {
|
Some(dir) => {
|
||||||
dir.rank += rank;
|
dir.rank += rank;
|
||||||
dir.last_accessed = dir.last_accessed.max(last_accessed);
|
dir.last_accessed = dir.last_accessed.max(last_accessed);
|
||||||
}
|
}
|
||||||
None => store.dirs.push(Dir {
|
None => db.dirs.push(Dir {
|
||||||
path: Cow::Owned(path.into()),
|
path: Cow::Owned(path.into()),
|
||||||
rank,
|
rank,
|
||||||
last_accessed,
|
last_accessed,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
store.modified = true;
|
db.modified = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})()
|
})()
|
||||||
|
20
src/main.rs
20
src/main.rs
@ -1,31 +1,33 @@
|
|||||||
mod cmd;
|
mod cmd;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod db;
|
||||||
mod error;
|
mod error;
|
||||||
mod fzf;
|
mod fzf;
|
||||||
mod import;
|
mod import;
|
||||||
mod shell;
|
mod shell;
|
||||||
mod store;
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::cmd::{App, Cmd};
|
use crate::cmd::{App, Cmd};
|
||||||
use crate::error::SilentExit;
|
use crate::error::SilentExit;
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::io::{self, Write};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
pub fn main() -> Result<()> {
|
pub fn main() {
|
||||||
// Forcibly disable backtraces.
|
// Forcibly disable backtraces.
|
||||||
env::remove_var("RUST_LIB_BACKTRACE");
|
env::remove_var("RUST_LIB_BACKTRACE");
|
||||||
env::remove_var("RUST_BACKTRACE");
|
env::remove_var("RUST_BACKTRACE");
|
||||||
|
|
||||||
App::parse()
|
if let Err(e) = App::parse().run() {
|
||||||
.run()
|
match e.downcast::<SilentExit>() {
|
||||||
.map_err(|e| match e.downcast::<SilentExit>() {
|
|
||||||
Ok(SilentExit { code }) => process::exit(code),
|
Ok(SilentExit { code }) => process::exit(code),
|
||||||
// TODO: change the error prefix to `zoxide:`
|
Err(e) => {
|
||||||
Err(e) => e,
|
let _ = writeln!(io::stderr(), "zoxide: {:?}", e);
|
||||||
})
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::store::Epoch;
|
use crate::db::Epoch;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user