Setup local and remote with deserialization rules.
parent
606e3da69f
commit
17e2bf7a69
|
@ -20,3 +20,4 @@ serde = "1.0"
|
|||
serde_derive = "1.0.132"
|
||||
serde_yaml = "0.8"
|
||||
yaml-rust = "0.4.4"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
remote:
|
||||
owner: jrpotter
|
||||
name: home-config
|
||||
remote: https://github.com/jrpotter/home-config.git
|
||||
packages:
|
||||
homesync:
|
||||
configs:
|
||||
|
|
147
src/cli.rs
147
src/cli.rs
|
@ -1,42 +1,125 @@
|
|||
use super::config;
|
||||
use super::config::PathConfig;
|
||||
use ansi_term::Colour::Green as Success;
|
||||
use ansi_term::Colour::Yellow as Warning;
|
||||
use std::io;
|
||||
use super::path::ResPathBuf;
|
||||
use super::{config, path};
|
||||
use ansi_term::Colour::{Green, Yellow};
|
||||
use std::env::VarError;
|
||||
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.
|
||||
|
||||
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!(
|
||||
"Generating config at {}...\n",
|
||||
Success.paint(pending.0.unresolved().display().to_string())
|
||||
Green.paint(pending.0.unresolved().display().to_string())
|
||||
);
|
||||
|
||||
print!(
|
||||
"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.1.local = Some(prompt_local(&pending)?);
|
||||
pending.1.remote = prompt_remote(&pending)?;
|
||||
pending.write()?;
|
||||
println!("\nFinished writing configuration file.");
|
||||
Ok(())
|
||||
|
@ -45,7 +128,7 @@ pub fn write_config(mut pending: PathConfig) -> config::Result<()> {
|
|||
pub fn list_packages(config: PathConfig) {
|
||||
println!(
|
||||
"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.
|
||||
for (k, _) in config.1.packages {
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::collections::BTreeMap;
|
|||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{error, fmt, fs, io};
|
||||
use url::Url;
|
||||
|
||||
// ========================================
|
||||
// Error
|
||||
|
@ -46,12 +47,6 @@ impl error::Error for Error {}
|
|||
// Config
|
||||
// ========================================
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Remote {
|
||||
pub owner: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Package {
|
||||
pub configs: Vec<PathBuf>,
|
||||
|
@ -59,7 +54,8 @@ pub struct Package {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub remote: Remote,
|
||||
pub local: Option<ResPathBuf>,
|
||||
pub remote: Url,
|
||||
pub packages: BTreeMap<String, Package>,
|
||||
}
|
||||
|
||||
|
@ -77,10 +73,8 @@ impl PathConfig {
|
|||
PathConfig(
|
||||
path.clone(),
|
||||
config.unwrap_or(Config {
|
||||
remote: Remote {
|
||||
owner: "example-user".to_owned(),
|
||||
name: "home-config".to_owned(),
|
||||
},
|
||||
local: None,
|
||||
remote: Url::parse("http://github.com/user/repo.git").unwrap(),
|
||||
packages: BTreeMap::new(),
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -26,7 +26,7 @@ enum PollEvent {
|
|||
fn resolve_pending(tx: &Sender<DebouncedEvent>, pending: &HashSet<PathBuf>) -> Vec<PathBuf> {
|
||||
let mut to_remove = vec![];
|
||||
for path in pending {
|
||||
match path::resolve(&path) {
|
||||
match path::soft_resolve(&path) {
|
||||
Ok(Some(resolved)) => {
|
||||
to_remove.push(path.clone());
|
||||
tx.send(DebouncedEvent::Create(resolved.into()))
|
||||
|
@ -125,7 +125,7 @@ impl<'a> WatchState<'a> {
|
|||
self.watching.clear();
|
||||
for (_, package) in &config.1.packages {
|
||||
for path in &package.configs {
|
||||
match path::resolve(&path) {
|
||||
match path::soft_resolve(&path) {
|
||||
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
|
||||
Ok(Some(n)) => self.watch(n),
|
||||
Err(_) => (),
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use super::config::PathConfig;
|
||||
use git2::Repository;
|
||||
use octocrab;
|
||||
|
||||
/// 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.
|
||||
|
@ -13,6 +11,6 @@ use octocrab;
|
|||
///
|
||||
/// NOTE! This does not perform any syncing between local and remote. That
|
||||
/// 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.
|
||||
}
|
||||
|
|
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(())
|
||||
}
|
||||
|
||||
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`.");
|
||||
if candidates.is_empty() {
|
||||
return Err(config::Error::FileError(io::Error::new(
|
||||
Err(config::Error::FileError(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"No suitable config file found.",
|
||||
)));
|
||||
)))?;
|
||||
}
|
||||
match config::load(&candidates) {
|
||||
// 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(pending) => cli::write_config(pending),
|
||||
Ok(pending) => {
|
||||
cli::write_config(pending)?;
|
||||
Ok(())
|
||||
}
|
||||
// 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
|
||||
|
@ -40,9 +43,10 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), config::Error> {
|
|||
// Make directories if necessary.
|
||||
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
|
||||
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![];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
221
src/path.rs
221
src/path.rs
|
@ -1,9 +1,51 @@
|
|||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use serde::de;
|
||||
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::io;
|
||||
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
|
||||
|
@ -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
|
||||
/// argument is not defined.
|
||||
fn expand_env(s: &OsStr) -> Option<OsString> {
|
||||
/// 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 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()?;
|
||||
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);
|
||||
}
|
||||
Some(path.into())
|
||||
Ok(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
|
||||
/// future (e.g. for paths with environment variables defined), this will
|
||||
/// return a `None` instead of an error.
|
||||
pub fn resolve(path: &Path) -> io::Result<Option<ResPathBuf>> {
|
||||
let mut expanded = 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 => {
|
||||
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),
|
||||
pub fn soft_resolve(path: &Path) -> Result<Option<ResPathBuf>> {
|
||||
match resolve(path) {
|
||||
Ok(resolved) => Ok(Some(resolved)),
|
||||
Err(Error::IOError(e)) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e @ Error::IOError(_)) => Err(e),
|
||||
// An ENV variable isn't defined yet, but we assume its possible it'll
|
||||
// be defined in the future. Don't report as an error.
|
||||
Err(Error::VarError(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue