use anyhow::Result;
use log::debug;
use log::error;
use log::warn;
use path_helpers::{config_file_name, strip};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
};
use thiserror::Error;
use walkdir::WalkDir;
mod constants;
mod path_helpers;
use constants::{CONFIG_FILE_NAME, SHELLOCK_HOMES};
use path_helpers::{data_dir, flip_path, home_dir};
#[derive(Debug, Error)]
pub enum Error {
#[error("io error {e:?}")]
IO { e: std::io::Error },
#[error("directory error")]
Dir,
#[error("Could not read config file")]
Read,
#[error("Could not write config file")]
Write,
#[error("serialization error {e:?}")]
Serde { e: serde_json::Error },
}
pub enum Direction {
FromHome,
FromRepo,
}
pub trait Backend: Default {
fn init(&self) -> Result<()>;
fn sync(&self, direction: Direction, files: Vec<PathBuf>, ignored: Vec<PathBuf>) -> Result<()>;
fn list(&self) -> Vec<PathBuf>;
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SyncOptions {
pub files: Vec<PathBuf>,
pub ignore: Vec<PathBuf>,
}
impl SyncOptions {
fn default() -> Self {
let mut ignore: Vec<PathBuf> = Vec::new();
let p = PathBuf::from(".git");
ignore.push(p);
let mut files: Vec<PathBuf> = Vec::new();
let cfg = strip(config_file_name());
files.push(cfg);
SyncOptions { files, ignore }
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FileSystemBackend {
pub config_dir: PathBuf,
pub data_dir: PathBuf,
}
impl Default for FileSystemBackend {
fn default() -> Self {
let xdg_dirs = xdg::BaseDirectories::with_prefix(SHELLOCK_HOMES).unwrap();
fs::create_dir_all(xdg_dirs.get_config_home()).unwrap();
fs::create_dir_all(xdg_dirs.get_data_home()).unwrap();
return FileSystemBackend {
config_dir: xdg_dirs.get_config_home(),
data_dir: xdg_dirs.get_data_home(),
};
}
}
impl Backend for FileSystemBackend {
fn init(&self) -> Result<()> {
debug!("init data dir: {:?}", data_dir());
fs::create_dir_all(data_dir())?;
Ok(())
}
fn sync(&self, direction: Direction, files: Vec<PathBuf>, ignored: Vec<PathBuf>) -> Result<()> {
match direction {
Direction::FromHome => sync(home_dir(), files, ignored),
Direction::FromRepo => sync(data_dir(), files, ignored),
}
}
fn list(&self) -> Vec<PathBuf> {
let mut files = Vec::new();
for f in WalkDir::new(data_dir()).into_iter().filter_map(|f| f.ok()) {
files.push(f.clone().into_path());
}
files
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Config<T: Backend> {
pub backend: T,
pub sync: SyncOptions,
}
pub trait ConfigWithBackend {
fn save(&self) -> Result<()>;
fn load(&mut self) -> Result<()>;
fn add_path(&mut self, source_path: Vec<PathBuf>) -> Result<()>;
fn remove_path(&mut self, backend_path: Vec<PathBuf>) -> Result<()>;
fn init(&self) -> Result<()>;
}
impl Config<FileSystemBackend> {
pub fn config_path(&self) -> PathBuf {
self.backend.config_dir.clone().join(CONFIG_FILE_NAME)
}
pub fn default() -> Self {
let backend = FileSystemBackend::default();
Config {
backend,
sync: SyncOptions::default(),
}
}
pub fn exists(&self) -> bool {
debug!("config path {:?}", self.config_path());
return self.config_path().as_path().exists();
}
}
impl ConfigWithBackend for Config<FileSystemBackend> {
fn save(&self) -> Result<()> {
let configs = vec![config_file_name(), flip_path(config_file_name())?];
for config_file in configs {
if !config_file.exists() {
let mut dir = config_file.clone();
dir.pop();
fs::create_dir_all(dir).unwrap();
File::create(&config_file).unwrap();
}
let content = serde_json::to_string_pretty(self)?;
let res = File::options()
.write(true)
.open(config_file)
.or(Err(Error::Write));
match res {
Ok(mut fh) => fh
.write_all(content.as_bytes())
.or_else(|e| Err(Error::IO { e }))?,
Err(e) => return Err(e.into()),
};
}
Ok(())
}
fn load(&mut self) -> Result<()> {
fs::read(self.config_path()).and_then(|bytes| {
let s = String::from_utf8(bytes).unwrap();
debug!("content: {}", s);
*self = serde_json::from_str(s.as_str()).unwrap();
Ok(())
})?;
Ok(())
}
fn add_path(&mut self, source_path: Vec<PathBuf>) -> Result<()> {
self.sync.files.extend(source_path);
self.save()
}
fn remove_path(&mut self, backend_path: Vec<PathBuf>) -> Result<()> {
self.sync.files.retain(|f| !backend_path.contains(f));
self.save()
}
fn init(&self) -> Result<()> {
debug!("initializing backend");
self.backend.init()?;
debug!("backend initialized");
if !self.config_path().as_path().exists() {
debug!("saving config");
return self.save();
}
debug!("config exists");
Ok(())
}
}
fn sync(source: PathBuf, files: Vec<PathBuf>, ignore: Vec<PathBuf>) -> Result<()> {
for file in files {
let source = source.join(file);
debug!("source: {:?}", source);
if source.is_file() {
let dest = flip_path(source.clone())?;
let base = dest.parent();
if base.is_some() {
fs::create_dir_all(base.unwrap())?;
}
fs::copy(source, dest)?;
continue;
}
debug!("walking");
for f in walkdir::WalkDir::new(&source).into_iter().filter(|f| {
debug!("filtering {:?}", f);
for p in &ignore {
debug!("checking {:?}", p);
match f.as_ref() {
Ok(r) => {
if r.path().starts_with(home_dir().join(p)) {
debug!("ignoring {:?}", p);
return false;
}
}
Err(e) => {
warn!("ignoring {:?} due to {:?}", p, e);
return false;
}
}
}
true
}) {
let entry = f.unwrap();
debug!("{:?} reached block", entry);
if entry.path().is_dir() {
debug!("{:?} is dir", entry);
let d = flip_path(entry.into_path()).unwrap();
debug!("dest: {:?}", d);
fs::create_dir_all(d).unwrap();
} else if entry.path().is_file() {
debug!("{:?} is file", entry);
let s = entry.clone().into_path();
let d = flip_path(entry.into_path()).unwrap();
debug!("source: {:?} dest: {:?}", s, d);
fs::copy(s, d).unwrap();
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
const TEST_DATA_DIR: &str = "testdata";
#[test]
#[serial]
fn test_sync() {
env_logger::init();
let test_data = PathBuf::from(TEST_DATA_DIR).join(PathBuf::from("dir"));
let test_home = tempfile::tempdir().unwrap().into_path();
let files = vec![
PathBuf::from("a-file.txt"),
PathBuf::from("layer1/layer2/another-file.txt"),
];
let ignore = vec![
PathBuf::from("ignored"),
PathBuf::from("layer1/ignore-me.txt"),
];
env::set_var("HOME", test_home.clone().as_os_str());
env::set_var(
"XDG_DATA_HOME",
env::current_dir().unwrap().join(test_data.clone()),
);
debug!(
"HOME: {:?} XDG_DATA_HOME: {:?}",
env::var("HOME").unwrap(),
env::var("XDG_DATA_HOME").unwrap(),
);
debug!("home: {:?} data: {:?}", home_dir(), data_dir());
let res = sync(data_dir(), files.clone(), ignore.clone());
assert!(res.is_ok());
for elem in files {
let f = home_dir().join(elem);
assert!(f.exists());
assert!(f.is_file());
}
for i in ignore {
let ignored = home_dir().join(i);
assert!(!ignored.exists());
}
}
}