Separate path module and a normalized newtype.
parent
ba69724c49
commit
4fa6654423
36
README.md
36
README.md
|
@ -1,6 +1,6 @@
|
||||||
# homesync
|
# homesync
|
||||||
|
|
||||||
**Caution! This is unstable code!**
|
**Caution! This is a work in progress and far from complete!**
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
@ -31,7 +31,39 @@ configuration is according to package manager, platform, etc.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
TODO
|
Verify your installation by running `homesync` from the command line. If
|
||||||
|
installed, you will likely want to initialize a new config instance. Do so by
|
||||||
|
typing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ homesync init
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then walk through what github repository you want to sync your various
|
||||||
|
files with. You can have homesync automatically monitor all configuration files
|
||||||
|
and post updates on changes by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ homesync daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
As changes are made to your `homesync` config or any configuration files
|
||||||
|
referred to within the `homesync` config, the daemon service will sync the
|
||||||
|
changes to the local git repository. To push these changes upward, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ homesync push --all
|
||||||
|
```
|
||||||
|
|
||||||
|
which will expose a git interface for you to complete the push. Lastly, to sync
|
||||||
|
the remote configurations to your local files, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ homesync pull --all
|
||||||
|
```
|
||||||
|
|
||||||
|
This will load up a diff wrapper for you to ensure you make the changes you'd
|
||||||
|
like.
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use ansi_term::Colour::Green;
|
use super::path;
|
||||||
|
use super::path::{NormalPathBuf, Normalize};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::{env, error, fmt, fs, io};
|
use std::{error, fmt, fs, io};
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Error
|
// Error
|
||||||
|
@ -70,12 +71,12 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PathConfig(pub PathBuf, pub Config);
|
pub struct PathConfig(pub NormalPathBuf, pub Config);
|
||||||
|
|
||||||
impl PathConfig {
|
impl PathConfig {
|
||||||
pub fn new(path: &Path, config: Option<Config>) -> Self {
|
pub fn new(path: &NormalPathBuf, config: Option<Config>) -> Self {
|
||||||
PathConfig(
|
PathConfig(
|
||||||
path.to_path_buf(),
|
path.clone(),
|
||||||
config.unwrap_or(Config {
|
config.unwrap_or(Config {
|
||||||
remote: Remote {
|
remote: Remote {
|
||||||
owner: "example-user".to_owned(),
|
owner: "example-user".to_owned(),
|
||||||
|
@ -86,8 +87,8 @@ impl PathConfig {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self) -> Result<()> {
|
|
||||||
// TODO(jrpotter): Create backup file before overwriting.
|
// TODO(jrpotter): Create backup file before overwriting.
|
||||||
|
pub fn write(&self) -> Result<()> {
|
||||||
let mut file = fs::File::create(&self.0)?;
|
let mut file = fs::File::create(&self.0)?;
|
||||||
let serialized = serde_yaml::to_string(&self.1)?;
|
let serialized = serde_yaml::to_string(&self.1)?;
|
||||||
file.write_all(serialized.as_bytes())?;
|
file.write_all(serialized.as_bytes())?;
|
||||||
|
@ -99,45 +100,27 @@ impl PathConfig {
|
||||||
// Loading
|
// Loading
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
/// Returns the default configuration files `homesync` looks for.
|
pub const DEFAULT_PATHS: &[&str] = &[
|
||||||
///
|
"$HOME/.homesync.yml",
|
||||||
/// - `$HOME/.homesync.yml`
|
"$HOME/.config/homesync/homesync.yml",
|
||||||
/// - `$HOME/.config/homesync/homesync.yml`
|
"$XDG_CONFIG_HOME/homesync.yml",
|
||||||
/// - `$XDG_CONFIG_HOME/homesync.yml`
|
"$XDG_CONFIG_HOME/homesync/homesync.yml",
|
||||||
/// - `$XDG_CONFIG_HOME/homesync/homesync.yml`
|
];
|
||||||
///
|
|
||||||
/// Returned `PathBuf`s are looked for in the above order.
|
|
||||||
pub fn default_paths() -> Vec<PathBuf> {
|
pub fn default_paths() -> Vec<PathBuf> {
|
||||||
let mut paths: Vec<PathBuf> = Vec::new();
|
DEFAULT_PATHS.iter().map(|s| PathBuf::from(s)).collect()
|
||||||
if let Ok(home) = env::var("HOME") {
|
|
||||||
paths.extend_from_slice(&[
|
|
||||||
[&home, ".homesync.yml"].iter().collect(),
|
|
||||||
[&home, ".config", "homesync", "homesync.yml"]
|
|
||||||
.iter()
|
|
||||||
.collect(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
|
|
||||||
paths.extend_from_slice(&[
|
|
||||||
[&xdg_config_home, "homesync.yml"].iter().collect(),
|
|
||||||
[&xdg_config_home, "homesync", "homesync.yml"]
|
|
||||||
.iter()
|
|
||||||
.collect(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
paths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(candidates: &Vec<PathBuf>) -> Result<PathConfig> {
|
pub fn load(candidates: &Vec<NormalPathBuf>) -> Result<PathConfig> {
|
||||||
// When trying our paths, the only acceptable error is a `NotFound` file.
|
// When trying our paths, the only acceptable error is a `NotFound` file.
|
||||||
// Anything else should be surfaced to the end user.
|
// Anything else should be surfaced to the end user.
|
||||||
for path in candidates {
|
for candidate in candidates {
|
||||||
match fs::read_to_string(path) {
|
match fs::read_to_string(candidate) {
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||||
Err(err) => return Err(Error::FileError(err)),
|
Err(err) => return Err(Error::FileError(err)),
|
||||||
Ok(contents) => {
|
Ok(contents) => {
|
||||||
let config = Config::new(&contents)?;
|
let config = Config::new(&contents)?;
|
||||||
return Ok(PathConfig::new(&path, Some(config)));
|
return Ok(PathConfig::new(candidate, Some(config)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
163
src/daemon.rs
163
src/daemon.rs
|
@ -1,114 +1,93 @@
|
||||||
|
use super::config;
|
||||||
use super::config::PathConfig;
|
use super::config::PathConfig;
|
||||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
use super::path;
|
||||||
use regex::Regex;
|
use super::path::{NormalPathBuf, Normalize};
|
||||||
|
use notify::{RecommendedWatcher, Watcher};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::path::PathBuf;
|
||||||
use std::ffi::{OsStr, OsString};
|
|
||||||
use std::io;
|
|
||||||
use std::path::{Component, Path, PathBuf};
|
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// TODO(jrpotter): Add logging.
|
// TODO(jrpotter): Add logging.
|
||||||
// TODO(jrpotter): Add pid file to only allow one daemon at a time.
|
// TODO(jrpotter): Add pid file to only allow one daemon at a time.
|
||||||
|
|
||||||
// Find environment variables found within the argument and expand them if
|
// ========================================
|
||||||
// possible.
|
// State
|
||||||
//
|
// ========================================
|
||||||
// Returns `None` in the case an environment variable present within the
|
|
||||||
// argument is not defined.
|
struct WatchState {
|
||||||
fn expand_str(s: &OsStr) -> Option<OsString> {
|
// Paths that we were not able to watch properly but could potentially do so
|
||||||
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap();
|
// in the future. These include paths that did not exist at the time of
|
||||||
let lossy = s.to_string_lossy();
|
// canonicalization or did not have environment variables defined that may
|
||||||
let mut path = lossy.clone().to_string();
|
// be defined later on.
|
||||||
for caps in re.captures_iter(&lossy) {
|
pending: HashSet<PathBuf>,
|
||||||
let evar = env::var(&caps["env"]).ok()?;
|
// Paths that we are currently watching.
|
||||||
path = path.replace(&format!("${}", &caps["env"]), &evar);
|
watching: HashSet<NormalPathBuf>,
|
||||||
}
|
// Paths that are not valid and will never become valid. These may include
|
||||||
Some(path.into())
|
// paths that include prefixes or refer to directories that could never be
|
||||||
|
// reached (e.g. parent of root).
|
||||||
|
invalid: HashSet<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalizes the provided path, returning a new instance.
|
impl WatchState {
|
||||||
//
|
pub fn new(config: &PathConfig) -> notify::Result<Self> {
|
||||||
// There current doesn't exist a method that yields some canonical path for
|
let mut pending: HashSet<PathBuf> = HashSet::new();
|
||||||
// files that do not exist (at least in the parts of the standard library I've
|
let mut watching: HashSet<NormalPathBuf> = HashSet::new();
|
||||||
// looked in). We create a consistent view of every path so as to avoid
|
watching.insert(config.0.clone());
|
||||||
// watching the same path multiple times, which would duplicate messages on
|
// We try and resolve our configuration again here. We want to
|
||||||
// changes.
|
// specifically track any new configs that may pop up with higher
|
||||||
fn normalize_path(path: &Path) -> io::Result<PathBuf> {
|
// priority.
|
||||||
let mut pb = env::current_dir()?;
|
for path in config::default_paths() {
|
||||||
for comp in path.components() {
|
match path::normalize(&path)? {
|
||||||
match comp {
|
// TODO(jrpotter): Check if the path can be canonicalized.
|
||||||
Component::Prefix(_) => {
|
Normalize::Done(p) => watching.insert(p),
|
||||||
return Err(io::Error::new(
|
Normalize::Pending => pending.insert(path),
|
||||||
io::ErrorKind::InvalidInput,
|
};
|
||||||
"We do not currently support Windows.",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
Component::RootDir => {
|
Ok(WatchState {
|
||||||
pb.clear();
|
pending,
|
||||||
pb.push(Component::RootDir)
|
watching,
|
||||||
|
invalid: HashSet::new(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Component::CurDir => (), // Make no changes.
|
|
||||||
Component::ParentDir => {
|
|
||||||
if !pb.pop() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Cannot take parent of root.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component::Normal(c) => match expand_str(c) {
|
|
||||||
Some(c) => pb.push(Component::Normal(&c)),
|
|
||||||
None => {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
format!("Cannot find path {}", path.display().to_string()),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(pb)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Launches the daemon instance.
|
// ========================================
|
||||||
///
|
// Daemon
|
||||||
/// This method also spawns an additional thread responsible for handling
|
// ========================================
|
||||||
/// polling of files that are specified in the config but do not exist.
|
|
||||||
|
fn reload_config(config: &PathConfig) -> notify::Result<WatchState> {
|
||||||
|
let state = WatchState::new(config)?;
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn launch(config: PathConfig) -> notify::Result<()> {
|
pub fn launch(config: PathConfig) -> notify::Result<()> {
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
// Create a "debounced" watcher. Events will not trigger until after the
|
// Create a "debounced" watcher. Events will not trigger until after the
|
||||||
// specified duration has passed with no additional changes.
|
// specified duration has passed with no additional changes.
|
||||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?;
|
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?;
|
||||||
// Take in the `homesync` configuration and add watchers to all paths
|
// for (_, package) in &config.1.packages {
|
||||||
// specified in the configuration. `watch` appends the path onto a list so
|
// for path in &package.configs {
|
||||||
// avoid tracking the same path multiple times.
|
// match normalize_path(&path) {
|
||||||
let mut tracked_paths: HashSet<PathBuf> = HashSet::new();
|
// // `notify-rs` is not able to handle files that do not exist and
|
||||||
// TODO(jrpotter): Spawn thread responsible for polling for missing files.
|
// // are then created. This is handled internally by the library
|
||||||
let mut missing_paths: HashSet<PathBuf> = HashSet::new();
|
// // via the `fs::canonicalize` which fails on missing paths. So
|
||||||
for (_, package) in &config.1.packages {
|
// // track which paths end up missing and apply polling on them.
|
||||||
for path in &package.configs {
|
// Ok(normalized) => match watcher.watch(&normalized, RecursiveMode::NonRecursive) {
|
||||||
match normalize_path(&path) {
|
// Ok(_) => {
|
||||||
// `notify-rs` is not able to handle files that do not exist and
|
// tracked_paths.insert(normalized);
|
||||||
// are then created. This is handled internally by the library
|
// }
|
||||||
// via the `fs::canonicalize` which fails on missing paths. So
|
// Err(notify::Error::PathNotFound) => {
|
||||||
// track which paths end up missing and apply polling on them.
|
// missing_paths.insert(normalized);
|
||||||
Ok(normalized) => match watcher.watch(&normalized, RecursiveMode::NonRecursive) {
|
// }
|
||||||
Ok(_) => {
|
// Err(e) => return Err(e),
|
||||||
tracked_paths.insert(normalized);
|
// },
|
||||||
}
|
// // TODO(jrpotter): Retry even in cases where environment
|
||||||
Err(notify::Error::PathNotFound) => {
|
// // variables are not defined.
|
||||||
missing_paths.insert(normalized);
|
// Err(e) => eprintln!("{}", e),
|
||||||
}
|
// };
|
||||||
Err(e) => return Err(e),
|
// }
|
||||||
},
|
// }
|
||||||
// TODO(jrpotter): Retry even in cases where environment
|
|
||||||
// variables are not defined.
|
|
||||||
Err(e) => eprintln!("{}", e),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This is a simple loop, but you may want to use more complex logic here,
|
// This is a simple loop, but you may want to use more complex logic here,
|
||||||
// for example to handle I/O.
|
// for example to handle I/O.
|
||||||
loop {
|
loop {
|
||||||
|
|
29
src/lib.rs
29
src/lib.rs
|
@ -1,23 +1,31 @@
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
|
pub mod path;
|
||||||
|
|
||||||
use config::PathConfig;
|
use config::PathConfig;
|
||||||
|
use path::NormalPathBuf;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::PathBuf;
|
use std::io;
|
||||||
|
|
||||||
pub fn run_add(_candidates: Vec<PathBuf>) -> Result<(), config::Error> {
|
pub fn run_add(_config: PathConfig) -> Result<(), config::Error> {
|
||||||
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
|
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_daemon(candidates: Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
|
pub fn run_daemon(config: PathConfig) -> Result<(), Box<dyn Error>> {
|
||||||
let loaded = config::load(&candidates)?;
|
daemon::launch(config)?;
|
||||||
daemon::launch(loaded)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_init(candidates: Vec<PathBuf>) -> Result<(), config::Error> {
|
pub fn run_init(candidates: Vec<NormalPathBuf>) -> Result<(), config::Error> {
|
||||||
|
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
|
||||||
|
if candidates.is_empty() {
|
||||||
|
return Err(config::Error::FileError(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"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
|
||||||
|
@ -37,16 +45,15 @@ pub fn run_init(candidates: Vec<PathBuf>) -> Result<(), config::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_list(candidates: Vec<PathBuf>) -> Result<(), config::Error> {
|
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
|
||||||
let loaded = config::load(&candidates)?;
|
cli::list_packages(config);
|
||||||
cli::list_packages(loaded);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_pull() -> Result<(), Box<dyn Error>> {
|
pub fn run_pull(_config: PathConfig) -> Result<(), Box<dyn Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_push() -> Result<(), Box<dyn Error>> {
|
pub fn run_push(_config: PathConfig) -> Result<(), Box<dyn Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
74
src/main.rs
74
src/main.rs
|
@ -1,20 +1,9 @@
|
||||||
use clap::{App, AppSettings, Arg};
|
use clap::{App, AppSettings, Arg};
|
||||||
|
use homesync::path::{NormalPathBuf, Normalize};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn dispatch(paths: Vec<PathBuf>, matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
|
||||||
match matches.subcommand() {
|
|
||||||
Some(("add", _)) => homesync::run_add(paths)?,
|
|
||||||
Some(("daemon", _)) => homesync::run_daemon(paths)?,
|
|
||||||
Some(("init", _)) => homesync::run_init(paths)?,
|
|
||||||
Some(("list", _)) => homesync::run_list(paths)?,
|
|
||||||
Some(("pull", _)) => homesync::run_pull()?,
|
|
||||||
Some(("push", _)) => homesync::run_push()?,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = App::new("homesync")
|
let matches = App::new("homesync")
|
||||||
.about("Cross desktop configuration sync tool.")
|
.about("Cross desktop configuration sync tool.")
|
||||||
|
@ -37,12 +26,59 @@ fn main() {
|
||||||
.subcommand(App::new("push").about("Push local repository to remote repository"))
|
.subcommand(App::new("push").about("Push local repository to remote repository"))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let candidates = match matches.value_of("config") {
|
if let Err(e) = dispatch(matches) {
|
||||||
Some(path) => vec![PathBuf::from(path)],
|
|
||||||
None => homesync::config::default_paths(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = dispatch(candidates, matches) {
|
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||||
|
let candidates = find_candidates(&matches)?;
|
||||||
|
match matches.subcommand() {
|
||||||
|
Some(("init", _)) => Ok(homesync::run_init(candidates)?),
|
||||||
|
// All subcommands beside `init` require a config. If we invoke any of
|
||||||
|
// these, immediately attempt to load our config. Note once a config is
|
||||||
|
// loaded, this same config is used throughout the lifetime of the
|
||||||
|
// process. We avoid introducing the ability to "change" which config is
|
||||||
|
// used, even if one of higher priority is eventually defined.
|
||||||
|
subcommand => {
|
||||||
|
let config = homesync::config::load(&candidates)?;
|
||||||
|
match subcommand {
|
||||||
|
Some(("add", _)) => Ok(homesync::run_add(config)?),
|
||||||
|
Some(("daemon", _)) => Ok(homesync::run_daemon(config)?),
|
||||||
|
Some(("list", _)) => Ok(homesync::run_list(config)?),
|
||||||
|
Some(("pull", _)) => Ok(homesync::run_pull(config)?),
|
||||||
|
Some(("push", _)) => Ok(homesync::run_push(config)?),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_candidates(matches: &clap::ArgMatches) -> Result<Vec<NormalPathBuf>, Box<dyn Error>> {
|
||||||
|
let candidates = match matches.value_of("config") {
|
||||||
|
Some(config_match) => vec![PathBuf::from(config_match)],
|
||||||
|
None => homesync::config::default_paths(),
|
||||||
|
};
|
||||||
|
let mut normals = vec![];
|
||||||
|
for candidate in candidates {
|
||||||
|
if let Ok(Normalize::Done(n)) = homesync::path::normalize(&candidate) {
|
||||||
|
normals.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if normals.is_empty() {
|
||||||
|
if let Some(config_match) = matches.value_of("config") {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!("{} is not a valid config path.", config_match),
|
||||||
|
))?
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"Could not find a suitable configuration path. Is \
|
||||||
|
$XDG_CONFIG_PATH or $HOME defined?",
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(normals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
use regex::Regex;
|
||||||
|
use std::env;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Path
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct NormalPathBuf(PathBuf);
|
||||||
|
|
||||||
|
impl NormalPathBuf {
|
||||||
|
pub fn display(&self) -> std::path::Display {
|
||||||
|
self.0.display()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for NormalPathBuf {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<PathBuf> for NormalPathBuf {
|
||||||
|
fn as_ref(&self) -> &PathBuf {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for NormalPathBuf {
|
||||||
|
fn hash<H: Hasher>(&self, h: &mut H) {
|
||||||
|
for component in self.0.components() {
|
||||||
|
component.hash(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Normalize {
|
||||||
|
Done(NormalPathBuf), // An instance of a fully resolved path.
|
||||||
|
Pending, // An instance of a path that cannot yet be normalized.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find environment variables found within the argument and expand them if
|
||||||
|
// possible.
|
||||||
|
//
|
||||||
|
// Returns `None` in the case an environment variable present within the
|
||||||
|
// argument is not defined.
|
||||||
|
fn expand_env(s: &OsStr) -> Option<OsString> {
|
||||||
|
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap();
|
||||||
|
let lossy = s.to_string_lossy();
|
||||||
|
let mut path = lossy.clone().to_string();
|
||||||
|
for caps in re.captures_iter(&lossy) {
|
||||||
|
let evar = env::var(&caps["env"]).ok()?;
|
||||||
|
path = path.replace(&format!("${}", &caps["env"]), &evar);
|
||||||
|
}
|
||||||
|
Some(path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizes the provided path, returning a new instance.
|
||||||
|
//
|
||||||
|
// There currently does not exist a method that yields some canonical path for
|
||||||
|
// files that do not exist (at least in the parts of the standard library I've
|
||||||
|
// looked in). We create a consistent view of every path so as to avoid watching
|
||||||
|
// the same path multiple times, which would duplicate messages on changes.
|
||||||
|
//
|
||||||
|
// Note this does not actually prevent the issue fully. We could have two paths
|
||||||
|
// that refer to the same real path - normalization would not catch this.
|
||||||
|
pub fn normalize(path: &Path) -> io::Result<Normalize> {
|
||||||
|
let mut pb = env::current_dir()?;
|
||||||
|
for comp in path.components() {
|
||||||
|
match comp {
|
||||||
|
Component::Prefix(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"We do not currently support Windows.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Component::RootDir => {
|
||||||
|
pb.clear();
|
||||||
|
pb.push(Component::RootDir)
|
||||||
|
}
|
||||||
|
Component::CurDir => (), // Make no changes.
|
||||||
|
Component::ParentDir => {
|
||||||
|
if !pb.pop() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Cannot take parent of root.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component::Normal(c) => match expand_env(c) {
|
||||||
|
Some(c) => pb.push(Component::Normal(&c)),
|
||||||
|
// The environment variable isn't defined yet but might be in
|
||||||
|
// the future.
|
||||||
|
None => return Ok(Normalize::Pending),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Normalize::Done(NormalPathBuf(pb)))
|
||||||
|
}
|
Loading…
Reference in New Issue