Setup apply to sync between desktop and homesync repo.
Also remove unused commands.pull/3/head
parent
61b9a338a5
commit
bf65142e61
|
@ -14,7 +14,6 @@ clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
|||
git2 = "0.13.25"
|
||||
log = "0.4.14"
|
||||
notify = "4.0.16"
|
||||
regex = "1.5.4"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0.132"
|
||||
serde_yaml = "0.8"
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
rustc
|
||||
rustfmt
|
||||
] ++ lib.optionals stdenv.isDarwin (
|
||||
with darwin.apple_sdk.frameworks; [ CoreServices ]);
|
||||
with darwin.apple_sdk.frameworks; [ CoreServices ]
|
||||
) ++ lib.optionals stdenv.isLinux [
|
||||
pkgs.openssl
|
||||
pkgs.zlib
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -96,17 +96,23 @@ impl Config {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PathConfig(pub ResPathBuf, pub Config);
|
||||
pub struct PathConfig {
|
||||
pub homesync_yml: ResPathBuf,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl PathConfig {
|
||||
pub fn new(path: &ResPathBuf, config: Config) -> Self {
|
||||
PathConfig(path.clone(), config)
|
||||
PathConfig {
|
||||
homesync_yml: path.clone(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jrpotter): Create backup file before overwriting.
|
||||
pub fn write(&self) -> Result<()> {
|
||||
let mut file = fs::File::create(&self.0)?;
|
||||
let serialized = serde_yaml::to_string(&self.1)?;
|
||||
let mut file = fs::File::create(&self.homesync_yml)?;
|
||||
let serialized = serde_yaml::to_string(&self.config)?;
|
||||
file.write_all(serialized.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -143,12 +149,12 @@ pub fn load(candidates: &Vec<ResPathBuf>) -> Result<PathConfig> {
|
|||
Err(Error::MissingConfig)
|
||||
}
|
||||
|
||||
pub fn reload(config: &PathConfig) -> Result<PathConfig> {
|
||||
pub fn reload(pc: &PathConfig) -> Result<PathConfig> {
|
||||
info!(
|
||||
"<green>{}</> configuration reloaded.",
|
||||
config.1.local.display()
|
||||
pc.config.local.display()
|
||||
);
|
||||
load(&vec![config.0.clone()])
|
||||
load(&vec![pc.homesync_yml.clone()])
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
@ -205,14 +211,14 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
|
|||
Some(c) => Some(&c.remote),
|
||||
None => None,
|
||||
})?;
|
||||
let generated = PathConfig(
|
||||
path.clone(),
|
||||
Config {
|
||||
let generated = PathConfig {
|
||||
homesync_yml: path.clone(),
|
||||
config: Config {
|
||||
local,
|
||||
remote,
|
||||
packages: loaded.map_or(BTreeMap::new(), |c| c.packages),
|
||||
},
|
||||
);
|
||||
};
|
||||
generated.write()?;
|
||||
Ok(generated)
|
||||
}
|
||||
|
@ -221,13 +227,16 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
|
|||
// Listing
|
||||
// ========================================
|
||||
|
||||
pub fn list_packages(config: PathConfig) {
|
||||
pub fn list_packages(pc: PathConfig) {
|
||||
println!(
|
||||
"Listing packages in {}...\n",
|
||||
colorize_string(format!("<green>{}</>", config.0.unresolved().display())),
|
||||
colorize_string(format!(
|
||||
"<green>{}</>",
|
||||
pc.homesync_yml.unresolved().display()
|
||||
)),
|
||||
);
|
||||
// Alphabetical ordered ensured by B-tree implementation.
|
||||
for (k, _) in config.1.packages {
|
||||
for (k, _) in pc.config.packages {
|
||||
println!("• {}", k);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ impl<'a> WatchState<'a> {
|
|||
|
||||
/// Reads in the new path config, updating all watched and pending files
|
||||
/// according to the packages in the specified config.
|
||||
pub fn update(&mut self, config: &PathConfig) {
|
||||
pub fn update(&mut self, pc: &PathConfig) {
|
||||
self.send_poll(PollEvent::Clear);
|
||||
for path in &self.watching {
|
||||
match self.watcher.unwatch(&path) {
|
||||
|
@ -122,7 +122,7 @@ impl<'a> WatchState<'a> {
|
|||
}
|
||||
}
|
||||
self.watching.clear();
|
||||
for (_, package) in &config.1.packages {
|
||||
for (_, package) in &pc.config.packages {
|
||||
for path in &package.configs {
|
||||
match path::soft_resolve(&path) {
|
||||
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
|
||||
|
@ -138,7 +138,7 @@ impl<'a> WatchState<'a> {
|
|||
// Daemon
|
||||
// ========================================
|
||||
|
||||
pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
||||
pub fn launch(mut pc: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
||||
let (poll_tx, poll_rx) = channel();
|
||||
let (watch_tx, watch_rx) = channel();
|
||||
let watch_tx1 = watch_tx.clone();
|
||||
|
@ -152,9 +152,9 @@ pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
|
|||
// changes to it for hot reloading purposes, and not worry that our wrapper
|
||||
// will ever clear it from its watch state.
|
||||
let mut watcher: RecommendedWatcher = Watcher::new(watch_tx1, Duration::from_secs(freq_secs))?;
|
||||
watcher.watch(&config.0, RecursiveMode::NonRecursive)?;
|
||||
watcher.watch(&pc.homesync_yml, RecursiveMode::NonRecursive)?;
|
||||
let mut state = WatchState::new(poll_tx, &mut watcher)?;
|
||||
state.update(&config);
|
||||
state.update(&pc);
|
||||
loop {
|
||||
// Received paths should always be the fully resolved ones so safe to
|
||||
// compare against our current config path.
|
||||
|
@ -166,16 +166,16 @@ pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
|
|||
trace!("NoticeRemove {}", p.display());
|
||||
}
|
||||
Ok(DebouncedEvent::Create(p)) => {
|
||||
if config.0 == p {
|
||||
config = config::reload(&config)?;
|
||||
state.update(&config);
|
||||
if pc.homesync_yml == p {
|
||||
pc = config::reload(&pc)?;
|
||||
state.update(&pc);
|
||||
}
|
||||
trace!("Create {}", p.display());
|
||||
}
|
||||
Ok(DebouncedEvent::Write(p)) => {
|
||||
if config.0 == p {
|
||||
config = config::reload(&config)?;
|
||||
state.update(&config);
|
||||
if pc.homesync_yml == p {
|
||||
pc = config::reload(&pc)?;
|
||||
state.update(&pc);
|
||||
}
|
||||
trace!("Write {}", p.display());
|
||||
}
|
||||
|
|
160
src/git.rs
160
src/git.rs
|
@ -1,7 +1,43 @@
|
|||
use super::{config::PathConfig, path};
|
||||
use git2::Repository;
|
||||
use path::ResPathBuf;
|
||||
use simplelog::{info, paris};
|
||||
use std::{env::VarError, error, fmt, fs, io, path::PathBuf, result};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env::VarError,
|
||||
error, fmt, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
result,
|
||||
};
|
||||
|
||||
// All git error codes.
|
||||
// TODO(jrpotter): Remove these once done needing to reference them.
|
||||
// git2::ErrorCode::GenericError => panic!("generic"),
|
||||
// git2::ErrorCode::NotFound => panic!("not_found"),
|
||||
// git2::ErrorCode::Exists => panic!("exists"),
|
||||
// git2::ErrorCode::Ambiguous => panic!("ambiguous"),
|
||||
// git2::ErrorCode::BufSize => panic!("buf_size"),
|
||||
// git2::ErrorCode::User => panic!("user"),
|
||||
// git2::ErrorCode::BareRepo => panic!("bare_repo"),
|
||||
// git2::ErrorCode::UnbornBranch => panic!("unborn_branch"),
|
||||
// git2::ErrorCode::Unmerged => panic!("unmerged"),
|
||||
// git2::ErrorCode::NotFastForward => panic!("not_fast_forward"),
|
||||
// git2::ErrorCode::InvalidSpec => panic!("invalid_spec"),
|
||||
// git2::ErrorCode::Conflict => panic!("conflict"),
|
||||
// git2::ErrorCode::Locked => panic!("locked"),
|
||||
// git2::ErrorCode::Modified => panic!("modified"),
|
||||
// git2::ErrorCode::Auth => panic!("auth"),
|
||||
// git2::ErrorCode::Certificate => panic!("certificate"),
|
||||
// git2::ErrorCode::Applied => panic!("applied"),
|
||||
// git2::ErrorCode::Peel => panic!("peel"),
|
||||
// git2::ErrorCode::Eof => panic!("eof"),
|
||||
// git2::ErrorCode::Invalid => panic!("invalid"),
|
||||
// git2::ErrorCode::Uncommitted => panic!("uncommitted"),
|
||||
// git2::ErrorCode::Directory => panic!("directory"),
|
||||
// git2::ErrorCode::MergeConflict => panic!("merge_conflict"),
|
||||
// git2::ErrorCode::HashsumMismatch => panic!("hashsum_mismatch"),
|
||||
// git2::ErrorCode::IndexDirty => panic!("index_dirty"),
|
||||
// git2::ErrorCode::ApplyFail => panic!("apply_fail"),
|
||||
|
||||
// ========================================
|
||||
// Error
|
||||
|
@ -14,6 +50,7 @@ pub enum Error {
|
|||
GitError(git2::Error),
|
||||
IOError(io::Error),
|
||||
NotHomesyncRepo,
|
||||
NotWorkingRepo,
|
||||
VarError(VarError),
|
||||
}
|
||||
|
||||
|
@ -53,6 +90,10 @@ impl fmt::Display for Error {
|
|||
f,
|
||||
"Local repository is not managed by `homesync`. Missing `.homesync` sentinel file."
|
||||
),
|
||||
Error::NotWorkingRepo => write!(
|
||||
f,
|
||||
"Local repository should be a working directory. Did you manually initialize with `--bare`?"
|
||||
),
|
||||
Error::VarError(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
|
@ -64,53 +105,17 @@ impl error::Error for Error {}
|
|||
// Initialization
|
||||
// ========================================
|
||||
|
||||
// All git error codes.
|
||||
// TODO(jrpotter): Remove these once done needing to reference them.
|
||||
// git2::ErrorCode::GenericError => panic!("generic"),
|
||||
// git2::ErrorCode::NotFound => panic!("not_found"),
|
||||
// git2::ErrorCode::Exists => panic!("exists"),
|
||||
// git2::ErrorCode::Ambiguous => panic!("ambiguous"),
|
||||
// git2::ErrorCode::BufSize => panic!("buf_size"),
|
||||
// git2::ErrorCode::User => panic!("user"),
|
||||
// git2::ErrorCode::BareRepo => panic!("bare_repo"),
|
||||
// git2::ErrorCode::UnbornBranch => panic!("unborn_branch"),
|
||||
// git2::ErrorCode::Unmerged => panic!("unmerged"),
|
||||
// git2::ErrorCode::NotFastForward => panic!("not_fast_forward"),
|
||||
// git2::ErrorCode::InvalidSpec => panic!("invalid_spec"),
|
||||
// git2::ErrorCode::Conflict => panic!("conflict"),
|
||||
// git2::ErrorCode::Locked => panic!("locked"),
|
||||
// git2::ErrorCode::Modified => panic!("modified"),
|
||||
// git2::ErrorCode::Auth => panic!("auth"),
|
||||
// git2::ErrorCode::Certificate => panic!("certificate"),
|
||||
// git2::ErrorCode::Applied => panic!("applied"),
|
||||
// git2::ErrorCode::Peel => panic!("peel"),
|
||||
// git2::ErrorCode::Eof => panic!("eof"),
|
||||
// git2::ErrorCode::Invalid => panic!("invalid"),
|
||||
// git2::ErrorCode::Uncommitted => panic!("uncommitted"),
|
||||
// git2::ErrorCode::Directory => panic!("directory"),
|
||||
// git2::ErrorCode::MergeConflict => panic!("merge_conflict"),
|
||||
// git2::ErrorCode::HashsumMismatch => panic!("hashsum_mismatch"),
|
||||
// git2::ErrorCode::IndexDirty => panic!("index_dirty"),
|
||||
// git2::ErrorCode::ApplyFail => panic!("apply_fail"),
|
||||
|
||||
/// Sets up a local github repository all configuration files will be synced to.
|
||||
/// If there does not exist a local repository at the requested location, we
|
||||
/// attempt to make it.
|
||||
///
|
||||
/// NOTE! This does not perform any syncing between local and remote. In fact,
|
||||
/// this method does not perform any validation on the remote.
|
||||
pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
||||
/// this method does not perform any validation on remote at all.
|
||||
pub fn init(pc: &PathConfig) -> Result<Repository> {
|
||||
// Permit the use of environment variables within the local configuration
|
||||
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
|
||||
// environment variable is not defined.
|
||||
let expanded = match config.1.local.to_str() {
|
||||
Some(s) => s,
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Could not local path to a UTF-8 encoded string.",
|
||||
))?,
|
||||
};
|
||||
let expanded = path::expand_env(expanded)?;
|
||||
let expanded = path::expand(&pc.config.local)?;
|
||||
// Attempt to open the local path as a git repository if possible. The
|
||||
// `NotFound` error is thrown if:
|
||||
//
|
||||
|
@ -146,7 +151,7 @@ pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
|||
None => {
|
||||
info!(
|
||||
"Creating new homesync repository at <green>{}</>.",
|
||||
config.1.local.display()
|
||||
pc.config.local.display()
|
||||
);
|
||||
let repo = Repository::init(&expanded)?;
|
||||
fs::File::create(sentinel)?;
|
||||
|
@ -154,3 +159,76 @@ pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Application
|
||||
// ========================================
|
||||
|
||||
fn find_repo_files(path: &Path) -> Result<Vec<ResPathBuf>> {
|
||||
let mut seen = Vec::new();
|
||||
if path.is_dir() {
|
||||
for entry in fs::read_dir(path)? {
|
||||
let nested = entry?.path();
|
||||
if nested.is_dir() {
|
||||
if nested.ends_with(".git") {
|
||||
continue;
|
||||
}
|
||||
let nested = find_repo_files(&nested)?;
|
||||
seen.extend_from_slice(&nested);
|
||||
} else if !nested.ends_with(".homesync") {
|
||||
seen.push(ResPathBuf::new(&nested)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(seen)
|
||||
}
|
||||
|
||||
fn find_package_files(pc: &PathConfig) -> Vec<ResPathBuf> {
|
||||
let mut seen = Vec::new();
|
||||
for (_, package) in &pc.config.packages {
|
||||
for path in &package.configs {
|
||||
if let Ok(resolved) = path::resolve(path) {
|
||||
seen.push(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
seen
|
||||
}
|
||||
|
||||
pub fn apply(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||
let workdir = repo.workdir().ok_or(Error::NotWorkingRepo)?;
|
||||
let repo_files = find_repo_files(&workdir)?;
|
||||
let package_files = find_package_files(&pc);
|
||||
// Find all files in our repository that are no longer being referenced in
|
||||
// our primary config file. They should be removed from the repository.
|
||||
let lookup_files: HashSet<PathBuf> = package_files
|
||||
.iter()
|
||||
.map(|m| m.unresolved().to_path_buf())
|
||||
.collect();
|
||||
for repo_file in &repo_files {
|
||||
let relative = repo_file
|
||||
.resolved()
|
||||
.strip_prefix(workdir)
|
||||
.expect("Relative git file could not be stripped properly.")
|
||||
.to_path_buf();
|
||||
if !lookup_files.contains(&relative) {
|
||||
fs::remove_file(repo_file)?;
|
||||
}
|
||||
if let Some(p) = repo_file.resolved().parent() {
|
||||
if p.read_dir()?.next().is_none() {
|
||||
fs::remove_dir(p)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find all resolvable files in our primary config and copy them into the
|
||||
// repository.
|
||||
for package_file in &package_files {
|
||||
let mut copy = workdir.to_path_buf();
|
||||
copy.push(package_file.unresolved());
|
||||
if let Some(p) = copy.parent() {
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
fs::copy(package_file.resolved(), copy)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
26
src/lib.rs
26
src/lib.rs
|
@ -7,17 +7,20 @@ use config::PathConfig;
|
|||
use path::ResPathBuf;
|
||||
use std::{error::Error, io};
|
||||
|
||||
pub fn run_add(_config: PathConfig) -> Result<(), config::Error> {
|
||||
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
|
||||
type Result = std::result::Result<(), Box<dyn Error>>;
|
||||
|
||||
pub fn run_apply(config: PathConfig) -> Result {
|
||||
let repo = git::init(&config)?;
|
||||
git::apply(&config, &repo)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
||||
pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result {
|
||||
daemon::launch(config, freq_secs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
||||
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result {
|
||||
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
|
||||
if candidates.is_empty() {
|
||||
Err(config::Error::IOError(io::Error::new(
|
||||
|
@ -29,7 +32,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
|||
// Check if we already have a local config somewhere. If so, reprompt
|
||||
// the same configuration options and override the values present in the
|
||||
// current YAML file.
|
||||
Ok(loaded) => config::write(&loaded.0, Some(loaded.1))?,
|
||||
Ok(loaded) => config::write(&loaded.homesync_yml, Some(loaded.config))?,
|
||||
// Otherwise create a new config file at the given location. We always
|
||||
// assume we want to write to the first file in our priority list. If
|
||||
// not, the user should specify which config they want to write using
|
||||
|
@ -41,23 +44,12 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
Err(e) => Err(e)?,
|
||||
};
|
||||
// Verify (or create) our local and remote git repositories. The internal
|
||||
// git library we chose to use employs async/await so let's wrap around a
|
||||
// channel.
|
||||
git::init(&config)?;
|
||||
println!("\nFinished initialization.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
|
||||
pub fn run_list(config: PathConfig) -> Result {
|
||||
config::list_packages(config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_pull(_config: PathConfig) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_push(_config: PathConfig) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -16,7 +16,7 @@ fn main() {
|
|||
.expect("Could not initialize logger library.");
|
||||
|
||||
let matches = App::new("homesync")
|
||||
.about("Cross desktop configuration sync tool.")
|
||||
.about("Cross desktop sync tool.")
|
||||
.version("0.1.0")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.author("Joshua Potter <jrpotter.github.io>")
|
||||
|
@ -28,7 +28,9 @@ fn main() {
|
|||
.help("Specify a configuration file to use in place of defaults")
|
||||
.takes_value(true),
|
||||
)
|
||||
.subcommand(App::new("add").about("Add new configuration to local repository"))
|
||||
.subcommand(
|
||||
App::new("apply").about("Find all changes and apply them to the local repository"),
|
||||
)
|
||||
.subcommand(
|
||||
App::new("daemon")
|
||||
.about("Start up a new homesync daemon")
|
||||
|
@ -50,8 +52,6 @@ fn main() {
|
|||
)
|
||||
.subcommand(App::new("init").about("Initialize the homesync local repository"))
|
||||
.subcommand(App::new("list").about("See which packages homesync manages"))
|
||||
.subcommand(App::new("pull").about("Pull remote repository into local repository"))
|
||||
.subcommand(App::new("push").about("Push local repository to remote repository"))
|
||||
.get_matches();
|
||||
|
||||
if let Err(e) = dispatch(matches) {
|
||||
|
@ -71,7 +71,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
|||
subcommand => {
|
||||
let config = homesync::config::load(&candidates)?;
|
||||
match subcommand {
|
||||
Some(("add", _)) => Ok(homesync::run_add(config)?),
|
||||
Some(("apply", _)) => Ok(homesync::run_apply(config)?),
|
||||
Some(("daemon", matches)) => {
|
||||
let freq_secs: u64 = match matches.value_of("frequency") {
|
||||
Some(f) => f.parse().unwrap_or(0),
|
||||
|
@ -85,8 +85,6 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
|||
Ok(())
|
||||
}
|
||||
Some(("list", _)) => Ok(homesync::run_list(config)?),
|
||||
Some(("pull", _)) => Ok(homesync::run_pull(config)?),
|
||||
Some(("push", _)) => Ok(homesync::run_push(config)?),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
62
src/path.rs
62
src/path.rs
|
@ -1,4 +1,3 @@
|
|||
use regex::Regex;
|
||||
use serde::{
|
||||
de,
|
||||
de::{Unexpected, Visitor},
|
||||
|
@ -61,13 +60,30 @@ pub struct ResPathBuf {
|
|||
unresolved: PathBuf,
|
||||
}
|
||||
|
||||
fn unresolved_error(path: &Path) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Path '{}' should be fully resolved.", path.display()),
|
||||
)
|
||||
}
|
||||
|
||||
impl ResPathBuf {
|
||||
pub fn display(&self) -> std::path::Display {
|
||||
self.inner.display()
|
||||
pub fn new(path: &Path) -> Result<Self> {
|
||||
if !path.is_absolute() {
|
||||
Err(unresolved_error(path))?;
|
||||
}
|
||||
Ok(ResPathBuf {
|
||||
inner: path.to_path_buf(),
|
||||
unresolved: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolved(&self) -> &PathBuf {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn unresolved(&self) -> &PathBuf {
|
||||
return &self.unresolved;
|
||||
&self.unresolved
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,20 +230,8 @@ pub fn validate_is_dir(path: &Path) -> Result<()> {
|
|||
/// Find environment variables within the argument and expand them if possible.
|
||||
///
|
||||
/// Returns an error if any found environment variables are not defined.
|
||||
pub fn expand_env(s: &str) -> Result<String> {
|
||||
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap();
|
||||
let mut path = s.to_owned();
|
||||
for caps in re.captures_iter(s) {
|
||||
let evar = env::var(&caps["env"])?;
|
||||
path = path.replace(&format!("${}", &caps["env"]), &evar);
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Attempt to resolve the provided path, returning a fully resolved path
|
||||
/// instance if successful.
|
||||
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
||||
let mut resolved = env::current_dir()?;
|
||||
pub fn expand(path: &Path) -> Result<PathBuf> {
|
||||
let mut expanded = env::current_dir()?;
|
||||
for comp in path.components() {
|
||||
match comp {
|
||||
Component::Prefix(_) => Err(io::Error::new(
|
||||
|
@ -235,12 +239,12 @@ pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
|||
"We do not currently support Windows.",
|
||||
))?,
|
||||
Component::RootDir => {
|
||||
resolved.clear();
|
||||
resolved.push(Component::RootDir)
|
||||
expanded.clear();
|
||||
expanded.push(Component::RootDir)
|
||||
}
|
||||
Component::CurDir => (),
|
||||
Component::ParentDir => {
|
||||
if !resolved.pop() {
|
||||
if !expanded.pop() {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Cannot take parent of root.",
|
||||
|
@ -248,11 +252,23 @@ pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
|||
}
|
||||
}
|
||||
Component::Normal(c) => {
|
||||
let c: OsString = expand_env(&c.to_string_lossy())?.into();
|
||||
resolved.push(Component::Normal(&c));
|
||||
let lossy = c.to_string_lossy();
|
||||
if lossy.starts_with("$") {
|
||||
let evar = env::var(lossy.replacen("$", "", 1))?;
|
||||
expanded.push(Component::Normal(&OsString::from(evar)));
|
||||
} else {
|
||||
expanded.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
/// Attempt to resolve the provided path, returning a fully resolved path
|
||||
/// instance if successful.
|
||||
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
||||
let resolved = expand(&path)?;
|
||||
let resolved = resolved.canonicalize()?;
|
||||
Ok(ResPathBuf {
|
||||
inner: resolved,
|
||||
|
|
Loading…
Reference in New Issue