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 let xdg_dirs = xdg::BaseDirectories::with_prefix(SHELLOCK_HOMES).unwrap();
74 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}