Consolidate config functions and keep `Config` values loose.
parent
223dfaf8a0
commit
d4183f2b52
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
remote: https://github.com/jrpotter/home-config.git
|
||||
local: ""
|
||||
remote: "https://github.com/jrpotter/home-config.git"
|
||||
packages:
|
||||
homesync:
|
||||
configs:
|
||||
|
|
142
src/cli.rs
142
src/cli.rs
|
@ -1,142 +0,0 @@
|
|||
use super::config::PathConfig;
|
||||
use super::{config, git, path};
|
||||
use ansi_term::Colour::{Green, Yellow};
|
||||
use std::env::VarError;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{error, fmt, io};
|
||||
use url::{ParseError, Url};
|
||||
|
||||
// TODO(jrpotter): Use curses to make this module behave nicer.
|
||||
|
||||
// ========================================
|
||||
// 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<git::Error> for Error {
|
||||
fn from(err: git::Error) -> Error {
|
||||
match err {
|
||||
git::Error::IOError(e) => Error::IOError(e),
|
||||
git::Error::VarError(e) => Error::VarError(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// ========================================
|
||||
|
||||
fn prompt_local(config: &PathConfig) -> Result<PathBuf> {
|
||||
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)?;
|
||||
Ok(PathBuf::from(path::expand_env(&local.trim())?))
|
||||
}
|
||||
|
||||
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",
|
||||
Green.paint(pending.0.unresolved().display().to_string())
|
||||
);
|
||||
let local = prompt_local(&pending)?;
|
||||
let remote = prompt_remote(&pending)?;
|
||||
// Try to initialize the local respository if we can.
|
||||
let resolved = git::init(&local, &pending)?;
|
||||
pending.1.local = Some(resolved);
|
||||
pending.1.remote = remote;
|
||||
pending.write()?;
|
||||
println!("\nFinished writing configuration file.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_packages(config: PathConfig) {
|
||||
println!(
|
||||
"Listing packages in {}...\n",
|
||||
Green.paint(config.0.unresolved().display().to_string())
|
||||
);
|
||||
// Alphabetical ordered ensured by B-tree implementation.
|
||||
for (k, _) in config.1.packages {
|
||||
println!("• {}", k);
|
||||
}
|
||||
}
|
137
src/config.rs
137
src/config.rs
|
@ -1,10 +1,13 @@
|
|||
use super::path;
|
||||
use super::path::ResPathBuf;
|
||||
use ansi_term::Colour::{Green, Yellow};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::env::VarError;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{error, fmt, fs, io};
|
||||
use url::Url;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
// ========================================
|
||||
// Error
|
||||
|
@ -14,14 +17,16 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
FileError(io::Error),
|
||||
IOError(io::Error),
|
||||
MissingConfig,
|
||||
ParseError(ParseError),
|
||||
SerdeError(serde_yaml::Error),
|
||||
VarError(VarError),
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::FileError(err)
|
||||
Error::IOError(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,12 +36,35 @@ impl From<serde_yaml::Error> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
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::FileError(e) => write!(f, "{}", e),
|
||||
Error::IOError(e) => write!(f, "{}", e),
|
||||
Error::MissingConfig => write!(f, "Could not find configuration file"),
|
||||
Error::ParseError(e) => write!(f, "{}", e),
|
||||
Error::SerdeError(e) => write!(f, "{}", e),
|
||||
Error::VarError(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +82,7 @@ pub struct Package {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub local: Option<ResPathBuf>,
|
||||
pub local: PathBuf,
|
||||
pub remote: Url,
|
||||
pub packages: BTreeMap<String, Package>,
|
||||
}
|
||||
|
@ -69,15 +97,8 @@ impl Config {
|
|||
pub struct PathConfig(pub ResPathBuf, pub Config);
|
||||
|
||||
impl PathConfig {
|
||||
pub fn new(path: &ResPathBuf, config: Option<Config>) -> Self {
|
||||
PathConfig(
|
||||
path.clone(),
|
||||
config.unwrap_or(Config {
|
||||
local: None,
|
||||
remote: Url::parse("http://github.com/user/repo.git").unwrap(),
|
||||
packages: BTreeMap::new(),
|
||||
}),
|
||||
)
|
||||
pub fn new(path: &ResPathBuf, config: Config) -> Self {
|
||||
PathConfig(path.clone(), config)
|
||||
}
|
||||
|
||||
// TODO(jrpotter): Create backup file before overwriting.
|
||||
|
@ -110,10 +131,10 @@ pub fn load(candidates: &Vec<ResPathBuf>) -> Result<PathConfig> {
|
|||
for candidate in candidates {
|
||||
match fs::read_to_string(candidate) {
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(Error::FileError(err)),
|
||||
Err(err) => Err(Error::IOError(err))?,
|
||||
Ok(contents) => {
|
||||
let config = Config::new(&contents)?;
|
||||
return Ok(PathConfig::new(candidate, Some(config)));
|
||||
return Ok(PathConfig::new(candidate, config));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,3 +146,85 @@ pub fn reload(config: &PathConfig) -> Result<PathConfig> {
|
|||
println!("Configuration reloaded.");
|
||||
load(&vec![config.0.clone()])
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Creation
|
||||
// ========================================
|
||||
|
||||
fn prompt_local(path: Option<&Path>) -> Result<PathBuf> {
|
||||
let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string());
|
||||
print!(
|
||||
"Local git repository <{}> (enter to continue): ",
|
||||
Yellow.paint(&default)
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut local = String::new();
|
||||
io::stdin().read_line(&mut local)?;
|
||||
// Defer validation this path until initialization of the repository.
|
||||
let local = local.trim();
|
||||
if local.is_empty() {
|
||||
Ok(PathBuf::from(default))
|
||||
} else {
|
||||
Ok(PathBuf::from(local))
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_remote(url: Option<&Url>) -> Result<Url> {
|
||||
let default = url.map_or("https://github.com/owner/repo.git".to_owned(), |u| {
|
||||
u.to_string()
|
||||
});
|
||||
print!(
|
||||
"Remote git repository <{}> (enter to continue): ",
|
||||
Yellow.paint(&default)
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut remote = String::new();
|
||||
io::stdin().read_line(&mut remote)?;
|
||||
let remote = remote.trim();
|
||||
if remote.is_empty() {
|
||||
Ok(Url::parse(&default)?)
|
||||
} else {
|
||||
Ok(Url::parse(&remote)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
|
||||
println!(
|
||||
"Generating config at {}...\n",
|
||||
Green.paint(path.unresolved().display().to_string())
|
||||
);
|
||||
let local = prompt_local(match &loaded {
|
||||
Some(c) => Some(c.local.as_ref()),
|
||||
None => None,
|
||||
})?;
|
||||
let remote = prompt_remote(match &loaded {
|
||||
Some(c) => Some(&c.remote),
|
||||
None => None,
|
||||
})?;
|
||||
let generated = PathConfig(
|
||||
path.clone(),
|
||||
Config {
|
||||
local,
|
||||
remote,
|
||||
packages: loaded.map_or(BTreeMap::new(), |c| c.packages),
|
||||
},
|
||||
);
|
||||
generated.write()?;
|
||||
println!("\nFinished writing configuration file.");
|
||||
Ok(generated)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Listing
|
||||
// ========================================
|
||||
|
||||
pub fn list_packages(config: PathConfig) {
|
||||
println!(
|
||||
"Listing packages in {}...\n",
|
||||
Green.paint(config.0.unresolved().display().to_string())
|
||||
);
|
||||
// Alphabetical ordered ensured by B-tree implementation.
|
||||
for (k, _) in config.1.packages {
|
||||
println!("• {}", k);
|
||||
}
|
||||
}
|
||||
|
|
30
src/git.rs
30
src/git.rs
|
@ -53,33 +53,9 @@ impl error::Error for Error {}
|
|||
// Validation
|
||||
// ========================================
|
||||
|
||||
fn validate_is_file(path: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
if !metadata.is_file() {
|
||||
// TODO(jrpotter): Use `IsADirectory` when stable.
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("'{}' is not a file.", path.display()),
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_is_dir(path: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
if !metadata.is_dir() {
|
||||
// TODO(jrpotter): Use `NotADirectory` when stable.
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("'{}' is not a directory.", path.display()),
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_local(path: &Path) -> Result<()> {
|
||||
let resolved = path::resolve(path)?;
|
||||
validate_is_dir(resolved.as_ref())?;
|
||||
path::validate_is_dir(resolved.as_ref())?;
|
||||
|
||||
let mut local: PathBuf = resolved.into();
|
||||
local.push(".git");
|
||||
|
@ -92,7 +68,7 @@ pub fn validate_local(path: &Path) -> Result<()> {
|
|||
),
|
||||
)
|
||||
})?;
|
||||
validate_is_dir(local.as_ref())?;
|
||||
path::validate_is_dir(local.as_ref())?;
|
||||
|
||||
local.pop();
|
||||
local.push(".homesync");
|
||||
|
@ -105,7 +81,7 @@ pub fn validate_local(path: &Path) -> Result<()> {
|
|||
),
|
||||
)
|
||||
})?;
|
||||
validate_is_file(local.as_ref())?;
|
||||
path::validate_is_file(local.as_ref())?;
|
||||
|
||||
// TODO(jrpotter): Verify git repository is pointing to remote.
|
||||
|
||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,4 +1,3 @@
|
|||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod daemon;
|
||||
pub mod git;
|
||||
|
@ -22,7 +21,7 @@ pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
|
|||
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() {
|
||||
Err(config::Error::FileError(io::Error::new(
|
||||
Err(config::Error::IOError(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"No suitable config file found.",
|
||||
)))?;
|
||||
|
@ -31,8 +30,8 @@ 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(pending) => {
|
||||
cli::write_config(pending)?;
|
||||
Ok(loaded) => {
|
||||
config::write(&loaded.0, Some(loaded.1))?;
|
||||
Ok(())
|
||||
}
|
||||
// Otherwise create a new config file at the given location. We always
|
||||
|
@ -42,8 +41,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
|||
// TODO(jrpotter): Verify I have permission to write at specified path.
|
||||
// Make directories if necessary.
|
||||
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
|
||||
let pending = PathConfig::new(&candidates[0], None);
|
||||
cli::write_config(pending)?;
|
||||
config::write(&candidates[0], None)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)?,
|
||||
|
@ -51,7 +49,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
|
||||
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
|
||||
cli::list_packages(config);
|
||||
config::list_packages(config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -60,9 +60,6 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
|||
// used, even if one of higher priority is eventually defined.
|
||||
subcommand => {
|
||||
let config = homesync::config::load(&candidates)?;
|
||||
if let Some(local) = &config.1.local {
|
||||
homesync::git::validate_local(local.as_ref())?;
|
||||
}
|
||||
match subcommand {
|
||||
Some(("add", _)) => Ok(homesync::run_add(config)?),
|
||||
Some(("daemon", matches)) => {
|
||||
|
|
30
src/path.rs
30
src/path.rs
|
@ -6,7 +6,7 @@ use std::env::VarError;
|
|||
use std::ffi::OsString;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::{env, error, fmt, io, result, str};
|
||||
use std::{env, error, fmt, fs, io, result, str};
|
||||
|
||||
// ========================================
|
||||
// Error
|
||||
|
@ -171,6 +171,34 @@ impl<'de> Deserialize<'de> for ResPathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Validation
|
||||
// ========================================
|
||||
|
||||
pub fn validate_is_file(path: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
if !metadata.is_file() {
|
||||
// TODO(jrpotter): Use `IsADirectory` when stable.
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("'{}' is not a file.", path.display()),
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_is_dir(path: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
if !metadata.is_dir() {
|
||||
// TODO(jrpotter): Use `NotADirectory` when stable.
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("'{}' is not a directory.", path.display()),
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Resolution
|
||||
// ========================================
|
||||
|
|
Loading…
Reference in New Issue