Migrate from `z` database

The new `migrate` function takes in a path to the old `z` database and
naively parses it to add to the database. It sets the epoch to be the
current time in order to prevent scaling down old directories from the
`z` database -- at least until `zoxide` is up and running.

The program will fail if the user already has a database, so as to
prevent tainting it in any way. It will also fail if the database is not
in the expected format -- e.g. if it doesn't follow the
`/path/to/somewhere|1.0101|10101010101` schema. The way this is
validated is by looking for 2 pipe characters and erroring if there is
only 1 (`first_pipe_idx == last_pipe_idx`). It will ignore any dead
paths if the user has not cleaned their database recently.
This commit is contained in:
Cole Helbling 2020-03-11 22:37:34 -07:00
parent 65b37082b6
commit ee8f3427d1
No known key found for this signature in database
GPG Key ID: B37E0F2371016A4C
2 changed files with 66 additions and 3 deletions

View File

@ -1,10 +1,10 @@
use crate::dir::Dir;
use crate::types::{Rank, Timestamp};
use crate::util::get_zo_maxage;
use crate::util;
use anyhow::{anyhow, Context, Result};
use fs2::FileExt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufReader, BufWriter};
use std::io::{self, BufRead, BufReader, BufWriter};
use std::path::{Path, PathBuf};
pub struct DB {
@ -71,6 +71,59 @@ impl DB {
Ok(())
}
pub fn migrate<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
if !self.dirs.is_empty() {
return Err(anyhow!(
"To prevent conflicts, you can only migrate from z with an empty \
zoxide database!"
));
}
let zdata = File::open(path)?;
let reader = BufReader::new(zdata);
for line in reader.lines().filter_map(|l| l.ok()) {
let first_pipe_idx = line
.find('|')
.with_context(|| anyhow!("missing separator"))?;
let last_pipe_idx = line
.rfind('|')
.with_context(|| anyhow!("missing separator"))?;
if first_pipe_idx == last_pipe_idx {
return Err(anyhow!("invalid data file format -- only 1 separator"));
}
let path = PathBuf::from(&line[..first_pipe_idx]);
let rank = line[first_pipe_idx + 1..last_pipe_idx]
.parse::<f64>()
.with_context(|| anyhow!("could not parse rank"))?;
// otherwise, the rank will get scaled down, depending on how old
// the entry is
let epoch = util::get_current_time()?;
let path_abs = match path.canonicalize() {
Ok(path) => path,
Err(_) => continue, // ignore dead paths
};
let path_str = path_abs
.to_str()
.ok_or_else(|| anyhow!("invalid unicode in path: {}", path_abs.display()))?;
self.dirs.push(Dir {
path: path_str.to_owned(),
last_accessed: epoch,
rank,
});
}
self.modified = true;
Ok(())
}
pub fn add<P: AsRef<Path>>(&mut self, path: P, now: Timestamp) -> Result<()> {
let path_abs = path
.as_ref()
@ -93,7 +146,7 @@ impl DB {
}
};
let max_age = get_zo_maxage()?;
let max_age = util::get_zo_maxage()?;
let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::<Rank>();
if sum_age > max_age {

View File

@ -28,6 +28,9 @@ enum Zoxide {
#[structopt(about = "Add a new directory or increment its rank")]
Add { path: Option<String> },
#[structopt(about = "Migrate from z database")]
Migrate { path: String },
#[structopt(about = "Prints shell configuration")]
Init {
#[structopt(possible_values = &Shell::variants(), case_insensitive = true)]
@ -94,6 +97,12 @@ pub fn main() -> Result<()> {
}
}?;
}
Zoxide::Migrate { path } => {
let mut db = get_db()?;
db.migrate(path)?;
db.save()?;
}
Zoxide::Init {
shell,
no_define_aliases,
@ -208,6 +217,7 @@ end
const INIT_FISH_ALIAS: &str = r#"
abbr -a zi 'z -i'
abbr -a za 'zoxide add'
abbr -a zq 'zoxide query'
abbr -a zr 'zoxide remove'