Setup local and remote with deserialization rules.
parent
606e3da69f
commit
17e2bf7a69
|
@ -20,3 +20,4 @@ serde = "1.0"
|
||||||
serde_derive = "1.0.132"
|
serde_derive = "1.0.132"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
yaml-rust = "0.4.4"
|
yaml-rust = "0.4.4"
|
||||||
|
url = { version = "2.2.2", features = ["serde"] }
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
---
|
---
|
||||||
remote:
|
remote: https://github.com/jrpotter/home-config.git
|
||||||
owner: jrpotter
|
|
||||||
name: home-config
|
|
||||||
packages:
|
packages:
|
||||||
homesync:
|
homesync:
|
||||||
configs:
|
configs:
|
||||||
|
|
147
src/cli.rs
147
src/cli.rs
|
@ -1,42 +1,125 @@
|
||||||
use super::config;
|
|
||||||
use super::config::PathConfig;
|
use super::config::PathConfig;
|
||||||
use ansi_term::Colour::Green as Success;
|
use super::path::ResPathBuf;
|
||||||
use ansi_term::Colour::Yellow as Warning;
|
use super::{config, path};
|
||||||
use std::io;
|
use ansi_term::Colour::{Green, Yellow};
|
||||||
|
use std::env::VarError;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{error, fmt, fs, io};
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Error
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ConfigError(config::Error),
|
||||||
|
IOError(io::Error),
|
||||||
|
ParseError(ParseError),
|
||||||
|
VarError(VarError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<config::Error> for Error {
|
||||||
|
fn from(err: config::Error) -> Error {
|
||||||
|
Error::ConfigError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(err: io::Error) -> Error {
|
||||||
|
Error::IOError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<path::Error> for Error {
|
||||||
|
fn from(err: path::Error) -> Error {
|
||||||
|
match err {
|
||||||
|
path::Error::IOError(e) => Error::IOError(e),
|
||||||
|
path::Error::VarError(e) => Error::VarError(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseError> for Error {
|
||||||
|
fn from(err: ParseError) -> Error {
|
||||||
|
Error::ParseError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VarError> for Error {
|
||||||
|
fn from(err: VarError) -> Error {
|
||||||
|
Error::VarError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::ConfigError(e) => write!(f, "{}", e),
|
||||||
|
Error::IOError(e) => write!(f, "{}", e),
|
||||||
|
Error::ParseError(e) => write!(f, "{}", e),
|
||||||
|
Error::VarError(e) => write!(f, "{}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Prompts
|
||||||
|
// ========================================
|
||||||
|
|
||||||
// TODO(jrpotter): Use curses to make this module behave nicer.
|
// TODO(jrpotter): Use curses to make this module behave nicer.
|
||||||
|
|
||||||
pub fn write_config(mut pending: PathConfig) -> config::Result<()> {
|
fn prompt_local(config: &PathConfig) -> Result<ResPathBuf> {
|
||||||
|
print!(
|
||||||
|
"Local git repository <{}> (enter to continue): ",
|
||||||
|
Yellow.paint(
|
||||||
|
config
|
||||||
|
.1
|
||||||
|
.local
|
||||||
|
.as_ref()
|
||||||
|
.map_or("".to_owned(), |v| v.display().to_string())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
let mut local = String::new();
|
||||||
|
io::stdin().read_line(&mut local)?;
|
||||||
|
let expanded = PathBuf::from(path::expand_env(&local.trim())?);
|
||||||
|
// We need to generate the directory beforehand to verify the path is
|
||||||
|
// actually valid. Worst case this leaves empty directories scattered in
|
||||||
|
// various locations after repeated initialization.
|
||||||
|
fs::create_dir_all(&expanded)?;
|
||||||
|
// Hard resolution should succeed now that the above directory was created.
|
||||||
|
Ok(path::resolve(&expanded)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_remote(config: &PathConfig) -> Result<Url> {
|
||||||
|
print!(
|
||||||
|
"Remote git repository <{}> (enter to continue): ",
|
||||||
|
Yellow.paint(config.1.remote.to_string())
|
||||||
|
);
|
||||||
|
io::stdout().flush()?;
|
||||||
|
let mut remote = String::new();
|
||||||
|
io::stdin().read_line(&mut remote)?;
|
||||||
|
Ok(Url::parse(&remote)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CLI
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
pub fn write_config(mut pending: PathConfig) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"Generating config at {}...\n",
|
"Generating config at {}...\n",
|
||||||
Success.paint(pending.0.unresolved().display().to_string())
|
Green.paint(pending.0.unresolved().display().to_string())
|
||||||
);
|
);
|
||||||
|
pending.1.local = Some(prompt_local(&pending)?);
|
||||||
print!(
|
pending.1.remote = prompt_remote(&pending)?;
|
||||||
"Git repository owner <{}> (enter to continue): ",
|
|
||||||
Warning.paint(pending.1.remote.owner.trim())
|
|
||||||
);
|
|
||||||
io::stdout().flush()?;
|
|
||||||
let mut owner = String::new();
|
|
||||||
io::stdin().read_line(&mut owner)?;
|
|
||||||
let owner = owner.trim().to_owned();
|
|
||||||
if !owner.is_empty() {
|
|
||||||
pending.1.remote.owner = owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
print!(
|
|
||||||
"Git repository name <{}> (enter to continue): ",
|
|
||||||
Warning.paint(pending.1.remote.name.trim())
|
|
||||||
);
|
|
||||||
io::stdout().flush()?;
|
|
||||||
let mut name = String::new();
|
|
||||||
io::stdin().read_line(&mut name)?;
|
|
||||||
let name = name.trim().to_owned();
|
|
||||||
if !name.is_empty() {
|
|
||||||
pending.1.remote.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending.write()?;
|
pending.write()?;
|
||||||
println!("\nFinished writing configuration file.");
|
println!("\nFinished writing configuration file.");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -45,7 +128,7 @@ pub fn write_config(mut pending: PathConfig) -> config::Result<()> {
|
||||||
pub fn list_packages(config: PathConfig) {
|
pub fn list_packages(config: PathConfig) {
|
||||||
println!(
|
println!(
|
||||||
"Listing packages in {}...\n",
|
"Listing packages in {}...\n",
|
||||||
Success.paint(config.0.unresolved().display().to_string())
|
Green.paint(config.0.unresolved().display().to_string())
|
||||||
);
|
);
|
||||||
// Alphabetical ordered ensured by B-tree implementation.
|
// Alphabetical ordered ensured by B-tree implementation.
|
||||||
for (k, _) in config.1.packages {
|
for (k, _) in config.1.packages {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::collections::BTreeMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{error, fmt, fs, io};
|
use std::{error, fmt, fs, io};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Error
|
// Error
|
||||||
|
@ -46,12 +47,6 @@ impl error::Error for Error {}
|
||||||
// Config
|
// Config
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Remote {
|
|
||||||
pub owner: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Package {
|
pub struct Package {
|
||||||
pub configs: Vec<PathBuf>,
|
pub configs: Vec<PathBuf>,
|
||||||
|
@ -59,7 +54,8 @@ pub struct Package {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub remote: Remote,
|
pub local: Option<ResPathBuf>,
|
||||||
|
pub remote: Url,
|
||||||
pub packages: BTreeMap<String, Package>,
|
pub packages: BTreeMap<String, Package>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,10 +73,8 @@ impl PathConfig {
|
||||||
PathConfig(
|
PathConfig(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
config.unwrap_or(Config {
|
config.unwrap_or(Config {
|
||||||
remote: Remote {
|
local: None,
|
||||||
owner: "example-user".to_owned(),
|
remote: Url::parse("http://github.com/user/repo.git").unwrap(),
|
||||||
name: "home-config".to_owned(),
|
|
||||||
},
|
|
||||||
packages: BTreeMap::new(),
|
packages: BTreeMap::new(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,7 @@ enum PollEvent {
|
||||||
fn resolve_pending(tx: &Sender<DebouncedEvent>, pending: &HashSet<PathBuf>) -> Vec<PathBuf> {
|
fn resolve_pending(tx: &Sender<DebouncedEvent>, pending: &HashSet<PathBuf>) -> Vec<PathBuf> {
|
||||||
let mut to_remove = vec![];
|
let mut to_remove = vec![];
|
||||||
for path in pending {
|
for path in pending {
|
||||||
match path::resolve(&path) {
|
match path::soft_resolve(&path) {
|
||||||
Ok(Some(resolved)) => {
|
Ok(Some(resolved)) => {
|
||||||
to_remove.push(path.clone());
|
to_remove.push(path.clone());
|
||||||
tx.send(DebouncedEvent::Create(resolved.into()))
|
tx.send(DebouncedEvent::Create(resolved.into()))
|
||||||
|
@ -125,7 +125,7 @@ impl<'a> WatchState<'a> {
|
||||||
self.watching.clear();
|
self.watching.clear();
|
||||||
for (_, package) in &config.1.packages {
|
for (_, package) in &config.1.packages {
|
||||||
for path in &package.configs {
|
for path in &package.configs {
|
||||||
match path::resolve(&path) {
|
match path::soft_resolve(&path) {
|
||||||
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
|
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
|
||||||
Ok(Some(n)) => self.watch(n),
|
Ok(Some(n)) => self.watch(n),
|
||||||
Err(_) => (),
|
Err(_) => (),
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use super::config::PathConfig;
|
use super::config::PathConfig;
|
||||||
use git2::Repository;
|
|
||||||
use octocrab;
|
|
||||||
|
|
||||||
/// Sets up a local github repository all configuration files will be synced to.
|
/// Sets up a local github repository all configuration files will be synced to.
|
||||||
/// We attempt to clone the remote repository in favor of building our own.
|
/// We attempt to clone the remote repository in favor of building our own.
|
||||||
|
@ -13,6 +11,6 @@ use octocrab;
|
||||||
///
|
///
|
||||||
/// NOTE! This does not perform any syncing between local and remote. That
|
/// NOTE! This does not perform any syncing between local and remote. That
|
||||||
/// should be done as a specific command line request.
|
/// should be done as a specific command line request.
|
||||||
pub async fn init(config: &PathConfig) {
|
pub async fn init(_config: &PathConfig) {
|
||||||
// TODO(jrpotter): Fill this out.
|
// TODO(jrpotter): Fill this out.
|
||||||
}
|
}
|
||||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -19,19 +19,22 @@ pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), config::Error> {
|
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
||||||
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
|
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
return Err(config::Error::FileError(io::Error::new(
|
Err(config::Error::FileError(io::Error::new(
|
||||||
io::ErrorKind::NotFound,
|
io::ErrorKind::NotFound,
|
||||||
"No suitable config file found.",
|
"No suitable config file found.",
|
||||||
)));
|
)))?;
|
||||||
}
|
}
|
||||||
match config::load(&candidates) {
|
match config::load(&candidates) {
|
||||||
// Check if we already have a local config somewhere. If so, reprompt
|
// Check if we already have a local config somewhere. If so, reprompt
|
||||||
// the same configuration options and override the values present in the
|
// the same configuration options and override the values present in the
|
||||||
// current YAML file.
|
// current YAML file.
|
||||||
Ok(pending) => cli::write_config(pending),
|
Ok(pending) => {
|
||||||
|
cli::write_config(pending)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
// Otherwise create a new config file at the given location. We always
|
// 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
|
// 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
|
// not, the user should specify which config they want to write using
|
||||||
|
@ -40,9 +43,10 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), config::Error> {
|
||||||
// Make directories if necessary.
|
// Make directories if necessary.
|
||||||
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
|
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
|
||||||
let pending = PathConfig::new(&candidates[0], None);
|
let pending = PathConfig::new(&candidates[0], None);
|
||||||
cli::write_config(pending)
|
cli::write_config(pending)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ fn find_candidates(matches: &clap::ArgMatches) -> Result<Vec<ResPathBuf>, io::Er
|
||||||
};
|
};
|
||||||
let mut resolved = vec![];
|
let mut resolved = vec![];
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
if let Ok(Some(r)) = homesync::path::resolve(&candidate) {
|
if let Ok(Some(r)) = homesync::path::soft_resolve(&candidate) {
|
||||||
resolved.push(r);
|
resolved.push(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
221
src/path.rs
221
src/path.rs
|
@ -1,9 +1,51 @@
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::env;
|
use serde::de;
|
||||||
use std::ffi::{OsStr, OsString};
|
use serde::de::{Unexpected, Visitor};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use std::env::VarError;
|
||||||
|
use std::error;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io;
|
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
use std::result;
|
||||||
|
use std::str;
|
||||||
|
use std::{env, io};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Error
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
IOError(io::Error),
|
||||||
|
VarError(VarError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(err: io::Error) -> Error {
|
||||||
|
Error::IOError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VarError> for Error {
|
||||||
|
fn from(err: VarError) -> Error {
|
||||||
|
Error::VarError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::IOError(e) => write!(f, "{}", e),
|
||||||
|
Error::VarError(e) => write!(f, "{}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Path
|
// Path
|
||||||
|
@ -65,65 +107,140 @@ impl Hash for ResPathBuf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find environment variables found within the argument and expand them if
|
// ========================================
|
||||||
/// possible.
|
// (De)serialization
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
impl Serialize for ResPathBuf {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
self.inner.as_path().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResPathBufVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for ResPathBufVisitor {
|
||||||
|
type Value = ResPathBuf;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("path string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
resolve(&PathBuf::from(&v))
|
||||||
|
.map_err(|_| de::Error::custom(format!("Could not resolve path {}", v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E>(self, v: String) -> result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
resolve(&PathBuf::from(&v))
|
||||||
|
.map_err(|_| de::Error::custom(format!("Could not resolve path {}", v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, v: &[u8]) -> result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
let value = str::from_utf8(v)
|
||||||
|
.map(From::from)
|
||||||
|
.map_err(|_| de::Error::invalid_value(Unexpected::Bytes(v), &self))?;
|
||||||
|
self.visit_str(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_byte_buf<E>(self, v: Vec<u8>) -> result::Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
let value = String::from_utf8(v)
|
||||||
|
.map(From::from)
|
||||||
|
.map_err(|e| de::Error::invalid_value(Unexpected::Bytes(&e.into_bytes()), &self))?;
|
||||||
|
self.visit_string(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ResPathBuf {
|
||||||
|
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_string(ResPathBufVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Resolution
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Find environment variables within the argument and expand them if possible.
|
||||||
///
|
///
|
||||||
/// Returns `None` in the case an environment variable present within the
|
/// Returns an error if any found environment variables are not defined.
|
||||||
/// argument is not defined.
|
pub fn expand_env(s: &str) -> Result<String> {
|
||||||
fn expand_env(s: &OsStr) -> Option<OsString> {
|
|
||||||
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap();
|
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap();
|
||||||
let lossy = s.to_string_lossy();
|
let mut path = s.to_owned();
|
||||||
let mut path = lossy.clone().to_string();
|
for caps in re.captures_iter(s) {
|
||||||
for caps in re.captures_iter(&lossy) {
|
let evar = env::var(&caps["env"])?;
|
||||||
let evar = env::var(&caps["env"]).ok()?;
|
|
||||||
path = path.replace(&format!("${}", &caps["env"]), &evar);
|
path = path.replace(&format!("${}", &caps["env"]), &evar);
|
||||||
}
|
}
|
||||||
Some(path.into())
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to resolve the provided path, returning a fully resolved path
|
/// Attempt to resolve the provided path, returning a fully resolved path
|
||||||
/// instance.
|
/// instance if successful.
|
||||||
|
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
||||||
|
let mut resolved = env::current_dir()?;
|
||||||
|
for comp in path.components() {
|
||||||
|
match comp {
|
||||||
|
Component::Prefix(_) => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"We do not currently support Windows.",
|
||||||
|
))?,
|
||||||
|
Component::RootDir => {
|
||||||
|
resolved.clear();
|
||||||
|
resolved.push(Component::RootDir)
|
||||||
|
}
|
||||||
|
Component::CurDir => (),
|
||||||
|
Component::ParentDir => {
|
||||||
|
if !resolved.pop() {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Cannot take parent of root.",
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component::Normal(c) => {
|
||||||
|
let c: OsString = expand_env(&c.to_string_lossy())?.into();
|
||||||
|
resolved.push(Component::Normal(&c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let resolved = resolved.canonicalize()?;
|
||||||
|
Ok(ResPathBuf {
|
||||||
|
inner: resolved,
|
||||||
|
unresolved: path.to_path_buf(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to resolve the provided path, returning a fully resolved path
|
||||||
|
/// instance if successful.
|
||||||
///
|
///
|
||||||
/// If the provided file does not exist but could potentially exist in the
|
/// If the provided file does not exist but could potentially exist in the
|
||||||
/// future (e.g. for paths with environment variables defined), this will
|
/// future (e.g. for paths with environment variables defined), this will
|
||||||
/// return a `None` instead of an error.
|
/// return a `None` instead of an error.
|
||||||
pub fn resolve(path: &Path) -> io::Result<Option<ResPathBuf>> {
|
pub fn soft_resolve(path: &Path) -> Result<Option<ResPathBuf>> {
|
||||||
let mut expanded = env::current_dir()?;
|
match resolve(path) {
|
||||||
for comp in path.components() {
|
Ok(resolved) => Ok(Some(resolved)),
|
||||||
match comp {
|
Err(Error::IOError(e)) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||||
Component::Prefix(_) => {
|
Err(e @ Error::IOError(_)) => Err(e),
|
||||||
return Err(io::Error::new(
|
// An ENV variable isn't defined yet, but we assume its possible it'll
|
||||||
io::ErrorKind::InvalidInput,
|
// be defined in the future. Don't report as an error.
|
||||||
"We do not currently support Windows.",
|
Err(Error::VarError(_)) => Ok(None),
|
||||||
))
|
|
||||||
}
|
|
||||||
Component::RootDir => {
|
|
||||||
expanded.clear();
|
|
||||||
expanded.push(Component::RootDir)
|
|
||||||
}
|
|
||||||
Component::CurDir => (), // Make no changes.
|
|
||||||
Component::ParentDir => {
|
|
||||||
if !expanded.pop() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Cannot take parent of root.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component::Normal(c) => match expand_env(c) {
|
|
||||||
Some(c) => expanded.push(Component::Normal(&c)),
|
|
||||||
// The environment variable isn't defined yet but might be in
|
|
||||||
// the future.
|
|
||||||
None => return Ok(None),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match expanded.canonicalize() {
|
|
||||||
Ok(resolved) => Ok(Some(ResPathBuf {
|
|
||||||
inner: resolved,
|
|
||||||
unresolved: path.to_path_buf(),
|
|
||||||
})),
|
|
||||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue