Copy owner from previous database file (#364)

This commit is contained in:
Ajeet D'Souza 2022-03-09 13:43:52 +05:30
parent 90e781a192
commit 17365710af
13 changed files with 236 additions and 205 deletions

View File

@ -15,11 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Bash/Zsh: rename `_z` completion function to avoid conflicts with other shell plugins.
- Bash/Zsh: rename `_z` completion function to avoid conflicts with other shell
plugins.
- Elvish: upgrade to new lambda syntax.
- Fzf: added `--keep-right` option by default, upgraded minimum version to v0.21.0.
- Fzf: added `--keep-right` option by default, upgraded minimum version to
v0.21.0.
- Bash: only enable completions on 4.4+.
- Fzf: bypass `ls` alias in preview window.
- Retain ownership of database file.
## [0.8.0] - 2021-12-25
@ -30,7 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Fzf: better default options.
- Fish: interactive completions are only triggered when the last argument is empty.
- Fish: interactive completions are only triggered when the last argument is
empty.
- PowerShell: installation instructions.
### Fixed
@ -131,7 +135,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Auto-generated shell completions.
- `zoxide query --all` for listing deleted directories.
- Lazy deletion for removed directories that have not been accessed in > 90 days.
- Lazy deletion for removed directories that have not been accessed in > 90
days.
- Nushell: support for 0.32.0+.
### Fixed
@ -158,7 +163,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- `cd -` on fish shells.
- `__zoxide_hook` no longer changes value of `$?` within `$PROMPT_COMMAND` on bash.
- `__zoxide_hook` no longer changes value of `$?` within `$PROMPT_COMMAND` on
bash.
### Removed
@ -195,7 +201,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `$_ZO_EXCLUDE_DIRS` now supports globs.
- `zoxide init` now defines `__zoxide_z*` functions that can be aliased as needed.
- `zoxide init` now defines `__zoxide_z*` functions that can be aliased as
needed.
- Support for the [xonsh](https://xon.sh/) shell.
- `zoxide import` can now import from Autojump.

82
Cargo.lock generated
View File

@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.55"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
[[package]]
name = "askama"
@ -117,6 +117,12 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -125,9 +131,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.1.5"
version = "3.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123"
dependencies = [
"atty",
"bitflags",
@ -365,6 +371,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
@ -387,6 +402,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nix"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "nom"
version = "7.1.0"
@ -500,24 +528,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.11"
@ -539,9 +549,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.5.4"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
dependencies = [
"aho-corasick",
"memchr",
@ -695,26 +705,6 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.4"
@ -825,12 +815,12 @@ dependencies = [
"clap_complete_fig",
"dirs",
"dunce",
"fastrand",
"glob",
"nix",
"ordered-float",
"rand",
"rstest",
"rstest_reuse",
"serde",
"tempfile",
"thiserror",
]

View File

@ -23,17 +23,13 @@ bincode = "1.3.1"
clap = { version = "3.1.0", features = ["derive"] }
dirs = "4.0.0"
dunce = "1.0.1"
fastrand = "1.7.0"
glob = "0.3.0"
ordered-float = "2.0.0"
serde = { version = "1.0.116", features = ["derive"] }
tempfile = "3.1.0"
thiserror = "1.0.30"
[target.'cfg(windows)'.dependencies]
rand = { version = "0.8.4", features = [
"getrandom",
"small_rng",
], default-features = false }
[target.'cfg(unix)'.dependencies]
nix = "0.23.1"
[build-dependencies]
clap = { version = "3.1.0", features = ["derive"] }
@ -44,10 +40,11 @@ clap_complete_fig = "3.1.0"
assert_cmd = "2.0.0"
rstest = "0.12.0"
rstest_reuse = "0.3.0"
tempfile = "3.1.0"
[features]
default = []
nix = []
nix-dev = []
[profile.release]
codegen-units = 1

View File

@ -3,10 +3,10 @@ use std::io::{self, Write};
use anyhow::{Context, Result};
use crate::cmd::{Query, Run};
use crate::config;
use crate::db::{Database, DatabaseFile};
use crate::error::BrokenPipeHandler;
use crate::fzf::Fzf;
use crate::{config, util};
use crate::util::{self, Fzf};
impl Run for Query {
fn run(&self) -> Result<()> {

View File

@ -3,9 +3,9 @@ use std::io::{self, Write};
use anyhow::{bail, Context, Result};
use crate::cmd::{Remove, Run};
use crate::config;
use crate::db::DatabaseFile;
use crate::fzf::Fzf;
use crate::{config, util};
use crate::util::{self, Fzf};
impl Run for Remove {
fn run(&self) -> Result<()> {

View File

@ -2,13 +2,14 @@ mod dir;
mod stream;
use std::fs;
use std::io::{self, Write};
use std::io;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub use dir::{Dir, DirList, Epoch, Rank};
pub use stream::Stream;
use tempfile::{NamedTempFile, PersistError};
use crate::util;
#[derive(Debug)]
pub struct Database<'file> {
@ -24,18 +25,8 @@ impl<'file> Database<'file> {
}
let buffer = self.dirs.to_bytes()?;
let mut file = NamedTempFile::new_in(self.data_dir)
.with_context(|| format!("could not create temporary database in: {}", self.data_dir.display()))?;
// 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 _);
file.write_all(&buffer)
.with_context(|| format!("could not write to temporary database: {}", file.path().display()))?;
let path = db_path(&self.data_dir);
persist(file, &path).with_context(|| format!("could not replace database: {}", path.display()))?;
util::write(&path, &buffer).context("could not write to database")?;
self.modified = false;
Ok(())
}
@ -120,44 +111,6 @@ impl<'file> Database<'file> {
}
}
#[cfg(unix)]
fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistError> {
file.persist(path)?;
Ok(())
}
#[cfg(windows)]
fn persist<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> {
use std::thread;
use std::time::Duration;
use rand::distributions::{Distribution, Uniform};
use rand::rngs::SmallRng;
use rand::SeedableRng;
// 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(SmallRng::from_entropy);
let between = Uniform::from(50..150);
let duration = Duration::from_millis(between.sample(&mut rng));
thread::sleep(duration);
file = e.file;
}
Err(e) => return Err(e),
}
}
Ok(())
}
pub struct DatabaseFile {
buffer: Vec<u8>,
data_dir: PathBuf,

View File

@ -1,19 +1,20 @@
use std::fmt::{self, Display, Formatter};
use std::io;
use anyhow::{bail, Context, Result};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("could not find fzf, is it installed?")]
pub struct FzfNotFound;
/// Custom error type for early exit.
#[derive(Debug, Error)]
#[error("")]
#[derive(Debug)]
pub struct SilentExit {
pub code: i32,
}
impl Display for SilentExit {
fn fmt(&self, _: &mut Formatter<'_>) -> fmt::Result {
Ok(())
}
}
pub trait BrokenPipeHandler {
fn pipe_exit(self, device: &str) -> Result<()>;
}

View File

@ -1,77 +0,0 @@
use std::io::{self, Read};
use std::mem;
use std::process::{Child, ChildStdin, Stdio};
use anyhow::{bail, Context, Result};
use crate::error::{FzfNotFound, SilentExit};
use crate::{config, util};
pub struct Fzf {
child: Child,
}
impl Fzf {
pub fn new(multiple: bool) -> Result<Self> {
let bin = if cfg!(windows) { "fzf.exe" } else { "fzf" };
let mut command = util::get_command(bin).map_err(|_| FzfNotFound)?;
if multiple {
command.arg("-m");
}
command.arg("-n2..").stdin(Stdio::piped()).stdout(Stdio::piped());
if let Some(fzf_opts) = config::fzf_opts() {
command.env("FZF_DEFAULT_OPTS", fzf_opts);
} else {
command.args(&[
// Search result
"--no-sort",
// Interface
"--keep-right",
// Layout
"--height=40%",
"--info=inline",
"--layout=reverse",
// Scripting
"--exit-0",
"--select-1",
// Key/Event bindings
"--bind=ctrl-z:ignore",
]);
if cfg!(unix) {
command.env("SHELL", "sh");
command.arg(r"--preview=\command -p ls -p {2..}");
}
}
let child = match command.spawn() {
Ok(child) => child,
Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(FzfNotFound),
Err(e) => Err(e).context("could not launch fzf")?,
};
Ok(Fzf { child })
}
pub fn stdin(&mut self) -> &mut ChildStdin {
self.child.stdin.as_mut().unwrap()
}
pub fn select(mut self) -> Result<String> {
// Drop stdin to prevent deadlock.
mem::drop(self.child.stdin.take());
let mut stdout = self.child.stdout.take().unwrap();
let mut output = String::new();
stdout.read_to_string(&mut output).context("failed to read from fzf")?;
let status = self.child.wait().context("wait failed on fzf")?;
match status.code() {
Some(0) => Ok(output),
Some(1) => bail!("no match found"),
Some(2) => bail!("fzf returned an error"),
Some(code @ 130) => bail!(SilentExit { code }),
Some(128..=254) | None => bail!("fzf was terminated"),
_ => bail!("fzf returned an unknown error"),
}
}
}

View File

@ -8,7 +8,6 @@ mod cmd;
mod config;
mod db;
mod error;
mod fzf;
mod shell;
mod util;

View File

@ -32,7 +32,7 @@ make_template!(Powershell, "powershell.txt");
make_template!(Xonsh, "xonsh.txt");
make_template!(Zsh, "zsh.txt");
#[cfg(feature = "nix")]
#[cfg(feature = "nix-dev")]
#[cfg(test)]
mod tests {
use askama::Template;

View File

@ -1,11 +1,172 @@
use std::env;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::mem;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::process::{Child, ChildStdin, Stdio};
use std::time::SystemTime;
use anyhow::{bail, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use crate::config;
use crate::db::Epoch;
use crate::error::SilentExit;
pub struct Fzf {
child: Child,
}
impl Fzf {
const ERR_NOT_FOUND: &'static str = "could not find fzf, is it installed?";
pub fn new(multiple: bool) -> Result<Self> {
let bin = if cfg!(windows) { "fzf.exe" } else { "fzf" };
let mut command = get_command(bin).map_err(|_| anyhow!(Self::ERR_NOT_FOUND))?;
if multiple {
command.arg("-m");
}
command.arg("-n2..").stdin(Stdio::piped()).stdout(Stdio::piped());
if let Some(fzf_opts) = config::fzf_opts() {
command.env("FZF_DEFAULT_OPTS", fzf_opts);
} else {
command.args(&[
// Search result
"--no-sort",
// Interface
"--keep-right",
// Layout
"--height=40%",
"--info=inline",
"--layout=reverse",
// Scripting
"--exit-0",
"--select-1",
// Key/Event bindings
"--bind=ctrl-z:ignore",
]);
if cfg!(unix) {
command.env("SHELL", "sh");
command.arg(r"--preview=\command -p ls -p {2..}");
}
}
let child = match command.spawn() {
Ok(child) => child,
Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_NOT_FOUND),
Err(e) => Err(e).context("could not launch fzf")?,
};
Ok(Fzf { child })
}
pub fn stdin(&mut self) -> &mut ChildStdin {
self.child.stdin.as_mut().unwrap()
}
pub fn select(mut self) -> Result<String> {
// Drop stdin to prevent deadlock.
mem::drop(self.child.stdin.take());
let mut stdout = self.child.stdout.take().unwrap();
let mut output = String::new();
stdout.read_to_string(&mut output).context("failed to read from fzf")?;
let status = self.child.wait().context("wait failed on fzf")?;
match status.code() {
Some(0) => Ok(output),
Some(1) => bail!("no match found"),
Some(2) => bail!("fzf returned an error"),
Some(code @ 130) => bail!(SilentExit { code }),
Some(128..=254) | None => bail!("fzf was terminated"),
_ => bail!("fzf returned an unknown error"),
}
}
}
/// Similar to [`fs::write`], but atomic (best effort on Windows).
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
let path = path.as_ref();
let contents = contents.as_ref();
let dir = path.parent().unwrap();
// Create a tmpfile.
let (mut tmp_file, tmp_path) = tmpfile(dir)?;
let result = (|| {
// Write to the tmpfile.
let _ = tmp_file.set_len(contents.len() as u64);
tmp_file.write_all(contents).with_context(|| format!("could not write to file: {}", tmp_path.display()))?;
// Set the owner of the tmpfile (UNIX only).
#[cfg(unix)]
if let Ok(metadata) = path.metadata() {
use nix::unistd::{self, Gid, Uid};
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
let uid = Uid::from_raw(metadata.uid());
let gid = Gid::from_raw(metadata.gid());
let _ = unistd::fchown(tmp_file.as_raw_fd(), Some(uid), Some(gid));
}
// Close and rename the tmpfile.
mem::drop(tmp_file);
rename(&tmp_path, path)
})();
// In case of an error, delete the tmpfile.
if result.is_err() {
let _ = fs::remove_file(&tmp_path);
}
result
}
/// Atomically create a tmpfile in the given directory.
fn tmpfile<P: AsRef<Path>>(dir: P) -> Result<(File, PathBuf)> {
const MAX_ATTEMPTS: usize = 5;
const TMP_NAME_LEN: usize = 16;
let dir = dir.as_ref();
let mut attempts = 0;
loop {
attempts += 1;
// Generate a random name for the tmpfile.
let mut name = String::with_capacity(TMP_NAME_LEN);
name.push_str("tmp_");
while name.len() < TMP_NAME_LEN {
name.push(fastrand::alphanumeric());
}
let path = dir.join(name);
// Atomically create the tmpfile.
match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => break Ok((file, path)),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => (),
Err(e) => break Err(e).with_context(|| format!("could not create file: {}", path.display())),
}
}
}
/// Similar to [`fs::rename`], but retries on Windows.
fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
const MAX_ATTEMPTS: usize = 5;
let from = from.as_ref();
let to = to.as_ref();
if cfg!(windows) {
let mut attempts = 0;
loop {
attempts += 1;
match fs::rename(from, to) {
Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => (),
result => break result,
}
}
} else {
fs::rename(from, to)
}
.with_context(|| format!("could not rename file: {} -> {}", from.display(), to.display()))
}
pub fn canonicalize<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
dunce::canonicalize(path).with_context(|| format!("could not resolve path: {}", path.as_ref().display()))

View File

@ -84,8 +84,8 @@ function __zoxide_z_complete
# If the last argument is empty, use interactive selection.
set -l query $tokens[2..-1]
set -l result (zoxide query -i -- $query)
and commandline -p "$tokens[1] "(string escape $result)
commandline -f repaint
commandline --current-process "$tokens[1] "(string escape $result)
commandline --function repaint
end
end

View File

@ -1,5 +1,5 @@
//! Test clap generated completions.
#![cfg(feature = "nix")]
#![cfg(feature = "nix-dev")]
use assert_cmd::Command;