shellock_homes/
lib.rs

1use anyhow::Result;
2use log::debug;
3use log::error;
4use log::warn;
5use path_helpers::{config_file_name, strip};
6use serde::{Deserialize, Serialize};
7use std::{
8    fs::{self, File},
9    io::Write,
10    path::PathBuf,
11};
12use thiserror::Error;
13use walkdir::WalkDir;
14
15mod constants;
16mod path_helpers;
17
18use constants::{CONFIG_FILE_NAME, SHELLOCK_HOMES};
19use path_helpers::{data_dir, flip_path, home_dir};
20
21#[derive(Debug, Error)]
22pub enum Error {
23    #[error("io error {e:?}")]
24    IO { e: std::io::Error },
25    #[error("directory error")]
26    Dir,
27    #[error("Could not read config file")]
28    Read,
29    #[error("Could not write config file")]
30    Write,
31    #[error("serialization error {e:?}")]
32    Serde { e: serde_json::Error },
33}
34
35pub enum Direction {
36    FromHome,
37    FromRepo,
38}
39
40pub trait Backend: Default {
41    fn init(&self) -> Result<()>;
42    fn sync(&self, direction: Direction, files: Vec<PathBuf>, ignored: Vec<PathBuf>) -> Result<()>;
43    fn list(&self) -> Vec<PathBuf>;
44}
45
46#[derive(Debug, Deserialize, Serialize)]
47pub struct SyncOptions {
48    pub files: Vec<PathBuf>,
49    pub ignore: Vec<PathBuf>,
50}
51
52impl SyncOptions {
53    fn default() -> Self {
54        let mut ignore: Vec<PathBuf> = Vec::new();
55        let p = PathBuf::from(".git");
56        ignore.push(p);
57        let mut files: Vec<PathBuf> = Vec::new();
58        let cfg = strip(config_file_name());
59        files.push(cfg);
60        SyncOptions { files, ignore }
61    }
62}
63
64#[derive(Debug, Deserialize, Serialize)]
65pub struct FileSystemBackend {
66    pub config_dir: PathBuf,
67    pub data_dir: PathBuf,
68}
69
70impl Default for FileSystemBackend {
71    fn default() -> Self {
72        // TODO implement for non-xdg platforms
73        let xdg_dirs = xdg::BaseDirectories::with_prefix(SHELLOCK_HOMES).unwrap();
74        // ensure directories exists
75        fs::create_dir_all(xdg_dirs.get_config_home()).unwrap();
76        fs::create_dir_all(xdg_dirs.get_data_home()).unwrap();
77
78        return FileSystemBackend {
79            config_dir: xdg_dirs.get_config_home(),
80            data_dir: xdg_dirs.get_data_home(),
81        };
82    }
83}
84
85impl Backend for FileSystemBackend {
86    fn init(&self) -> Result<()> {
87        debug!("init data dir: {:?}", data_dir());
88        fs::create_dir_all(data_dir())?;
89        Ok(())
90    }
91
92    fn sync(&self, direction: Direction, files: Vec<PathBuf>, ignored: Vec<PathBuf>) -> Result<()> {
93        match direction {
94            Direction::FromHome => sync(home_dir(), files, ignored),
95            Direction::FromRepo => sync(data_dir(), files, ignored),
96        }
97    }
98
99    fn list(&self) -> Vec<PathBuf> {
100        let mut files = Vec::new();
101        for f in WalkDir::new(data_dir()).into_iter().filter_map(|f| f.ok()) {
102            files.push(f.clone().into_path());
103        }
104
105        files
106    }
107}
108
109#[derive(Debug, Deserialize, Serialize)]
110pub struct Config<T: Backend> {
111    pub backend: T,
112    pub sync: SyncOptions,
113}
114
115pub trait ConfigWithBackend {
116    fn save(&self) -> Result<()>;
117    fn load(&mut self) -> Result<()>;
118    fn add_path(&mut self, source_path: Vec<PathBuf>) -> Result<()>;
119    fn remove_path(&mut self, backend_path: Vec<PathBuf>) -> Result<()>;
120    fn init(&self) -> Result<()>;
121}
122
123impl Config<FileSystemBackend> {
124    pub fn config_path(&self) -> PathBuf {
125        self.backend.config_dir.clone().join(CONFIG_FILE_NAME)
126    }
127
128    pub fn default() -> Self {
129        let backend = FileSystemBackend::default();
130        Config {
131            backend,
132            sync: SyncOptions::default(),
133        }
134    }
135
136    pub fn exists(&self) -> bool {
137        debug!("config path {:?}", self.config_path());
138        return self.config_path().as_path().exists();
139    }
140}
141
142impl ConfigWithBackend for Config<FileSystemBackend> {
143    fn save(&self) -> Result<()> {
144        let configs = vec![config_file_name(), flip_path(config_file_name())?];
145        for config_file in configs {
146            if !config_file.exists() {
147                let mut dir = config_file.clone();
148                dir.pop();
149                fs::create_dir_all(dir).unwrap();
150                File::create(&config_file).unwrap();
151            }
152            let content = serde_json::to_string_pretty(self)?;
153            let res = File::options()
154                .write(true)
155                .open(config_file)
156                .or(Err(Error::Write));
157
158            match res {
159                Ok(mut fh) => fh
160                    .write_all(content.as_bytes())
161                    .or_else(|e| Err(Error::IO { e }))?,
162                Err(e) => return Err(e.into()),
163            };
164        }
165        Ok(())
166    }
167
168    fn load(&mut self) -> Result<()> {
169        fs::read(self.config_path()).and_then(|bytes| {
170            let s = String::from_utf8(bytes).unwrap();
171            debug!("content: {}", s);
172            *self = serde_json::from_str(s.as_str()).unwrap();
173            Ok(())
174        })?;
175        Ok(())
176    }
177
178    fn add_path(&mut self, source_path: Vec<PathBuf>) -> Result<()> {
179        self.sync.files.extend(source_path);
180        self.save()
181    }
182
183    fn remove_path(&mut self, backend_path: Vec<PathBuf>) -> Result<()> {
184        self.sync.files.retain(|f| !backend_path.contains(f));
185        self.save()
186    }
187
188    fn init(&self) -> Result<()> {
189        debug!("initializing backend");
190        self.backend.init()?;
191        debug!("backend initialized");
192        if !self.config_path().as_path().exists() {
193            debug!("saving config");
194            return self.save();
195        }
196        debug!("config exists");
197        Ok(())
198    }
199}
200
201fn sync(source: PathBuf, files: Vec<PathBuf>, ignore: Vec<PathBuf>) -> Result<()> {
202    for file in files {
203        let source = source.join(file);
204        debug!("source: {:?}", source);
205        if source.is_file() {
206            let dest = flip_path(source.clone())?;
207            let base = dest.parent();
208            if base.is_some() {
209                fs::create_dir_all(base.unwrap())?;
210            }
211            fs::copy(source, dest)?;
212            continue;
213        }
214        debug!("walking");
215        for f in walkdir::WalkDir::new(&source).into_iter().filter(|f| {
216            debug!("filtering {:?}", f);
217            for p in &ignore {
218                debug!("checking {:?}", p);
219                match f.as_ref() {
220                    Ok(r) => {
221                        if r.path().starts_with(home_dir().join(p)) {
222                            debug!("ignoring {:?}", p);
223                            return false;
224                        }
225                    }
226                    Err(e) => {
227                        warn!("ignoring {:?} due to {:?}", p, e);
228                        return false;
229                    }
230                }
231            }
232            true
233        }) {
234            let entry = f.unwrap();
235            debug!("{:?} reached block", entry);
236            if entry.path().is_dir() {
237                debug!("{:?} is dir", entry);
238                let d = flip_path(entry.into_path()).unwrap();
239                debug!("dest: {:?}", d);
240                fs::create_dir_all(d).unwrap();
241            } else if entry.path().is_file() {
242                debug!("{:?} is file", entry);
243                let s = entry.clone().into_path();
244                let d = flip_path(entry.into_path()).unwrap();
245                debug!("source: {:?} dest: {:?}", s, d);
246                fs::copy(s, d).unwrap();
247            }
248        }
249    }
250
251    Ok(())
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use serial_test::serial;
258    use std::env;
259
260    const TEST_DATA_DIR: &str = "testdata";
261
262    #[test]
263    #[serial]
264    fn test_sync() {
265        env_logger::init();
266
267        let test_data = PathBuf::from(TEST_DATA_DIR).join(PathBuf::from("dir"));
268        let test_home = tempfile::tempdir().unwrap().into_path();
269        let files = vec![
270            PathBuf::from("a-file.txt"),
271            PathBuf::from("layer1/layer2/another-file.txt"),
272        ];
273        let ignore = vec![
274            PathBuf::from("ignored"),
275            PathBuf::from("layer1/ignore-me.txt"),
276        ];
277        env::set_var("HOME", test_home.clone().as_os_str());
278        env::set_var(
279            "XDG_DATA_HOME",
280            env::current_dir().unwrap().join(test_data.clone()),
281        );
282        debug!(
283            "HOME: {:?} XDG_DATA_HOME: {:?}",
284            env::var("HOME").unwrap(),
285            env::var("XDG_DATA_HOME").unwrap(),
286        );
287        debug!("home: {:?} data: {:?}", home_dir(), data_dir());
288        let res = sync(data_dir(), files.clone(), ignore.clone());
289        assert!(res.is_ok());
290
291        for elem in files {
292            let f = home_dir().join(elem);
293            assert!(f.exists());
294            assert!(f.is_file());
295        }
296
297        for i in ignore {
298            let ignored = home_dir().join(i);
299            assert!(!ignored.exists());
300        }
301    }
302}